最近发明者量化交易平台支持了富途证券,进一步增加了一个可以实战程序化交易、量化交易的市场。有很多古老的策略可以拿出来玩一玩,最起码可以测试一下模拟盘交易,毕竟国内股票市场程序化、量化这些技术还都是大机构、大庄家的工具。我们小散能体验一把股票市场的自动化交易还是很令人兴奋的~!
注册富途证券,开户后即可使用模拟盘,详细帖子可以参考:https://www.fmz.cn/bbs-topic/6270
发明者量化API文档链接可以直达富途官网:
接下来就要讲一下非常经典的策略了Dual Thrust
,这个策略是小编我在fmz.cn
学习程序化交易交易入门的第一个策略。在fmz.cn上这个策略有很多版本,例如:商品期货版本,数字货币版本等,以及各种不同编程语言的版本。为什么这个策略比较适合入门呢?因为这个策略涵盖了策略开发的很多方面,诸如策略图表,实时状态信息显示,数据处理,交易逻辑设计等等。并且策略并不复杂,代码也不难懂。本文讲解的这个「股票版Dual Thrust策略」移植自商品期货版的DualThrust策略。
虽然策略逻辑很简单,但是从期货市场移植到股票市场,还是有很多差别的。
下面我就讲讲移植策略时积累的一些经验。
期货市场T+0,股票市场T+1。
这个区别很重要,这一点就导致我们在设计策略时,需要在开仓、平仓检测时增加一些额外的判断。因为期货市场是T+0,所以开仓之后持仓数据中的FrozenAmount字段是0,仓位并不处于冻结状态,就是想平仓随时可以平仓。但是股票市场这点就有差别了,当天开仓后,持仓数据中的FrozenAmount字段是和开仓数量相同的数值,表示开仓后仓位数量全部冻结,当天不能平仓交易。平仓时需要额外注意的就是需要计算可平仓数量,因为有可能有部分持仓是冻结的(通常用持仓数据中的Amount减去FrozenAmount算出可平仓数量)。
下单前设置的交易方向。
和期货市场一样下单前要设置交易方向,不过方向只能设置开多,也就是调用exchange.SetContractType("buy")
。因为股票市场是属于现货交易,并没有开空头仓位的概念。所以是不能调用exchange.SetContractType("swap")
。所以原版商品期货策略中的做空相关的代码都可以剔除。
最小下单数量、下单价格精度
在GetTicker函数返回的数据中Info是券商接口的原始应答数据,其中LotSize字段就是当前品种(某只股票)的最小下单量。
这个数值通常是100,如果下单数量不能被这个数值整除,下单会报错。
下单价格精度同样也需要控制,和商品期货一样,股票信息中也有priceTick。在GetTicker函数返回的数据Info中PriceSpread
字段即为价格一跳数据。
涨跌停、停牌等
股票市场的涨跌停还是比较常见的,所以策略需要检测这种情况,尤其是程序化交易多只股票的时候,不能让一个涨跌停的股票,调用接口失败导致一直卡着。涨跌停时,深度接口GetDepth()
返回的数据中第一档订单量为0,以此识别。depth.Bids[0].Amount == 0
表示跌停。depth.Asks[0].Amount == 0
表示涨停。
if (!depth || depth.Bids[0].Amount == 0 || depth.Asks[0].Amount == 0) { // 标记涨跌停,处理 }
除了要检测涨跌停,还需要检测当前股票交易状态,是否停牌等等。这些信息可以通过GetTicker、SetContractType函数返回的数据中检测。
交易时段差别
股票市场和期货市场交易时段有一定的差别,策略可以根据要交易的板块、市场,具体定制交易时间,让策略在非交易时段休眠。
例如,对于国内A股,可以写一个函数(IsTrading)判断交易时段。
/* 1、9:15-9:25为开盘集合竞价; 2、9:30-11:30,13:00-14:57为连续竞价阶段; 3、14:57-15:00为收盘集合竞价。 */ function IsTrading() { var now = new Date() var day = now.getDay() var hour = now.getHours() var minute = now.getMinutes() StatusMsg = "非交易时段" if (day === 0 || day === 6) { return false } if((hour == 9 && minute >= 30) || (hour == 11 && minute < 30) || (hour > 9 && hour < 11)) { // 9:30-11:30 StatusMsg = "交易时段" return true } else if (hour >= 13 && hour < 15) { // 13:00-15:00 StatusMsg = "交易时段" return true } return false }
需要注意接口访问频率限制。
和商品期货不同,股票限制接口访问频率更加严格,需要在每次访问接口后增加一定的间隔时间,并且间隔时间还需要设置挺大(几秒),否则会触发报错(超过限制频率)。所以在股票策略中需要注意使用_C接口,避免卡死。
使用富途牛牛模拟盘测试。
使用的是一分钟K线测试,只为了测试交易下单、图表显示、数据显示等功能。
JavaScript策略
// 临时参数 var Ids = ["600519.SH", "600121.SH"] // 上证A股 "600121.SH" 郑州煤电,"600519.SH" 贵州茅台 测试的股票代码 var IsReset = false var _Symbols = [] var STATE_IDLE = 0 var STATE_LONG = 1 var SlideTick = 2 var StatusMsg = "" var _Chart = null var _ArrChart = [] var Interval = 1000 var ArrStateStr = ["空闲", "多仓"] function GetPosition(e, contractTypeName) { var allAmount = 0 var allProfit = 0 var allFrozen = 0 var posMargin = 0 var price = 0 var direction = null positions = _C(e.GetPosition) for (var i = 0; i < positions.length; i++) { if (positions[i].ContractType != contractTypeName) { continue } if (positions[i].Type == PD_LONG) { posMargin = positions[i].MarginLevel allAmount += positions[i].Amount allProfit += positions[i].Profit allFrozen += positions[i].FrozenAmount price = positions[i].Price direction = positions[i].Type } } if (allAmount === 0) { return null } return { MarginLevel: posMargin, FrozenAmount: allFrozen, Price: price, Amount: allAmount, Profit: allProfit, Type: direction, ContractType: contractTypeName, CanCoverAmount: allAmount - allFrozen } } function Buy(e, contractType, opAmount, insDetail) { var initPosition = GetPosition(e, contractType) var isFirst = true var initAmount = initPosition ? initPosition.Amount : 0 var positionNow = initPosition if(opAmount % insDetail.LotSize != 0) { throw "每手数量不匹配" } while (true) { var needOpen = opAmount if (isFirst) { isFirst = false } else { Sleep(Interval*20) positionNow = GetPosition(e, contractType) if (positionNow) { needOpen = opAmount - (positionNow.Amount - initAmount) } Log("positionNow:", positionNow, "needOpen:", needOpen)// 测试 } if (needOpen < insDetail.LotSize || needOpen % insDetail.LotSize != 0) { break } var depth = _C(e.GetDepth) var amount = needOpen e.SetDirection("buy") var orderId = e.Buy(depth.Asks[0].Price + (insDetail.PriceSpread * SlideTick), amount, contractType, \'Ask\', depth.Asks[0]) // CancelPendingOrders while (true) { Sleep(Interval*20) var orders = _C(e.GetOrders) if (orders.length === 0) { break } for (var j = 0; j < orders.length; j++) { e.CancelOrder(orders[j].Id) if (j < (orders.length - 1)) { Sleep(Interval*20) } } } } var ret = null if (!positionNow) { return ret } ret = positionNow return ret } function Sell(e, contractType, lots, insDetail) { var initAmount = 0 var firstLoop = true if(lots % insDetail.LotSize != 0) { throw "每手数量不匹配" } while (true) { var n = 0 var total = 0 var positions = _C(e.GetPosition) var nowAmount = 0 for (var i = 0; i < positions.length; i++) { if (positions[i].ContractType != contractType) { continue } nowAmount += positions[i].Amount } if (firstLoop) { initAmount = nowAmount firstLoop = false } var amountChange = initAmount - nowAmount if (typeof(lots) == \'number\' && amountChange >= lots) { break } for (var i = 0; i < positions.length; i++) { if (positions[i].ContractType != contractType) { continue } var amount = positions[i].Amount var depth var opAmount = 0 var opPrice = 0 if (positions[i].Type == PD_LONG) { depth = _C(e.GetDepth) opAmount = amount opPrice = depth.Bids[0].Price - (insDetail.PriceSpread * SlideTick) } if (typeof(lots) === \'number\') { opAmount = Math.min(opAmount, lots - (initAmount - nowAmount)) } if (opAmount > 0) { if (positions[i].Type == PD_LONG) { e.SetDirection("closebuy") e.Sell(opPrice, opAmount, contractType, "平仓", \'Bid\', depth.Bids[0]) } n++ } // break to check always if (typeof(lots) === \'number\') { break } } if (n === 0) { break } while (true) { Sleep(Interval*20) var orders = _C(e.GetOrders) if (orders.length === 0) { break } for (var j = 0; j < orders.length; j++) { e.CancelOrder(orders[j].Id) if (j < (orders.length - 1)) { Sleep(Interval*20) } } } } } /* 1、9:15-9:25为开盘集合竞价; 2、9:30-11:30,13:00-14:57为连续竞价阶段; 3、14:57-15:00为收盘集合竞价。 */ function IsTrading() { var now = new Date() var day = now.getDay() var hour = now.getHours() var minute = now.getMinutes() StatusMsg = "非交易时段" if (day === 0 || day === 6) { return false } if((hour == 9 && minute >= 30) || (hour == 11 && minute < 30) || (hour > 9 && hour < 11)) { // 9:30-11:30 StatusMsg = "交易时段" return true } else if (hour >= 13 && hour < 15) { // 13:00-15:00 StatusMsg = "交易时段" return true } return false } function init () { for (var i = 0 ; i < Ids.length ; i++) { _Symbols[i] = {} _Symbols[i].ContractTypeName = Ids[i] _Symbols[i].NPeriod = 4 _Symbols[i].Ks = 0.5 _Symbols[i].Kx = 0.5 _Symbols[i].AmountOP = 100 _Symbols[i].State = STATE_IDLE _Symbols[i].LastBarTime = 0 _Symbols[i].UpTrack = 0 _Symbols[i].DownTrack = 0 _Symbols[i].ChartIndex = i _Symbols[i].Status = "" _Symbols[i].Pos = null _Symbols[i].ChartCfg = { __isStock: true, title: { text: Ids[i] }, yAxis: { plotLines: [{ value: 0, color: \'red\', width: 2, label: { text: \'上轨\', align: \'center\' }, }, { value: 0, color: \'green\', width: 2, label: { text: \'下轨\', align: \'center\' }, }] }, series: [{ type: \'candlestick\', name: \'当前周期\', id: \'primary\', data: [] }] } _ArrChart.push(_Symbols[i].ChartCfg) } _Chart = Chart(_ArrChart) _Chart.reset() } function DualThrustProcess (symbols) { for (var i = 0 ; i < symbols.length ; i++) { var contractTypeName = symbols[i].ContractTypeName var NPeriod = symbols[i].NPeriod var Ks = symbols[i].Ks var Kx = symbols[i].Kx var AmountOP = symbols[i].AmountOP // 切换为当前 symbol 参数的合约 var insDetail = _C(exchange.SetContractType, contractTypeName) symbols[i].InstrumentName = insDetail.InstrumentName // 判断是不是交易状态 if (!insDetail.IsTrading || !IsTrading()) { continue } // 判断K线长度 var records = exchange.GetRecords() Sleep(3000) var ticker = exchange.GetTicker() Sleep(3000) var depth = exchange.GetDepth() if (!records || records.length <= NPeriod) { StatusMsg = "Calc Bars..." continue } if (!ticker) { continue } if (!depth || depth.Bids[0].Amount == 0 || depth.Asks[0].Amount == 0) { // 标记涨跌停 symbols[i].Status = "涨跌停" continue } symbols[i].Status = "正常交易" var Bar = records[records.length - 1] var index = symbols[i].ChartIndex if (symbols[i].LastBarTime !== Bar.Time) { var HH = TA.Highest(records, NPeriod, \'High\') var HC = TA.Highest(records, NPeriod, \'Close\') var LL = TA.Lowest(records, NPeriod, \'Low\') var LC = TA.Lowest(records, NPeriod, \'Close\') var Range = Math.max(HH - LC, HC - LL) symbols[i].UpTrack = _N(Bar.Open + (Ks * Range)) symbols[i].DownTrack = _N(Bar.Open - (Kx * Range)) if (symbols[i].LastBarTime > 0) { var PreBar = records[records.length - 2] _Chart.add(index, [PreBar.Time, PreBar.Open, PreBar.High, PreBar.Low, PreBar.Close], -1) } else { for (var j = Math.min(records.length, NPeriod * 3); j > 1; j--) { var b = records[records.length - j] _Chart.add(index, [b.Time, b.Open, b.High, b.Low, b.Close]) } } _Chart.add(index, [Bar.Time, Bar.Open, Bar.High, Bar.Low, Bar.Close]) symbols[i].ChartCfg.yAxis.plotLines[0].value = symbols[i].UpTrack symbols[i].ChartCfg.yAxis.plotLines[1].value = symbols[i].DownTrack symbols[i].ChartCfg.subtitle = { text: \'上轨: \' + symbols[i].UpTrack + \' 下轨: \' + symbols[i].DownTrack } _Chart.update(_ArrChart) symbols[i].LastBarTime = Bar.Time } else { _Chart.add(index, [Bar.Time, Bar.Open, Bar.High, Bar.Low, Bar.Close], -1) } // 检测持仓 var pos = GetPosition(exchange, contractTypeName) symbols[i].Pos = pos var posAmount = pos ? pos.Amount : 0 // 同步持仓状态 if (symbols[i].State == STATE_IDLE && posAmount > 0) { symbols[i].State = STATE_LONG } else if (symbols[i].State == STATE_LONG && posAmount == 0) { symbols[i].State = STATE_IDLE } if (symbols[i].State === STATE_IDLE) { if (Bar.Close >= symbols[i].UpTrack) { Log(contractTypeName, "开多仓") // 开仓操作 Buy(exchange, contractTypeName, AmountOP, ticker.Info) symbols[i].State = STATE_LONG } } if (symbols[i].State === STATE_LONG && pos && AmountOP <= pos.CanCoverAmount) { if (Bar.Close <= symbols[i].DownTrack) { Log(contractTypeName, "平多仓") // 平仓操作 Sell(exchange, contractTypeName, AmountOP, ticker.Info) symbols[i].State = STATE_IDLE } } } } function main(){ if(IsReset) { LogReset(1) } SetErrorFilter("market not ready") exchange.SetPrecision(3, 0) if(exchange.GetCurrency() != "STOCK" && exchange.GetName() != "Futures_Futu") { throw "不支持" } while(true){ var tbl = { "type" : "table", "title": "信息", "cols": ["InstrumentName", "ContractTypeName", "NPeriod", "Ks", "Kx", "AmountOP", "State" ,"LastBarTime" ,"UpTrack" ,"DownTrack", "Status", "State"], "rows": [], } for(var i = 0 ; i < _Symbols.length; i++) { tbl.rows.push([_Symbols[i].InstrumentName, _Symbols[i].ContractTypeName, _Symbols[i].NPeriod, _Symbols[i].Ks, _Symbols[i].Kx, _Symbols[i].AmountOP, _Symbols[i].State, _Symbols[i].LastBarTime, _Symbols[i].UpTrack, _Symbols[i].DownTrack, _Symbols[i].Status, ArrStateStr[_Symbols[i].State]]) } var tblPos = { "type" : "table", "title" : "持仓", "cols" : ["名称", "价格", "数量", "盈亏", "类型", "冻结数量", "可平量"], "rows" : [], } for (var j = 0 ; j < _Symbols.length; j++) { if(_Symbols[j].Pos) { tblPos.rows.push([_Symbols[j].Pos.ContractType, _Symbols[j].Pos.Price, _Symbols[j].Pos.Amount, _Symbols[j].Pos.Profit, _Symbols[j].Pos.Type, _Symbols[j].Pos.FrozenAmount, _Symbols[j].Pos.CanCoverAmount]) } } LogStatus(_D(), StatusMsg, "\n`" + JSON.stringify([tbl, tblPos]) + "`") DualThrustProcess(_Symbols) Sleep(1000) } }
小编是新手菜鸟,策略代码仅供分享、学习、探讨、研究,如有BUG感谢提出,如果感觉不错感谢推广一下平台(FMZ.CN)。