策略开发指南

本章将为您介绍如何开发交易策略

Previous page Next page

ThunderTrader支持两种类型的策略,

  • Native策略
  • Remote策略

Native策略采用与Engine相同的C++语言开发,以动态链接库的形式被Engine载入到进程空间中执行,与engine共享内存地址,是延迟最低,性能最佳的策略实现方案,适合高频交易场景。而Remote策略则可以使用Java/Python/Golang/Rust等任意语言进行开发,策略以独立的进程存在,engine通过HTTP协议调用Remote策略进程实现交易。

  Native策略 Remote策略
开发语言 c++ 任意编程语言
延迟 网络延迟以及数据序列化延迟
性能 根据语言特性决定
开发成本
故障恢复 支持 不支持
开发依赖 需要在DevelStudio中编译&链接 无外部依赖,任何环境中可开发
运行依赖 编译好的策略文件上传到Engine中执行 任何机器上可执行(通过HTTP协议进行通信)

概念介绍

  • Context:回调函数上下文接口,engine会为策略的不同回调函数中提供不同的context,通过这种方式,赋予策略访问engine获取信息、执行指令的能力。
  • Primary reference ID:第一报单ID。通常是策略在执行报单指令后立即产生的报单ID。策略通常会保存该ID方便后续继续对报单进行操作。
  • Secondary reference ID:第二报单ID。在交易所收到策略发送的报单指令后,会生成第二报单ID并回传到策略。该ID会在执行撤销报单指令时使用到,策略需要自行进行保存。
  • Timer:定时器。为策略提供延迟操作的能力。
  • Thunder devel: ThunderTrader开发套件。该套件由一系列头文件以及源代码组成,位于DevelStudio中的/thunder-devel/文件夹下,您可基于该套件进行行情/交易插件、策略、行情录制插件等开发。

Native策略指南

我们通过一个名叫Strategy101的策略,向您展示Native策略的基本运行逻辑。其中包含了行情接收、提交报单、自定义指标、自定义干预命令、State等基本功能的使用方法。该策略在DevelStudio的/strategy101文件夹下,包含strategy.hstrategy.cpp两个文件,您可使用网页版VSCode直接进行编译。

每个Native策略都必须继承于::thunder::strategy::strategy_base_imp_t,该类定义在DevelStudio中的/thunder-devel/strategy/strategy_base_imp.h文件中。策略需要实现基类中相关接口从而实现策略具体的算法。

接口实现

virtual std::string get_description(environment_context_t&) = 0;

get_description接口需要返回策略的相关描述,这是一个std::string类型的字符串,该描述会展示在Engine控制面板中。

virtual void on_initialization(initialization_context_t &) = 0;

on_initialization接口用于策略实现初始化,策略通常在该方法中对相关的数据进行初始化。在该函数被调用时,策略还尚未被挂在到Engine列表中。 initialization_context_t是该接口的上下文,其中有is_from_archive函数的返回值表示当前策略是否为从故障中恢复。

virtual void on_deployment(deployment_context_t &, runtime_context_t &) = 0;

如果on_initialization函数未抛出异常,则策略会被挂在到engine的策略列表中,并开始推送行情数据,此时on_deployment函数会被执行。在on_deployment函数中,通常需要保存runtime_context_t指针,后续接口中进行报单相关操作均依赖该指针。 deployment_context_t上下文指针仅限于在on_deployment函数中使用,其内含了logger_tstrategy_meta_context_tstrategy_state_manager_tparameter_access_context_t以及position_access_context_t上下文,并额外支持下列接口:

name description
get_slot_count 获取slot个数,即当前策略配置的合约的数量
get_price_tick 获取slot对应的价格最小跳动点
get_volume_multiple 获取slot对应的合约乘数
get_margin_ratio 获取slot对应的保证金比例
query_instrument 获取slot对应合约的更多信息

runtime_context_t上下文可以被保存,并在后续任何其他回调函数中使用,这也是最为常用的上下文,内置了下单、撤单、设置定时器等最常见的功能,可以在API索引中了解更加详细的说明。

virtual void on_exit(exit_context_t &) = 0;

当策略被撤销时,on_exit函数会被回调。

virtual void on_tick(trade_slot_t, const ::thunder::tick_t &) = 0;

当收到来自交易所的行情数据时,on_tick函数会被触发。策略通常会在on_tick函数中更新相关指标,并执行相应的交易操作。tick_t是通用行情tick数据结构,其定义可以在DevelStudio/thunder-devel/tick.h中找到。除大部分公用字段以外,不同的合约类型可能会有独有的数据字段,这些字段都可以在成员变量extension中找到。

