概述
在开发证券交易系统两腿套利功能的时候,对于每一条两腿套利监控,都需要有一个状态变迁维护。最初的实现没有使用状态机,但是定义并支持了停止、正在监控、正在执行、已执行四种状态。后来面临设计更改,需要增加一种“暂停”状态。为了简化代码,提高可读性,实现了FSM状态机。
因为以前在通信底层嵌入式系统中对于TCP/IP的协议栈的状态机处理有过经验,便自行动手实现了状态机。实现之后,调研了网上关于C#实现状态机的方式,发现有很多新的技术可以用于简化并优化状态机设计。尤其是了解到,状态机和“行为树Behavior Tree (BT)”的发展和关系。之前未接触过行为树,此篇也着重在状态机,希望下一次可以找时间探讨行为树的应用。
Introduction of arbitrage trading
An arbitrage trading has 2 legs, which acn be executed simultaneously, or one after another (successively).
每个套利有两个参考品种,当价差达到预定条件时自动触发执行。
when created, it is in Stopped state, user can start Monitoring, or stop executing.
<pre>
public enum ArbitrageStatus
{
Monitoring,
Executing,
Executed,
Stopped,
Paused
}
</pre>
第一种实现方式
<pre>
#region FSM 状态机
private bool FsmSetStatus(Arbitrage arbitrage, ArbitrageStatus to)
{
switch (arbitrage.ArbitrageStatus)
{
case ArbitrageStatus.暂停:
switch (to)
{
// 暂停状态->正在监控时,直接尝试转入正在执行状态
// 从暂停恢复,但是不允许从暂停状态立即执行
case ArbitrageStatus.正在监控:
return FsmFrom暂停to正在监控(arbitrage);
case ArbitrageStatus.停止:
return FsmFrom暂停to停止(arbitrage);
case ArbitrageStatus.已执行:
return FsmFrom暂停to已执行(arbitrage);
}
break;
case ArbitrageStatus.停止:
switch (to)
{
// 停止状态只允许转变到正在监控状态
case ArbitrageStatus.正在监控:
return FsmFrom停止to正在监控(arbitrage);
}
break;
case ArbitrageStatus.已执行:
switch (to)
{
case ArbitrageStatus.停止:
return FsmFrom已执行to停止(arbitrage);
}
break;
case ArbitrageStatus.正在执行:
switch (to)
{
case ArbitrageStatus.正在监控:
case ArbitrageStatus.已执行:
return FsmFrom正在执行to正在监控已执行(arbitrage);
case ArbitrageStatus.暂停:
return FsmFrom正在执行to暂停(arbitrage);
case ArbitrageStatus.停止:
return FsmFrom正在执行to停止(arbitrage);
}
break;
case ArbitrageStatus.正在监控:
switch (to)
{
case ArbitrageStatus.暂停:
return FsmFrom正在监控to暂停(arbitrage);
case ArbitrageStatus.停止:
return FsmFrom正在监控to停止(arbitrage);
case ArbitrageStatus.正在执行:
return FsmFrom正在监控to正在执行(arbitrage);
case ArbitrageStatus.正在监控:
return true;
}
break;
}
return false;
}
/// <summary>
/// 从暂停恢复,但是不允许从暂停再到立即执行。
/// </summary>
/// <param name="arbitrage"></param>
/// <returns></returns>
private bool FsmFrom暂停to正在监控(Arbitrage arbitrage)
{
if (UnfinishedEntrustCounter(arbitrage) == 0)
{
arbitrage.ArbitrageStatus = ArbitrageStatus.正在监控;
return true;
}
arbitrage.ArbitrageStatus = ArbitrageStatus.正在执行;
return true;
}
private bool FsmFrom暂停to停止(Arbitrage arbitrage)
{
// 执行次数加1,清除委托统计信息。
arbitrage.ExecutionTimesDone++;
ClearEntrustCounter(arbitrage);
arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
return true;
}
private bool FsmFrom暂停to已执行(Arbitrage arbitrage)
{
// 执行次数加1,清除委托统计信息。
arbitrage.ExecutionTimesDone++;
ClearEntrustCounter(arbitrage);
if(arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
arbitrage.ArbitrageStatus = ArbitrageStatus.已执行;
return true;
}
private bool FsmFrom正在监控to暂停(Arbitrage arbitrage)
{
arbitrage.ArbitrageStatus = ArbitrageStatus.暂停;
return true;
}
private bool FsmFrom正在监控to停止(Arbitrage arbitrage)
{
arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
return true;
}
private bool FsmFrom正在监控to正在执行(Arbitrage arbitrage)
{
if (!arbitrage.IgnoreCheckOnce)
{
if (!TsabUtility.CheckPriceContition(arbitrage))
return false;
}
else
{
arbitrage.IgnoreCheckOnce = false;
}
List<List<Entrust>> legEntrusts;
if (NextArbitrageBaskets(arbitrage, out legEntrusts))
{
arbitrage.ArbitrageStatus = ArbitrageStatus.正在执行;
// issue
LegEntrustsHandler(arbitrage, legEntrusts);
}
return true;
}
/// <summary>
/// 从正在执行状态的目标状态是“已执行”,“正在监控”是中间状态。
/// </summary>
/// <param name="arbitrage"></param>
/// <returns></returns>
private bool FsmFrom正在执行to正在监控已执行(Arbitrage arbitrage)
{
if (!FailedEntrustCheck(arbitrage))
{
ManualStopArbitrage(arbitrage, true);
_lastError = "委托废单超出监控限定值,自动停止两腿套利!";
if (AlarmHandler != null)
{
AlarmHandler(arbitrage);
}
ClearEntrustCounter(arbitrage);
arbitrage.NeedWaitNextLeg = false;
arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
return true;
}
if (UnfinishedEntrustCounter(arbitrage) == 0)
{
ClearEntrustCounter(arbitrage);
if (!arbitrage.NeedWaitNextLeg)
{
arbitrage.ExecutionTimesDone++;
if (arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
{
ClearEntrustCounter(arbitrage);
arbitrage.ArbitrageStatus = ArbitrageStatus.已执行;
return true;
}
}
if (arbitrage.NeedWaitNextLeg || TsabUtility.CheckPriceContition(arbitrage))
{
// 发送下一腿或下一轮
List<List<Entrust>> legEntrusts;
if (NextArbitrageBaskets(arbitrage, out legEntrusts))
{
// issue
LegEntrustsHandler(arbitrage, legEntrusts);
}
}
else
{
arbitrage.ArbitrageStatus = ArbitrageStatus.正在监控;
}
}
return true;
}
private bool FsmFrom正在执行to暂停(Arbitrage arbitrage)
{
arbitrage.ArbitrageStatus = ArbitrageStatus.暂停;
return true;
}
private bool FsmFrom正在执行to停止(Arbitrage arbitrage)
{
// 执行次数加1,清除委托统计信息。
arbitrage.ExecutionTimesDone++;
ClearEntrustCounter(arbitrage);
arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
return true;
}
private bool FsmFrom已执行to停止(Arbitrage arbitrage)
{
arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
return true;
}
private bool FsmFrom停止to正在监控(Arbitrage arbitrage)
{
if (arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
{
_lastError = "已经超过计划执行次数!" + arbitrage;
return false;
}
arbitrage.ArbitrageStatus = ArbitrageStatus.正在监控;
return true;
}
#endregion
</pre>
这种方式没有把Command或Event的概念直接独立出来,调用接口使用过选择下一个状态来触发状态变迁,也就是说没有将“状态变化”封装在状态机内部。
调用方式:
<pre>
public bool ManualStartArbitrage(Arbitrage arbitrage, bool forceIssueNow)
{
var exists = ArbitrageList.Where(p => p.Id == arbitrage.Id);
if (!exists.Any())
{
_lastError = "启动两腿套利监控失败: 不存在的Id:" + arbitrage.Id;
return false;
}
if (arbitrage.ExecutionTimesPlanned <= arbitrage.ExecutionTimesDone)
{
_lastError = "两腿监控策略已经达到最大执行次数!";
return false;
}
if (!FsmSetStatus(arbitrage, ArbitrageStatus.正在监控))
{
_lastError = GetLastError();
return false;
}
// 如果满足条件,应立即执行
if (forceIssueNow || TsabUtility.CheckPriceContition(arbitrage))
{
arbitrage.IgnoreCheckOnce = true;
var ret = FsmSetStatus(arbitrage, ArbitrageStatus.正在执行);
arbitrage.IgnoreCheckOnce = false;
return ret;
}
return true;
}
</pre>
选择的参考实现方式
第一种参考实现方式
第一种参考实现方式来自于Stackoverflow,明确定义Command,通过状态变迁字典存储由Command触发状态变化的动作。
Let's start with this simple state diagram:
![simple state machine diagram][1]
We have:
- 4 states (Inactive, Active, Paused, and Exited)
- 5 types of state transitions (Begin Command, End Command, Pause Command, Resume Command, Exit Command).
You can convert this to C# in a handful of ways, such as performing a switch statement on the current state and command, or looking up transitions in a transition table. For this simple state machine, I prefer a transition table, which is very easy to represent using a Dictionary
:
using System;
using System.Collections.Generic;
namespace Juliet
{
public enum ProcessState
{
Inactive,
Active,
Paused,
Terminated
}
public enum Command
{
Begin,
End,
Pause,
Resume,
Exit
}
public class Process
{
class StateTransition
{
readonly ProcessState CurrentState;
readonly Command Command;
public StateTransition(ProcessState currentState, Command command)
{
CurrentState = currentState;
Command = command;
}
public override int GetHashCode()
{
return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
}
public override bool Equals(object obj)
{
StateTransition other = obj as StateTransition;
return other != null && this.CurrentState == other.CurrentState && this.Command == other.Command;
}
}
Dictionary<StateTransition, ProcessState> transitions;
public ProcessState CurrentState { get; private set; }
public Process()
{
CurrentState = ProcessState.Inactive;
transitions = new Dictionary<StateTransition, ProcessState>
{
{ new StateTransition(ProcessState.Inactive, Command.Exit), ProcessState.Terminated },
{ new StateTransition(ProcessState.Inactive, Command.Begin), ProcessState.Active },
{ new StateTransition(ProcessState.Active, Command.End), ProcessState.Inactive },
{ new StateTransition(ProcessState.Active, Command.Pause), ProcessState.Paused },
{ new StateTransition(ProcessState.Paused, Command.End), ProcessState.Inactive },
{ new StateTransition(ProcessState.Paused, Command.Resume), ProcessState.Active }
};
}
public ProcessState GetNext(Command command)
{
StateTransition transition = new StateTransition(CurrentState, command);
ProcessState nextState;
if (!transitions.TryGetValue(transition, out nextState))
throw new Exception("Invalid transition: " + CurrentState + " -> " + command);
return nextState;
}
public ProcessState MoveNext(Command command)
{
CurrentState = GetNext(command);
return CurrentState;
}
}
public class Program
{
static void Main(string[] args)
{
Process p = new Process();
Console.WriteLine("Current State = " + p.CurrentState);
Console.WriteLine("Command.Begin: Current State = " + p.MoveNext(Command.Begin));
Console.WriteLine("Command.Pause: Current State = " + p.MoveNext(Command.Pause));
Console.WriteLine("Command.End: Current State = " + p.MoveNext(Command.End));
Console.WriteLine("Command.Exit: Current State = " + p.MoveNext(Command.Exit));
Console.ReadLine();
}
}
}
As a matter of personal preference, I like to design my state machines with a GetNext
function to return the next state <a href="http://en.wikipedia.org/wiki/Pure_function">deterministically</a>, and a MoveNext
function to mutate the state machine.
第二种参考实现方式
使用C#语言的标签机制,实现了通用的状态机代码。
Sidneys1/GFSM
A Generic Finite State Machine in C#
Implementation is easy:
<pre>
// Entirely unnecessary, just here to implement a common DoWork()
public abstract class MyStateBase : IState {
public virtual void Enter() {}
public abstract void DoWork();
public virtual void Leave() {}
}
// Define a state and it's transitions
[Transition("next", typeof(EndState))]
public class StartState : MyStateBase {
public override void DoWork() => Console.WriteLine("In StartState");
public override void Leave() => Console.WriteLine("\tLeaving StartState");
}
[Transition("next", null)]
public class EndState : MyStateBase {
public override void Enter() => Console.WriteLine("\tEntered EndState");
public override void DoWork() => Console.WriteLine("In EndState");
public override void Leave() => Console.WriteLine("\tLeaving EndState");
}
internal class Program {
private static void Main() {
var fsm = new FiniteStateMachine<StartState>();
fsm.Transitioning += transition => Console.WriteLine($"Beginning transition: {transition}");
fsm.Transitioned += transition => {
Console.WriteLine($"Done transitioning: {transition}");
if (transition.To == null)
Console.WriteLine("\nExited");
};
Console.WriteLine("Started\n");
fsm.GetCurrentState<MyStateBase>().DoWork();
fsm.DoTransition("next");
fsm.GetCurrentState<MyStateBase>().DoWork();
fsm.DoTransition("next");
Console.ReadLine();
}
}
/*
Will print:
Started
In StartState
Beginning transition: DemoApp.StartState + 'next' = DemoApp.EndState
Leaving StartState
Entered EndState
Done transitioning: DemoApp.StartState + 'next' = DemoApp.EndState
In EndState
Beginning transition: DemoApp.EndState + 'next' = null
Leaving EndState
Done transitioning: DemoApp.EndState + 'next' = null
Exited
*/
</pre>
第三种参考实现方式
Some shameless self-promo here, but a while ago I created a library called YieldMachine which allows a limited-complexity state machine to be described in a very clean and simple way. For example, consider a lamp:
Notice that this state machine has 2 triggers and 3 states. In YieldMachine code, we write a single method for all state-related behavior, in which we commit the horrible atrocity of using goto
for each state. A trigger becomes a property or field of type Action
, decorated with an attribute called Trigger
. I've commented the code of the first state and its transitions below; the next states follow the same pattern.
public class Lamp : StateMachine
{
// Triggers (or events, or actions, whatever) that our
// state machine understands.
[Trigger]
public readonly Action PressSwitch;
[Trigger]
public readonly Action GotError;
// Actual state machine logic
protected override IEnumerable WalkStates()
{
off:
Console.WriteLine("off.");
yield return null;
if (Trigger == PressSwitch) goto on;
InvalidTrigger();
on:
Console.WriteLine("*shiiine!*");
yield return null;
if (Trigger == GotError) goto error;
if (Trigger == PressSwitch) goto off;
InvalidTrigger();
error:
Console.WriteLine("-err-");
yield return null;
if (Trigger == PressSwitch) goto off;
InvalidTrigger();
}
}
Short and nice, eh!
This state machine is controlled simply by sending triggers to it:
var sm = new Lamp();
sm.PressSwitch(); //go on
sm.PressSwitch(); //go off
sm.PressSwitch(); //go on
sm.GotError(); //get error
sm.PressSwitch(); //go off
Just to clarify, I've added some comments to the first state to help you understand how to use this.
protected override IEnumerable WalkStates()
{
off: // Each goto label is a state
Console.WriteLine("off."); // State entry actions
yield return null; // This means "Wait until a
// trigger is called"
// Ah, we got triggered!
// perform state exit actions
// (none, in this case)
if (Trigger == PressSwitch) goto on; // Transitions go here:
// depending on the trigger
// that was called, go to
// the right state
InvalidTrigger(); // Throw exception on
// invalid trigger
...
This works because the C# compiler actually created a state machine internally for each method that uses yield return
. This construct is usually used to lazily create sequences of data, but in this case we're not actually interested in the returned sequence (which is all nulls anyway), but in the state behaviour that gets created under the hood.
The StateMachine
base class does some reflection on construction to assign code to each [Trigger]
action, which sets the Trigger
member and moves the state machine forward.
But you don't really need to understand the internals to be able to use it.
第四种参考实现方式
OmerMor/StateMachineToolkit
A generic state-machine framework, with support for active/passive machines, exposed events and rich exception handling.
通过注册EntryHandler
和ExitHandler
定义状态Entry/Exit Methods
,通过AddTransition
提供状态变迁和Action Methods。
第四种参考实现方式
Real-Serious-Games/Fluent-State-Machine
支持子状态嵌套。
不需要预先定义状态枚举值,通过字符串定义,非常灵活。
设计考量
不管是否利用了C#的语言特性,最重要的应该是从不同的实现方式中找到共性。
状态机的功能(不考虑语言特性,如Attribute等方式):
- 多种状态之间变迁:定义两两可转换状态的映射(字典、二维数组等)
- 是否支持active/passive machines(参考方式四)
- 通过Command/Event触发状态变迁:将所有状态转换逻辑都封装在内部,通过Command触发变化。
- hierarchical state machine?是否支持状态内部子状态。例如两腿套利时,两腿分开发送,需要等到第一腿的委托执行完毕之后再发送下一腿,此时状态仍然处于“正在执行”状态,仅将“等待下一腿”标识清空。
Eric Lippert在SO的一个问题中回答说:
If you want to write a state machine, just write a state machine. It's not hard. If you want to write a lot of state machines, write a library of useful helper methods that let you cleanly represent state machines, and then use your library. But don't abuse a language construct intended for something completely different that just happens to use state machines as an implementation detail. That makes your state machine code hard to read, understand, debug, maintain and extend.
(And incidentally, I did a double-take when reading your name. One of the designers of C# is also named Matt Warren!)
需要状态机的时候,直接去写。如果经常需要写状态机,可以自己建立一个库。千万不要太拘泥或追求形式,因为状态机并不复杂。
在我的第一种实现中,并没有考虑Command/Event,直接通过提供“设置下一状态”来实现状态变迁。这里是存在一些问题的:例如有些状态调用时有多种可能的下一种目标状态,这必须由外部Caller来明确进行多次调用SetStatus函数。最好的方式还是应该简单通过Command来改变状态。加入Command的设计其实就是封装了这种调用逻辑。