FMZ股票實盤、模擬盤程序化交易實戰--股票版DualThrust策略

最近發明者量化交易平臺支持了富途證券,進一步增加了一個可以實戰程序化交易、量化交易的市場。有很多古老的策略可以拿出來玩一玩,最起碼可以測試一下模擬盤交易,畢竟國內股票市場程序化、量化這些技術還都是大機構、大莊家的工具。我們小散能體驗一把股票市場的自動化交易還是很令人興奮的~!

註冊富途證券,開戶後即可使用模擬盤,詳細帖子可以參考:https://www.fmz.com/bbs-topic/6270
發明者量化API文檔鏈接可以直達富途官網:

股票版Dual Thrust策略

接下來就要講一下非常經典的策略了Dual Thrust,這個策略是小編我在FMZ.COM學習程序化交易交易入門的第一個策略。在FMZ.COM上這個策略有很多版本,例如:商品期貨版本,數字貨幣版本等,以及各種不同編程語言的版本。爲什麼這個策略比較適合入門呢?因爲這個策略涵蓋了策略開發的很多方面,諸如策略圖表,實時狀態信息顯示,數據處理,交易邏輯設計等等。並且策略並不複雜,代碼也不難懂。本文講解的這個「股票版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.COM)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章