virtual void on_order_trade(const order_trade_event_t &) = 0;

当收到来自交易所的报单成交事件时,engine会通过on_order_trade回调函数通知策略。order_trade_event_t包含了关于本次成交的所有信息,该数据结构的详细定义可以在DevelStudio/thunder-devel/strategy/strategy_event.h中找到,同样,合约独有的字段保存在在成员变量extension中。

virtual void on_order_progress(const order_progress_event_t &) = 0;

当收到来自交易所的报单变化事件时,engine会通过on_order_progress回调函数通知策略。该事件通常是在报单有新的成交时产生。例如当一次报单分批成交时,每笔成交都会产生相应的order_progress_event_t事件。

virtual void on_order_state_change(const order_state_change_event_t &) = 0;

当订单状态发生变化时,交易所会通过该回调函数通知engine,例如报单处于异常状态,或者报单已经撤销等。

virtual void on_secondary_ref(const secondary_order_ref_event_t &) = 0;

当交易所收到策略的报单指令后,会生成secondary_order_reference(即第二报单ID)并回传给策略。该ID会在撤销报单时使用到,策略应单在on_secondary_ref回调函数中自行保存该ID。

自定义策略参数

您可以为策略添加自定义参数,在engine控制面板进行策略部署时可以调整这些参数的值。

thunder-strategy-parameters.png

我们通过BEGIN_PARAMETER_BINDEND_PARAMETER_BIND指令定义参数,engine支持int64/double/string三种类型的参数:

method description
INT_PARAMETER 定义一个int64类型的参数
DOUBLE_PARAMETER 定义一个double类型的参数
STRING_PARAMETER 定义一个string类型的参数

例如:

BEGIN_PARAMETER_BIND
    INT_PARAMETER("a", "parameter a is a int64_t", 5)
    DOUBLE_PARAMETER("b", "parameter b is a double", 0.2)
    STRING_PARAMETER("c", "parameter c is a std::string", "1,2,3,4")
    INT_PARAMETER("enable_order_test", "Make test order automatic if value is 1", 0)
END_PARAMETER_BIND

每一个参数定义指令都有三个参数,分别是参数名称,参数描述以及参数的默认值。在策略中,我们可以同上下文的int_parameterdouble_parameter以及string_parameter三个函数访问参数的最终值:

ctx = &runtime_ctx;
ctx->logging(severity_levels::info, "runtime value of parameter `a` is `%ld`", ctx->int_parameter("a"));
ctx->logging(severity_levels::info, "runtime value of parameter `b` is `%lf`", ctx->double_parameter("b"));
ctx->logging(severity_levels::info, "runtime value of parameter `c` is `%s`", ctx->string_parameter("c").c_str());
ctx->logging(severity_levels::info, "runtime value of parameter `enable_order_test` is `%ld`", ctx->int_parameter("enable_order_test"));

自定义指标

自定义指标支持将您策略中的变量实时导出到engine控制面板,以便您能够实时观测策略运行状态,判断策略运行是否符合预期。 thunder-engine-strategy-dashboard.png

定义指标,每个指标的类型必须为std::atomic<double>

std::atomic<double> ask_price = NAN;
std::atomic<double> ask_price = NAN;
std::atomic<double> ask_volume = NAN;
std::atomic<double> bid_volume = NAN;
std::atomic<double> tick_count = NAN;
std::atomic<double> avg_price = NAN;

策略应当在回调函数中保持跟新定义好的指标。同时,我们需要将指标进行导出,并配置engine看板的布局。

BEGIN_DASHBOARD
    BEGIN_PANEL
        METRIC_EX2(&ask_price, "ask_price", color_t::GREEN, 0.8, metric_y_axis_t::RIGHT)
        METRIC_EX2(&bid_price, "bid_price", color_t::BLUE, 0.8, metric_y_axis_t::RIGHT)
    END_PANEL(R"({"row":[0,0], "column":[0,0]})")
    BEGIN_PANEL
        METRIC_EX2(&ask_volume, "ask_volume", color_t::GREEN,0.8, metric_y_axis_t::RIGHT)
        METRIC_EX2(&bid_volume, "bid_volume", color_t::BLUE,0.8, metric_y_axis_t::RIGHT)
    END_PANEL(R"({"row":[0,0], "column":[1,2]})")
    BEGIN_PANEL
        METRIC(&tick_count, "tick_count", color_t::GREEN)
    END_PANEL(R"({"row":[1,2], "column":[0,1]})")
    BEGIN_PANEL
        METRIC(&avg_price, "avg_price", color_t::GREEN)
    END_PANEL(R"({"row":[1,2], "column":[2,2]})")
