完全移植自「CTP商品期貨多品種均線策略」,由於Python版本商品期貨策略還沒有一個多品種的策略,所以就移植了JavaScript版本的「CTP商品期貨多品種均線策略」。提供一些Python商品期貨多品種策略的設計思路、例子。不論JavaScript版本還是Python版本,策略架構設計源於商品期貨多品種海龜策略。
均線策略作爲最簡單的策略,是非常易於學習的,因爲均線策略沒有什麼高深的算法,複雜的邏輯。思路清晰不繞彎,可以讓初學者更專注於策略設計方面的學習,甚至可以把均線策略相關的代碼剔除,留下一個多品種策略框架,可以很輕鬆的擴展成ATR、MACD、BOLL等策略。
JavaScript版本相關文章:https://www.fmz.com/bbs-topic/5235。
策略源碼
'''backtest
start: 2019-07-01 09:00:00
end: 2020-03-25 15:00:00
period: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
'''
import json
import re
import time
_bot = ext.NewPositionManager()
class Manager:
'策略邏輯控制類'
ACT_IDLE = 0
ACT_LONG = 1
ACT_SHORT = 2
ACT_COVER = 3
ERR_SUCCESS = 0
ERR_SET_SYMBOL = 1
ERR_GET_ORDERS = 2
ERR_GET_POS = 3
ERR_TRADE = 4
ERR_GET_DEPTH = 5
ERR_NOT_TRADING = 6
errMsg = ["成功", "切換合約失敗", "獲取訂單失敗", "獲取持倉失敗", "交易下單失敗", "獲取深度失敗", "不在交易時間"]
def __init__(self, needRestore, symbol, keepBalance, fastPeriod, slowPeriod):
# 獲取symbolDetail
symbolDetail = _C(exchange.SetContractType, symbol)
if symbolDetail["VolumeMultiple"] == 0 or symbolDetail["MaxLimitOrderVolume"] == 0 or symbolDetail["MinLimitOrderVolume"] == 0 or symbolDetail["LongMarginRatio"] == 0 or symbolDetail["ShortMarginRatio"] == 0:
Log(symbolDetail)
raise Exception("合約信息異常")
else :
Log("合約", symbolDetail["InstrumentName"], "一手", symbolDetail["VolumeMultiple"], "份,最大下單量", symbolDetail["MaxLimitOrderVolume"], "保證金率:", _N(symbolDetail["LongMarginRatio"]), _N(symbolDetail["ShortMarginRatio"]), "交割日期", symbolDetail["StartDelivDate"])
# 初始化
self.symbol = symbol
self.keepBalance = keepBalance
self.fastPeriod = fastPeriod
self.slowPeriod = slowPeriod
self.marketPosition = None
self.holdPrice = None
self.holdAmount = None
self.holdProfit = None
self.task = {
"action" : Manager.ACT_IDLE,
"amount" : 0,
"dealAmount" : 0,
"avgPrice" : 0,
"preCost" : 0,
"preAmount" : 0,
"init" : False,
"retry" : 0,
"desc" : "空閒",
"onFinish" : None
}
self.lastPrice = 0
self.symbolDetail = symbolDetail
# 持倉狀態信息
self.status = {
"symbol" : symbol,
"recordsLen" : 0,
"vm" : [],
"open" : 0,
"cover" : 0,
"st" : 0,
"marketPosition" : 0,
"lastPrice" : 0,
"holdPrice" : 0,
"holdAmount" : 0,
"holdProfit" : 0,
"symbolDetail" : symbolDetail,
"lastErr" : "",
"lastErrTime" : "",
"isTrading" : False
}
# 對象構造時其他處理工作
vm = None
if RMode == 0:
vm = _G(self.symbol)
else:
vm = json.loads(VMStatus)[self.symbol]
if vm:
Log("準備恢復進度,當前合約狀態爲", vm)
self.reset(vm[0])
else:
if needRestore:
Log("沒有找到" + self.symbol + "的進度恢復信息")
self.reset()
def setLastError(self, err=None):
if err is None:
self.status["lastErr"] = ""
self.status["lastErrTime"] = ""
return
t = _D()
self.status["lastErr"] = err
self.status["lastErrTime"] = t
def reset(self, marketPosition=None):
if marketPosition is not None:
self.marketPosition = marketPosition
pos = _bot.GetPosition(self.symbol, PD_LONG if marketPosition > 0 else PD_SHORT)
if pos is not None:
self.holdPrice = pos["Price"]
self.holdAmount = pos["Amount"]
Log(self.symbol, "倉位", pos)
else :
raise Exception("恢復" + self.symbol + "的持倉狀態出錯,沒有找到倉位信息")
Log("恢復", self.symbol, "持倉均價:", self.holdPrice, "持倉數量:", self.holdAmount)
self.status["vm"] = [self.marketPosition]
else :
self.marketPosition = 0
self.holdPrice = 0
self.holdAmount = 0
self.holdProfit = 0
self.holdProfit = 0
self.lastErr = ""
self.lastErrTime = ""
def Status(self):
self.status["marketPosition"] = self.marketPosition
self.status["holdPrice"] = self.holdPrice
self.status["holdAmount"] = self.holdAmount
self.status["lastPrice"] = self.lastPrice
if self.lastPrice > 0 and self.holdAmount > 0 and self.marketPosition != 0:
self.status["holdProfit"] = _N((self.lastPrice - self.holdPrice) * self.holdAmount * self.symbolDetail["VolumeMultiple"], 4) * (1 if self.marketPosition > 0 else -1)
else :
self.status["holdProfit"] = 0
return self.status
def setTask(self, action, amount = None, onFinish = None):
self.task["init"] = False
self.task["retry"] = 0
self.task["action"] = action
self.task["preAmount"] = 0
self.task["preCost"] = 0
self.task["amount"] = 0 if amount is None else amount
self.task["onFinish"] = onFinish
if action == Manager.ACT_IDLE:
self.task["desc"] = "空閒"
self.task["onFinish"] = None
else:
if action != Manager.ACT_COVER:
self.task["desc"] = ("加多倉" if action == Manager.ACT_LONG else "加空倉") + "(" + str(amount) + ")"
else :
self.task["desc"] = "平倉"
Log("接收到任務", self.symbol, self.task["desc"])
self.Poll(True)
def processTask(self):
insDetail = exchange.SetContractType(self.symbol)
if not insDetail:
return Manager.ERR_SET_SYMBOL
SlideTick = 1
ret = False
if self.task["action"] == Manager.ACT_COVER:
hasPosition = False
while True:
if not ext.IsTrading(self.symbol):
return Manager.ERR_NOT_TRADING
hasPosition = False
positions = exchange.GetPosition()
if positions is None:
return Manager.ERR_GET_POS
depth = exchange.GetDepth()
if depth is None:
return Manager.ERR_GET_DEPTH
orderId = None
for i in range(len(positions)):
if positions[i]["ContractType"] != self.symbol:
continue
amount = min(insDetail["MaxLimitOrderVolume"], positions[i]["Amount"])
if positions[i]["Type"] == PD_LONG or positions[i]["Type"] == PD_LONG_YD:
exchange.SetDirection("closebuy_today" if positions[i].Type == PD_LONG else "closebuy")
orderId = exchange.Sell(_N(depth["Bids"][0]["Price"] - (insDetail["PriceTick"] * SlideTick), 2), min(amount, depth["Bids"][0]["Amount"]), self.symbol, "平今" if positions[i]["Type"] == PD_LONG else "平昨", "Bid", depth["Bids"][0])
hasPosition = True
elif positions[i]["Type"] == PD_SHORT or positions[i]["Type"] == PD_SHORT_YD:
exchange.SetDirection("closesell_today" if positions[i]["Type"] == PD_SHORT else "closesell")
orderId = exchange.Buy(_N(depth["Asks"][0]["Price"] + (insDetail["PriceTick"] * SlideTick), 2), min(amount, depth["Asks"][0]["Amount"]), self.symbol, "平今" if positions[i]["Type"] == PD_SHORT else "平昨", "Ask", depth["Asks"][0])
hasPosition = True
if hasPosition:
if not orderId:
return Manager.ERR_TRADE
Sleep(1000)
while True:
orders = exchange.GetOrders()
if orders is None:
return Manager.ERR_GET_ORDERS
if len(orders) == 0:
break
for i in range(len(orders)):
exchange.CancelOrder(orders[i]["Id"])
Sleep(500)
if not hasPosition:
break
ret = True
elif self.task["action"] == Manager.ACT_LONG or self.task["action"] == Manager.ACT_SHORT:
while True:
if not ext.IsTrading(self.symbol):
return Manager.ERR_NOT_TRADING
Sleep(1000)
while True:
orders = exchange.GetOrders()
if orders is None:
return Manager.ERR_GET_ORDERS
if len(orders) == 0:
break
for i in range(len(orders)):
exchange.CancelOrder(orders[i]["Id"])
Sleep(500)
positions = exchange.GetPosition()
if positions is None:
return Manager.ERR_GET_POS
pos = None
for i in range(len(positions)):
if positions[i]["ContractType"] == self.symbol and (((positions[i]["Type"] == PD_LONG or positions[i]["Type"] == PD_LONG_YD) and self.task["action"] == Manager.ACT_LONG) or ((positions[i]["Type"] == PD_SHORT) or positions[i]["Type"] == PD_SHORT_YD) and self.task["action"] == Manager.ACT_SHORT):
if not pos:
pos = positions[i]
pos["Cost"] = positions[i]["Price"] * positions[i]["Amount"]
else :
pos["Amount"] += positions[i]["Amount"]
pos["Profit"] += positions[i]["Profit"]
pos["Cost"] += positions[i]["Price"] * positions[i]["Amount"]
# records pre position
if not self.task["init"]:
self.task["init"] = True
if pos:
self.task["preAmount"] = pos["Amount"]
self.task["preCost"] = pos["Cost"]
else:
self.task["preAmount"] = 0
self.task["preCost"] = 0
remain = self.task["amount"]
if pos:
self.task["dealAmount"] = pos["Amount"] - self.task["preAmount"]
remain = int(self.task["amount"] - self.task["dealAmount"])
if remain <= 0 or self.task["retry"] >= MaxTaskRetry:
ret = {
"price" : (pos["Cost"] - self.task["preCost"]) / (pos["Amount"] - self.task["preAmount"]),
"amount" : (pos["Amount"] - self.task["preAmount"]),
"position" : pos
}
break
elif self.task["retry"] >= MaxTaskRetry:
ret = None
break
depth = exchange.GetDepth()
if depth is None:
return Manager.ERR_GET_DEPTH
orderId = None
if self.task["action"] == Manager.ACT_LONG:
exchange.SetDirection("buy")
orderId = exchange.Buy(_N(depth["Asks"][0]["Price"] + (insDetail["PriceTick"] * SlideTick), 2), min(remain, depth["Asks"][0]["Amount"]), self.symbol, "Ask", depth["Asks"][0])
else:
exchange.SetDirection("sell")
orderId = exchange.Sell(_N(depth["Bids"][0]["Price"] - (insDetail["PriceTick"] * SlideTick), 2), min(remain, depth["Bids"][0]["Amount"]), self.symbol, "Bid", depth["Bids"][0])
if orderId is None:
self.task["retry"] += 1
return Manager.ERR_TRADE
if self.task["onFinish"]:
self.task["onFinish"](ret)
self.setTask(Manager.ACT_IDLE)
return Manager.ERR_SUCCESS
def Poll(self, subroutine = False):
# 判斷交易時段
self.status["isTrading"] = ext.IsTrading(self.symbol)
if not self.status["isTrading"]:
return
# 執行下單交易任務
if self.task["action"] != Manager.ACT_IDLE:
retCode = self.processTask()
if self.task["action"] != Manager.ACT_IDLE:
self.setLastError("任務沒有處理成功:" + Manager.errMsg[retCode] + ", " + self.task["desc"] + ", 重試:" + str(self.task["retry"]))
else :
self.setLastError()
return
if subroutine:
return
suffix = "@" if WXPush else ""
# switch symbol
_C(exchange.SetContractType, self.symbol)
# 獲取K線數據
records = exchange.GetRecords()
if records is None:
self.setLastError("獲取K線失敗")
return
self.status["recordsLen"] = len(records)
if len(records) < self.fastPeriod + 2 or len(records) < self.slowPeriod + 2:
self.setLastError("K線長度小於 均線週期:" + str(self.fastPeriod) + "或" + str(self.slowPeriod))
return
opCode = 0 # 0 : IDLE , 1 : LONG , 2 : SHORT , 3 : CoverALL
lastPrice = records[-1]["Close"]
self.lastPrice = lastPrice
fastMA = TA.EMA(records, self.fastPeriod)
slowMA = TA.EMA(records, self.slowPeriod)
# 策略邏輯
if self.marketPosition == 0:
if fastMA[-3] < slowMA[-3] and fastMA[-2] > slowMA[-2]:
opCode = 1
elif fastMA[-3] > slowMA[-3] and fastMA[-2] < slowMA[-2]:
opCode = 2
else:
if self.marketPosition < 0 and fastMA[-3] < slowMA[-3] and fastMA[-2] > slowMA[-2]:
opCode = 3
elif self.marketPosition > 0 and fastMA[-3] > slowMA[-3] and fastMA[-2] < slowMA[-2]:
opCode = 3
# 如果不觸發任何條件,操作碼爲0,返回
if opCode == 0:
return
# 執行平倉
if opCode == 3:
def coverCallBack(ret):
self.reset()
_G(self.symbol, None)
self.setTask(Manager.ACT_COVER, 0, coverCallBack)
return
account = _bot.GetAccount()
canOpen = int((account["Balance"] - self.keepBalance) / (self.symbolDetail["LongMarginRatio"] if opCode == 1 else self.symbolDetail["ShortMarginRatio"]) / (lastPrice * 1.2) / self.symbolDetail["VolumeMultiple"])
unit = min(1, canOpen)
# 設置交易任務
def setTaskCallBack(ret):
if not ret:
self.setLastError("下單失敗")
return
self.holdPrice = ret["position"]["Price"]
self.holdAmount = ret["position"]["Amount"]
self.marketPosition += 1 if opCode == 1 else -1
self.status["vm"] = [self.marketPosition]
_G(self.symbol, self.status["vm"])
self.setTask(Manager.ACT_LONG if opCode == 1 else Manager.ACT_SHORT, unit, setTaskCallBack)
def onexit():
Log("已退出策略...")
def main():
if exchange.GetName().find("CTP") == -1:
raise Exception("只支持商品期貨CTP")
SetErrorFilter("login|ready|流控|連接失敗|初始|Timeout")
mode = exchange.IO("mode", 0)
if mode is None:
raise Exception("切換模式失敗,請更新到最新託管者!")
while not exchange.IO("status"):
Sleep(3000)
LogStatus("正在等待與交易服務器連接," + _D())
positions = _C(exchange.GetPosition)
if len(positions) > 0:
Log("檢測到當前持有倉位,系統將開始嘗試恢復進度...")
Log("持倉信息:", positions)
initAccount = _bot.GetAccount()
initMargin = json.loads(exchange.GetRawJSON())["CurrMargin"]
keepBalance = _N((initAccount["Balance"] + initMargin) * (KeepRatio / 100), 3)
Log("資產信息", initAccount, "保留資金:", keepBalance)
tts = []
symbolFilter = {}
arr = Instruments.split(",")
arrFastPeriod = FastPeriodArr.split(",")
arrSlowPeriod = SlowPeriodArr.split(",")
if len(arr) != len(arrFastPeriod) or len(arr) != len(arrSlowPeriod):
raise Exception("均線週期參數與添加合約數量不匹配,請檢查參數!")
for i in range(len(arr)):
symbol = re.sub(r'/\s+$/g', "", re.sub(r'/^\s+/g', "", arr[i]))
if symbol in symbolFilter.keys():
raise Exception(symbol + "已經存在,請檢查參數!")
symbolFilter[symbol] = True
hasPosition = False
for j in range(len(positions)):
if positions[j]["ContractType"] == symbol:
hasPosition = True
break
fastPeriod = int(arrFastPeriod[i])
slowPeriod = int(arrSlowPeriod[i])
obj = Manager(hasPosition, symbol, keepBalance, fastPeriod, slowPeriod)
tts.append(obj)
preTotalHold = -1
lastStatus = ""
while True:
if GetCommand() == "暫停/繼續":
Log("暫停交易中...")
while GetCommand() != "暫停/繼續":
Sleep(1000)
Log("繼續交易中...")
while not exchange.IO("status"):
Sleep(3000)
LogStatus("正在等待與交易服務器連接," + _D() + "\n" + lastStatus)
tblStatus = {
"type" : "table",
"title" : "持倉信息",
"cols" : ["合約名稱", "持倉方向", "持倉均價", "持倉數量", "持倉盈虧", "加倉次數", "當前價格"],
"rows" : []
}
tblMarket = {
"type" : "table",
"title" : "運行狀態",
"cols" : ["合約名稱", "合約乘數", "保證金率", "交易時間", "柱線長度", "異常描述", "發生時間"],
"rows" : []
}
totalHold = 0
vmStatus = {}
ts = time.time()
holdSymbol = 0
for i in range(len(tts)):
tts[i].Poll()
d = tts[i].Status()
if d["holdAmount"] > 0:
vmStatus[d["symbol"]] = d["vm"]
holdSymbol += 1
tblStatus["rows"].append([d["symbolDetail"]["InstrumentName"], "--" if d["holdAmount"] == 0 else ("多" if d["marketPosition"] > 0 else "空"), d["holdPrice"], d["holdAmount"], d["holdProfit"], abs(d["marketPosition"]), d["lastPrice"]])
tblMarket["rows"].append([d["symbolDetail"]["InstrumentName"], d["symbolDetail"]["VolumeMultiple"], str(_N(d["symbolDetail"]["LongMarginRatio"], 4)) + "/" + str(_N(d["symbolDetail"]["ShortMarginRatio"], 4)), "是#0000ff" if d["isTrading"] else "否#ff0000", d["recordsLen"], d["lastErr"], d["lastErrTime"]])
totalHold += abs(d["holdAmount"])
now = time.time()
elapsed = now - ts
tblAssets = _bot.GetAccount(True)
nowAccount = _bot.Account()
if len(tblAssets["rows"]) > 10:
tblAssets["rows"][0] = ["InitAccount", "初始資產", initAccount]
else:
tblAssets["rows"].insert(0, ["NowAccount", "當前可用", nowAccount])
tblAssets["rows"].insert(0, ["InitAccount", "初始資產", initAccount])
lastStatus = "`" + json.dumps([tblStatus, tblMarket, tblAssets]) + "`\n輪詢耗時:" + str(elapsed) + " 秒,當前時間:" + _D() + ", 持有品種個數:" + str(holdSymbol)
if totalHold > 0:
lastStatus += "\n手動恢復字符串:" + json.dumps(vmStatus)
LogStatus(lastStatus)
if preTotalHold > 0 and totalHold == 0:
LogProfit(nowAccount.Balance - initAccount.Balance - initMargin)
preTotalHold = totalHold
Sleep(LoopInterval * 1000)
策略地址:https://www.fmz.com/strategy/208512
回測對比
我們用該策略的JavaScript版本和Python版本回測進行對比。
-
Python版本回測
我們使用公共服務器進行回測,可以看到Python版本的回測略微快了一點。
-
JavaScript版本回測
可以看到回測結果一模一樣,有興趣的小夥伴可以鑽研一下代碼,會有不小的收穫。
花裏胡哨的擴展
我們來做個擴展示範,給策略擴展出圖表功能,如圖:
主要增加代碼部分:
- 給
Manager
類增加一個成員:objChart
- 給
Manager
類增加一個方法:PlotRecords
其它的一些修改都是圍繞這兩點進行,可以對比兩個版本區別,學習擴展功能的思路。
python版商品期貨多品種均線策略 (擴展圖表)
以上策略學習爲主,實盤慎用。
歡迎留言。