END_DASHBOARD

定义指标看板需要用到个指令:

name description parameters
BEGIN_DASHBOARD/END_DASHBOARD 导出指标看板
BEGIN_PANEL/END_PANEL 导出一个图表,该图表可以包含多个指标 图表布局,row表示所占用的行范围,column表示所占用的列范围
METRIC/METRIC_EX2 定义一个指标 导出一个指标,并定义该指标的名字,颜色、透明度、坐标

END_DASHBOARD的参数是一个JSON字符串,表达了当前图表的布局。如果之前使用过matplotlibGridSpec功能,那您一定会对这种方式的布局配置感到很熟悉。例如上述配置将会在engine控制面板中进行如下展示:

metric-layout-demo.png

METRIC可以在一个图表内定义一个指标,并且可以指定其显示名称、颜色。而METRIC_EX2则是METRIC的扩展版,不但可以指定指标的名称与颜色,还可以指定透明度与坐标轴。透明度Alpha值是一个0.0-1.0之间的浮点数,而坐标轴类型取值可以为metric_y_axis_t::LEFT或者metric_y_axis_t::RIGHT,分别代表左侧坐标轴与右侧坐标轴。

预定义干预命令

干预指令是对程序化交易策略的一种补充,它允许在特殊情况下人工向策略发送信号,来引导策略朝更有利于盈利的方向发展;或者在有些情况下实现手动参数调整。当然策略中定义的任何成员函数都可以导出成为干预命令,您可以根据自己的需求进行定义。

strategy101策略为例(该策略可以在DevelStudio的/strategy101目录下找到),首先定于一个或者多个自定义干预函数:

std::string increase(int slot, order_direction_t direction);
std::string decrease(int slot);
std::string show_status();
std::string test_create_timer(int timeout_ms);
std::string test_cancel_timer(std::string timer_name);

然后通过BEGIN_SIGNAL_HANDLER_DEFEND_SIGNAL_HANDLER_DEF将函数导出为自定义干预命令:

BEGIN_SIGNAL_HANDLER_DEF
    SIGNAL_HANDLER(strategy_101_t, "Increase position", increase, "slot", "direction")
    SIGNAL_HANDLER(strategy_101_t, "Decrease position", decrease, "slot")
    SIGNAL_HANDLER(strategy_101_t, "Show status", show_status)
    SIGNAL_HANDLER(strategy_101_t, "Test create timer", test_create_timer, "timeout(ms)")
    SIGNAL_HANDLER(strategy_101_t, "Test cancel timer", test_cancel_timer, "timer name")
END_SIGNAL_HANDLER_DEF

我们需要通过多个SIGNAL_HANDLER指令将所有干预函数导出,每个指令的第一个参数是策略类名称,第二个参数是干预命令的简短描述,第三个参数是干预命令对应的函数名称,后面的参数分别是每个参数的名称。例如经过上述配置后您在策略界面点击右下角的图标后会看到如下窗口,您可以通过其上的命令向策略发送信号:

thunder-strategy-signal.png

故障恢复

engine如果在运行过程中遇到严重故障会强制退出(例如内存异常访问等),尤其当引入新的策略时,未经严格测试的新代码可能会导致engine以及其他已部署策略不可用。而故障恢复机制可以让engine在发生故障后自动进行恢复,最大程度减少损失。engine会定期保存将当前系统以及策略运行的状态到快照文件,在检测到故障时会利用最新的快照文件恢复服务(Checkpoint机制)。如果您希望您的策略支持故障恢复, 那么需要您在开发策略时使用状态(State)进行数据存储,而不是使用C++普通类成员变量。

class demo_strategy_t : public strategy_t {
    ...
protected:
    double last_price;
    int64_t last_volume;
    std::vector<double> price_list;
    std::map<std::string, std::string> order_ref_map;
    std::deque<uint64_t> other_data;
    ...

}

上述代码需要被改造为使用State的形式:

class demo_strategy_t : public strategy_t {
    ...
protected:
    runtime_context_t *ctx;
    void custom_function() {
        ctx->double_state("last_price") = 10.0;
        ctx->int_state("last_volume") = 20;
        ctx->double_vector("price_list").push_bask(1.2);
        ctx->string_string_state("order_ref_map")["a"] = "b"
        ctx->int_deque("other_data").push_bask(123L);
    }
    ...

}

Engine中提供了int64_tuint64_tstd::stringdouble四种基础数据类型,以及组合而来的vectormap以及deque。 当您把策略执行过程中用到的数据都保存为State时,您的策略就具备了故障恢复的能力,engine在检测到故障后,会自动重启并恢复策略中的State。 当然如数据本身并不需要故障恢复,您仍然可以使用C++成员变量进行保存。

Remote策略指南

同样,您也可以在在DevelStudio的/strategy101文件夹下,找到strategy101的Remote版本,文件的名称是strategy.py以及StrategyTemplate.py。该策略使用Python实现,实现了与Native版本策略同样的交易逻辑。其中StrategyTemplate.py文件作为策略的基础类,完成了http请求的解析与路由,而strategy.py文件则实现了策略的计算和交易逻辑。

Remote策略为您提供使用C++以外的第三方语言进行策略开发的能力,例如Java/Rust/Golang/Python/R/Matlab等等。其原理是基于标准HTTP协议实现一个远端的HTTP策略服务,为engine提供科学计算与交易信号生成的能力。 相比于基于C++的Native策略,通常Remote策略更加倾向于在策略研发前期做快速建模的过程中使用。当Remote策略在仿真环境中被验证符合预期后,通常建议使用C++重写为Native版本以达到更快的速度以及更高的稳定性。

接口实现

接口名称 接口类型 描述
/get_parameter_meta GET engine获取Remote策略的参数元信息
/get_description GET engine获取Remote策略的描述信息
/get_dashboard_meta GET engine获取Remote策略的自定义指标元信息
/get_dashboard_value GET engine通过该接口周期性获取自定义指标的当前值
/get_signal_meta GET engine获取Remote策略的自定义干预信号元信息
/on_initialization POST engine调用on_initialization回调函数
/on_deployment POST engine调用on_deployment回调函数
/on_exit POST 当策略被撤销时,engine回通过该接口通知Remote策略
/on_tick POST engine通过该接口向Remote策略推送行情数据信息
/on_order_trade POST engine通过该接口向Remote策略推送报单成交事件消息
/on_order_progress POST engine通过该接口向Remote策略推送报单消息事件
/on_order_state_change POST engine通过该接口向Remote策略推送报单状态变更事件消息
/on_secondary_ref POST engine通过该接口向Remote策略回传报单的Secondary Reference ID
/on_timer POST 当定时器被出发时,engine回通过该接口通知Remote策略
/on_signal POST 当有来自于engine控制面板的用户自定义信号时,Remote策略回通过该接口进行接收

接口输入与输出

get_parameter_meta接口中需要返回参数的元信息,该接口一般不与具体某个策略实例绑定。返回值为一个Json对象,具体示例如下:

 {
	"a": {
		"type": "INTEGER",
		"default": "1",
		"description": "description of parameter a"
	},
	"b": {
		"type": "DOUBLE",
		"default": "1.0",
		"description": "description of parameter b"
	},
	"c": {
		"type": "STRING",
		"default": "123",
		"description": "description of parameter c"
	}
}

上面的例子中,我们定义了三个参数,分别为abc,类型分别为整形、双精度浮点型以及字符串型。

get_description接口中需要返回策略的描述信息,该接口一般不与具体某个策略实例绑定。

on_initializationon_deployment以及on_exit三个接口分别会在策略被初始化完成后,部署完成后,以及退出之前被engine调用。这三个接口的返回值会被engine忽略。

策略通过下面几个接口分别接收行情信号、交易信号以及定时器和自定义干预命令信号,这些接口的的返回值需要是一个数组,数组中的每个元素代表一个执行动作,包括报单、撤单、添加定时器、撤销定时器。

  • on_tick
  • on_order_trade
  • on_order_progress
  • on_order_state_change
  • on_secondary_ref
  • on_timer
  • on_signal
action description
insert_order 插入报单
cancel_order 撤销报单
create_timer 创建定时器
cancel_timer 撤销定时器

insert_order

例如: 当instruction_type字段指定为insert_order时,表示当前动作为一个报单插入动作。其中primary_ref字段是一个随机生成的字符串,代表报单的Primary reference ID, 您可以使用一个递增的整形值对应的字符串进行填充,需要在同一个交易日内不重复。typedirectionoffset分别需要填充报单类型、方向以及开平类型,对应C++类型的order_type_torder_direction_torder_offset_t。 例如:

{
	"instruction_type": "insert_order",
	"primary_ref": "12",
	"type": "NORMAL_LIMIT_ORDER",
	"direction": "SELL",
	"offset": "DECREASE_TODAY",
	"volume": 1,
	"price": 123.5,
	"slot": 0
}

cancel_order

instruction_type字段指定为cancel_order时,代表当前动作为撤销一个报单,需要制定SlotPrimary reference ID以及Secondary reference ID。例如:

{
	"instruction_type": "cancel_order",
	"slot": 0,
	"primary_ref": "123",
	"secondary_ref": "456"
}

create_timer

instruction_type字段被指定为create_timer时,代表当前动作为新建一个定时器,timer_name表示定时器的名称,该名称会在on_timer接口被调用时被携带。expire_ms表示定时器过期时间,单位是毫秒。 例如:

{
	"instruction_type": "create_timer",
	"timer_name": "test_timer",
	"expire_ms": 10000
}

cancel_timer

instruction_type字段被指定为cancel_timer时,代表当前动作为新建一个定时器。timer_name表示定时器的名称。 例如:

{
	"instruction_type": "cancel_timer",
	"timer_name": "test_timer"
}

自定义指标

get_dashboard_meta接口中返回策略的自定义指标元信息,定义方式与Native策略相似,例如:

[{
		"metrics": [{
			"name": "ask_price_0",
			"color": "GREEN",
			"alpha": 1.0,
			"axis": "LEFT"
		}, {
			"name": "short_position",
			"color": "POWDERBLUE",
			"alpha": 0.8,
			"axis": "LEFT"
		}],
		"layout": {
			"row": [0, 0],
			"column": [0, 0]
		}
	},
	{
		"metrics": [{
			"name": "ask_volume_0",
			"color": "GREEN",
			"alpha": 1.0,
			"axis": "LEFT"
		}, {
			"name": "volume_0",
			"color": "YELLOW",
			"alpha": 0.8,
			"axis": "RIGHT"
		}],
		"layout": {
			"row": [1, 1],
			"column": [0, 0]
		}
	}
]

返回值为一个数组,其中每一个元素代表一个独立的监控面板。metrics代表监控面板中的指标,而layout代表监控面板的布局。

get_dashboard_value需要返回当前监控看板的数值,例如:

[
	[8083.0, 8082.0, 8082.0, null, null],
	[39, 226, 501795],
	[null, null, null, null, null],
	[null, null, null],
	[null, null, null],
	[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
]

数组中每一个子数组代表了一个监控面板中所有指标的当前取之,注意Not a number需要使用null表示。

预定义干预命令

get_signal_meta需要返回当前策略支持的所有自定义干预信号,与Native不同,Remote策略不支持干预信号的参数。策略需要返回代表干预信号名称的数组,例如:

["increase_slot_0_buy", "increase_slot_0_sell", "decrease_slot_0", "create_test_timer", "cancel_test_timer"]

编程语言

Python

我们可以基于http.server包制作一个简易的Remote策略,它可以engine识别,但是什么都不做,只是为您展示来自于engine的消息。

from http.server import BaseHTTPRequestHandler

class Strategy(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
		parsed_url = urllib.parse.urlparse(self.path)
        query_params = urllib.parse.parse_qs(parsed_url.query)
        print(parsed_url)
		print(query_params)

    def do_POST(self):
        content_len = int(self.headers.get("Content-Length"))
        body = self.rfile.read(content_len)
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        parsed_url = urllib.parse.urlparse(self.path)
        query_params = urllib.parse.parse_qs(parsed_url.query)
        print(parsed_url)
		print(query_params)
        json_body = json.loads(body)
        print(json_body)

if __name__ == "__main__":
    httpd = HTTPServer(("0.0.0.0", 6060), Strategy101)
    httpd.serve_forever()

Golang

下面的代码片段可以构建一个engine能够识别的Remote策略。

package main

import (
    "fmt"
    "net/http"
)

func on_initialization(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello\n")
}

func on_deployment(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello\n")
}

func on_tick(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello\n")
}


func headers(w http.ResponseWriter, req *http.Request) {
    for name, headers := range req.Header {
        for _, h := range headers {
            fmt.Fprintf(w, "%v: %v\n", name, h)
        }
    }
}

func main() {
    http.HandleFunc("/on_initialization", on_initialization)
    http.HandleFunc("/on_deployment", on_deployment)
	http.HandleFunc("/on_tick", on_tick)
    http.ListenAndServe(":6060", nil)
}