CTP商品期貨多品種海龜交易策略 (註釋版)
- 只支持操作CTP商品期貨
- 支持自動或手動恢復進度
- 可同時操作多個不同品種
- 增加時間段區分與各種網絡錯誤問題的應對處理
- 移倉功能目前正在加入中
請下載最新託管者並比較版本號是否最新
$ ./robot -v
BotVS docker 3.0 compiled at 2016-07-05T09:56:18+0800
代碼逐行翻譯了一邊,適合量化程序化初學者。
聽說看註釋前, 熟悉一下海龜交易法,效果會更好哦!
/*
參數:
Instruments 合約列表 字符串(string) MA701,CF701,zn1701,SR701,pp1701,l1701,hc1610,ni1701,i1701,v1701,rb1610,jm1701,ag1612,al1701,jd1701,cs1701,p1701
LoopInterval 輪詢週期(秒) 數字型(number) 3
RiskRatio % Risk Per N ( 0 - 100) 數字型(number) 1
ATRLength ATR計算週期 數字型(number) 20
EnterPeriodA 系統一入市週期 數字型(number) 20
LeavePeriodA 系統一離市週期 數字型(number) 10
EnterPeriodB 系統二入市週期 數字型(number) 55
LeavePeriodB 系統二離市週期 數字型(number) 20
UseEnterFilter 使用入市過濾 布爾型(true/false) true
IncSpace 加倉間隔(N的倍數) 數字型(number) 0.5
StopLossRatio 止損係數(N的倍數) 數字型(number) 2
MaxLots 單品種加倉次數 數字型(number) 4
RMode 進度恢復模式 下拉框(selected) 自動|手動
VMStatus@RMode==1 手動恢復字符串 字符串(string) {}
WXPush 推送交易信息 布爾型(true/false) true
MaxTaskRetry 開倉最多重試次數 數字型(number) 5
KeepRatio 預留保證金比例 數字型(number) 10
*/
var _bot = $.NewPositionManager(); // 調用CTP商品期貨交易類庫 的導出函數 生成一個用於單個品種交易的對象
var TTManager = { // 海龜策略 控制器
New: function(needRestore, symbol, keepBalance, riskRatio, atrLen, enterPeriodA, leavePeriodA, enterPeriodB, leavePeriodB, useFilter,
multiplierN, multiplierS, maxLots) {
// 該控制器對象 TTManager 的屬性 New 賦值一個 匿名函數(構造海龜的函數,即:構造函數),用於創建 海龜任務,參數分別是:
// needRestore: 是否需要恢復,symbol:合約代碼,keepBalance:必要的預留的資金,riskRatio:風險係數, atrLen:ATR指標(參數)週期。enterPeriodA:入市週期A
// leavePeriodA:離市週期A , enterPeriodB:入市週期B, leavePeriodB:離市週期B,useFilter:使用過濾,multiplierN:加倉係數,multiplierS:止損係數,maxLots:最大加倉次數
// subscribe
var symbolDetail = _C(exchange.SetContractType, symbol);
// 聲明一個局部變量 symbolDetail 用於接受API SetContractType 函數的返回值(值爲symbol的合約的詳細信息,symbol 是 "MA709",返回的就是甲醇709合約的詳細信息),
// 調用API SetContractType 訂閱並切換合約爲 symbol 變量值的合約。 _C() 函數的作用是 對 SetContractType 合約容錯處理,即如果 SetContractType返回null 會循環重試。
if (symbolDetail.VolumeMultiple == 0 || symbolDetail.MaxLimitOrderVolume == 0 || symbolDetail.MinLimitOrderVolume == 0 || symbolDetail.LongMarginRatio == 0 || symbolDetail.ShortMarginRatio == 0) {
// 如果 返回的合約信息對象symbolDetail 中 VolumeMultiple、MaxLimitOrderVolume 等數據異常,則調用 throw 拋出錯誤,終止程序。
Log(symbolDetail);
throw "合約信息異常";
} else { // 檢索的數據沒有異常則,輸出部分合約信息。
Log("合約", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "份, 最大下單量", symbolDetail.MaxLimitOrderVolume, "保證金率:", _N(symbolDetail.LongMarginRatio), _N(symbolDetail.ShortMarginRatio), "交割日期", symbolDetail.StartDelivDate);
}
var ACT_IDLE = 0; // 定義一些宏 (標記)
var ACT_LONG = 1;
var ACT_SHORT = 2;
var ACT_COVER = 3; // 動作宏
var ERR_SUCCESS = 0; // 錯誤宏
var ERR_SET_SYMBOL = 1;
var ERR_GET_ORDERS = 2;
var ERR_GET_POS = 3;
var ERR_TRADE = 4;
var ERR_GET_DEPTH = 5;
var ERR_NOT_TRADING = 6;
var errMsg = ["成功", "切換合約失敗", "獲取訂單失敗", "獲取持倉失敗", "交易下單失敗", "獲取深度失敗", "不在交易時間"]; // 錯誤宏的值 對應該數組的索引,對應索引的值就是翻譯
var obj = { // 聲明一個對象,構造完成後返回。單個的海龜策略控制對象。
symbol: symbol, // 合約代碼 構造函數執行時的參數傳入
keepBalance: keepBalance, // 預留的資金 構造函數執行時的參數傳入
riskRatio: riskRatio, // 風險係數 構造函數執行時的參數傳入
atrLen: atrLen, // ATR 長度 構造函數執行時的參數傳入
enterPeriodA: enterPeriodA, // 入市週期A 構造函數執行時的參數傳入
leavePeriodA: leavePeriodA, // 離市週期A 構造函數執行時的參數傳入
enterPeriodB: enterPeriodB, // 入市週期B 構造函數執行時的參數傳入
leavePeriodB: leavePeriodB, // 離市週期B 構造函數執行時的參數傳入
useFilter: useFilter, // 使用入市過濾條件 構造函數執行時的參數傳入
multiplierN: multiplierN, // 加倉係數 基於N 構造函數執行時的參數傳入
multiplierS: multiplierS // 止損係數 基於N 構造函數執行時的參數傳入
};
obj.task = { // 給 obj對象添加一個 task 屬性(值也是一個對象),用來保存 海龜的任務狀態數據。
action: ACT_IDLE, // 執行動作
amount: 0, // 操作量
dealAmount: 0, // 已經處理的操作量
avgPrice: 0, // 成交均價
preCost: 0, // 前一次交易成交的額度
preAmount: 0, // 前一次成交的量
init: false, // 是否初始化
retry: 0, // 重試次數
desc: "空閒", // 描述信息
onFinish: null // 處理完成時的 回調函數,即可以自行設定一個 回調函數在完成當前 action 記錄的任務後執行的代碼。
}
obj.maxLots = maxLots; // 賦值 最大加倉次數 構造函數執行時的參數傳入
obj.lastPrice = 0; // 最近成交價,用於計算 持倉盈虧。
obj.symbolDetail = symbolDetail; // 儲存 合約的詳細信息 到obj 對象的 symbolDetail 屬性
obj.status = { // 狀態數據
symbol: symbol, // 合約代碼
recordsLen: 0, // K線長度
vm: [], // 持倉狀態 , 用來儲存 每個品種的 ,手動恢復字符串。
open: 0, // 開倉次數
cover: 0, // 平倉次數
st: 0, // 止損平倉次數
marketPosition: 0, // 加倉次數
lastPrice: 0, // 最近成交價價格
holdPrice: 0, // 持倉均價
holdAmount: 0, // 持倉數量
holdProfit: 0, // 浮動持倉盈虧
N: 0, // N值 , 即ATR
upLine: 0, // 上線
downLine: 0, // 下線
symbolDetail: symbolDetail, // 合約詳細信息
lastErr: "", // 上次錯誤
lastErrTime: "", // 上次錯誤時間信息
stopPrice: '', // 止損價格
leavePrice: '', //
isTrading: false // 是否在交易時間
};
obj.setLastError = function(err) { // 給obj對象添加方法,設置 最近一次的錯誤信息
if (typeof(err) === 'undefined' || err === '') { // 如果參數未傳入,或者 錯誤信息爲 空字符串
obj.status.lastErr = ""; // 清空 obj 對象的 status 屬性的 對象的lastErr屬性
obj.status.lastErrTime = ""; // 清空
return; // 返回
}
var t = new Date(); // 獲取新時間
obj.status.lastErr = err; // 設置錯誤信息
obj.status.lastErrTime = t.toLocaleString(); // toLocaleString() 根據本地時間格式,把 Date 對象轉換爲字符串。
};
obj.reset = function(marketPosition, openPrice, N, leavePeriod, preBreakoutFailure) { // 給obj對象添加方法,恢復倉位。
// 參數,marketPosition:加倉次數,openPrice:最後一次加倉價, N:N值, leavePeriod:離市週期,preBreakoutFailure:是否上次突破失敗
if (typeof(marketPosition) !== 'undefined') { // 如果 第一個參數不是未定義 ,傳入參數
obj.marketPosition = marketPosition; // 給obj 添加屬性 marketPosition : 加倉次數 正數爲多倉,負數爲空倉
obj.openPrice = openPrice; // 最後一次加倉價
obj.preBreakoutFailure = preBreakoutFailure; // 是否上次突破失敗
obj.N = N; // N值
obj.leavePeriod = leavePeriod; // 離市週期
var pos = _bot.GetPosition(obj.symbol, marketPosition > 0 ? PD_LONG : PD_SHORT); // 調用 模板類庫生成的 交易控制對象的成員函數GetPosition 獲取 持倉信息
if (pos) { // 如果獲取到持倉信息
obj.holdPrice = pos.Price; // 根據獲取的持倉信息 給obj 屬性賦值
obj.holdAmount = pos.Amount; // 同上
Log(obj.symbol, "倉位", pos); // 輸出顯示當前倉位
} else { // 如果GetPosition 返回null ,沒有找到持倉信息。
throw "恢復" + obj.symbol + "的持倉狀態出錯, 沒有找到倉位信息"; // 拋出異常
}
Log("恢復", obj.symbol, "加倉次數", obj.marketPosition, "持倉均價:", obj.holdPrice, "持倉數量:", obj.holdAmount, "最後一次加倉價", obj.openPrice, "N值", obj.N, "離市週期:", leavePeriod, "上次突破:", obj.preBreakoutFailure ? "失敗" : "成功");
// 輸出恢復的 相關參數,數據。
obj.status.open = 1; // 設置 開倉 計數爲1
obj.status.vm = [obj.marketPosition, obj.openPrice, obj.N, obj.leavePeriod, obj.preBreakoutFailure]; // 儲存 手動恢復字符串 數據。
} else { // 沒有傳入參數,即不恢復, 全部初始化。
obj.marketPosition = 0; // 初始化各項變量
obj.holdPrice = 0;
obj.openPrice = 0;
obj.holdAmount = 0;
obj.holdProfit = 0;
obj.preBreakoutFailure = true; // test system A // 此處設置true 會使策略 嘗試 突破系統A
obj.N = 0;
obj.leavePeriod = leavePeriodA; // 用系統A 的離市週期 賦值
}
obj.holdProfit = 0; // 初始化
obj.lastErr = "";
obj.lastErrTime = "";
};
obj.Status = function() { // 給Obj 添加 Status 函數, 把Obj 的一些屬性值 賦值給 Obj.status 同樣意義的屬性
obj.status.N = obj.N; // 給 obj.status 賦值
obj.status.marketPosition = obj.marketPosition;
obj.status.holdPrice = obj.holdPrice;
obj.status.holdAmount = obj.holdAmount;
obj.status.lastPrice = obj.lastPrice;
if (obj.lastPrice > 0 && obj.holdAmount > 0 && obj.marketPosition !== 0) { // 如果有持倉
obj.status.holdProfit = _N((obj.lastPrice - obj.holdPrice) * obj.holdAmount * symbolDetail.VolumeMultiple, 4) * (obj.marketPosition > 0 ? 1 : -1);
// 計算持倉盈虧 = (最近成交價 - 持倉價格)* 持倉量 * 一手合約份數 , 計算出來 保留4位小數, 用 obj.marketPosition(加倉次數) 屬性的 正負 去修正,計算結果的正負(做空按照這個算法是相反的負數,所以要用-1修正)。
} else {
// 如果沒有持倉,浮動盈虧賦值爲0
obj.status.holdProfit = 0;
}
return obj.status; // 返回這個 obj.status 對象(用於顯示在界面狀態欄?)
};
obj.setTask = function(action, amount, onFinish) { // 給obj 對象添加 方法,設置任務
// 參數,action:執行動作,amount:數量,onFinish: 回調函數
obj.task.init = false; // 重置 初次執行標記 爲false
obj.task.retry = 0; // 重置..
obj.task.action = action; // 參數傳來的 動作指令 賦值
obj.task.preAmount = 0; // 重置
obj.task.preCost = 0;
obj.task.amount = typeof(amount) === 'number' ? amount : 0; // 如果沒傳入參數 ,設置 0
obj.task.onFinish = onFinish;
if (action == ACT_IDLE) { // 如果 動作指令是 空閒
obj.task.desc = "空閒"; // 描述變量 賦值爲 “空閒”
obj.task.onFinish = null; // 賦值爲 null
} else { // 其他動作
if (action !== ACT_COVER) { // 如果不等於 平倉動作
obj.task.desc = (action == ACT_LONG ? "加多倉" : "加空倉") + "(" + amount + ")"; // 根據 action 設置描述 信息
} else { // 如果是平倉 動作 設置描述信息爲 “平倉”
obj.task.desc = "平倉";
}
Log("接收到任務", obj.symbol, obj.task.desc); // 輸出日誌 顯示 接收到任務。
// process immediately
obj.Poll(true); // 調用 obj 對象的方法 處理 任務,參數是 true , 參數爲true ,控制Poll 只執行 一部分(子過程)
}
};
obj.processTask = function() { // 處理 交易任務
var insDetail = exchange.SetContractType(obj.symbol); // 切換 要操作的合約
if (!insDetail) { // 切換失敗 返回錯誤
return ERR_SET_SYMBOL;
}
var SlideTick = 1; // 滑價設置爲1 個 PriceTick
var ret = false; // 聲明返回值 初始false
if (obj.task.action == ACT_COVER) { // 處理 指令爲全平的 任務,這部分處理 類似 商品期貨交易類庫 不再贅述,可以參見 商品期貨交易類庫註釋版
var hasPosition = false;
do {
if (!$.IsTrading(obj.symbol)) {
return ERR_NOT_TRADING;
}
hasPosition = false;
var positions = exchange.GetPosition();
if (!positions) {
return ERR_GET_POS;
}
var depth = exchange.GetDepth();
if (!depth) {
return ERR_GET_DEPTH;
}
var orderId = null;
for (var i = 0; i < positions.length; i++) {
if (positions[i].ContractType !== obj.symbol) {
continue;
}
var amount = Math.min(insDetail.MaxLimitOrderVolume, positions[i].Amount);
if (positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) {
exchange.SetDirection(positions[i].Type == PD_LONG ? "closebuy_today" : "closebuy");
orderId = exchange.Sell(_N(depth.Bids[0].Price - (insDetail.PriceTick * SlideTick), 2), Math.min(amount, depth.Bids[0].Amount), obj.symbol, positions[i].Type == PD_LONG ? "平今" : "平昨", 'Bid', depth.Bids[0]);
hasPosition = true;
} else if (positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD) {
exchange.SetDirection(positions[i].Type == PD_SHORT ? "closesell_today" : "closesell");
orderId = exchange.Buy(_N(depth.Asks[0].Price + (insDetail.PriceTick * SlideTick), 2), Math.min(amount, depth.Asks[0].Amount), obj.symbol, positions[i].Type == PD_SHORT ? "平今" : "平昨", 'Ask', depth.Asks[0]);
hasPosition = true;
}
}
if (hasPosition) {
if (!orderId) {
return ERR_TRADE;
}
Sleep(1000);
while (true) {
// Wait order, not retry
var orders = exchange.GetOrders();
if (!orders) {
return ERR_GET_ORDERS;
}
if (orders.length == 0) {
break;
}
for (var i = 0; i < orders.length; i++) {
exchange.CancelOrder(orders[i].Id);
Sleep(500);
}
}
}
} while (hasPosition);
ret = true;
} else if (obj.task.action == ACT_LONG || obj.task.action == ACT_SHORT) { // 處理 建/加多倉 任務 或者 處理 建/加空倉 任務,這部分處理 類似 商品期貨交易類庫 不再贅述,可以參見 商品期貨交易類庫註釋版。(此策略沒有使用商品期貨交易類庫的交易功能,在次直接植入了處理代碼)
do {
if (!$.IsTrading(obj.symbol)) {
return ERR_NOT_TRADING;
}
Sleep(1000);
while (true) {
// Wait order, not retry
var orders = exchange.GetOrders();
if (!orders) {
return ERR_GET_ORDERS;
}
if (orders.length == 0) {
break;
}
for (var i = 0; i < orders.length; i++) {
exchange.CancelOrder(orders[i].Id);
Sleep(500);
}
}
var positions = exchange.GetPosition();
// Error
if (!positions) {
return ERR_GET_POS;
}
// search position
var pos = null;
for (var i = 0; i < positions.length; i++) {
if (positions[i].ContractType == obj.symbol && (((positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) && obj.task.action == ACT_LONG) || ((positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD) && obj.task.action == ACT_SHORT))) {
if (!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;
}
}
}
// record pre position
if (!obj.task.init) {
obj.task.init = true;
if (pos) {
obj.task.preAmount = pos.Amount;
obj.task.preCost = pos.Cost;
} else {
obj.task.preAmount = 0;
obj.task.preCost = 0;
}
}
var remain = obj.task.amount;
if (pos) {
obj.task.dealAmount = pos.Amount - obj.task.preAmount;
remain = parseInt(obj.task.amount - obj.task.dealAmount);
if (remain <= 0 || obj.task.retry >= MaxTaskRetry) {
ret = {
price: (pos.Cost - obj.task.preCost) / (pos.Amount - obj.task.preAmount),
amount: (pos.Amount - obj.task.preAmount),
position: pos
};
break;
}
} else if (obj.task.retry >= MaxTaskRetry) {
ret = null;
break;
}
var depth = exchange.GetDepth();
if (!depth) {
return ERR_GET_DEPTH;
}
var orderId = null;
if (obj.task.action == ACT_LONG) {
exchange.SetDirection("buy");
orderId = exchange.Buy(_N(depth.Asks[0].Price + (insDetail.PriceTick * SlideTick), 2), Math.min(remain, depth.Asks[0].Amount), obj.symbol, 'Ask', depth.Asks[0]);
} else {
exchange.SetDirection("sell");
orderId = exchange.Sell(_N(depth.Bids[0].Price - (insDetail.PriceTick * SlideTick), 2), Math.min(remain, depth.Bids[0].Amount), obj.symbol, 'Bid', depth.Bids[0]);
}
// symbol not in trading or other else happend
if (!orderId) {
obj.task.retry++;
return ERR_TRADE;
}
} while (true);
}
if (obj.task.onFinish) {
obj.task.onFinish(ret);
}
obj.setTask(ACT_IDLE); // 任務執行完成(中間沒有被 錯誤 return),重設爲 空閒任務
return ERR_SUCCESS;
};
obj.Poll = function(subroutine) { // 處理海龜交易法 策略邏輯, 參數: 子程序?
obj.status.isTrading = $.IsTrading(obj.symbol); // 調用 模板的導出函數 $.IsTrading 檢測 obj.symbol 記錄的品種是否在交易時間,結果賦值給obj.status.isTrading
if (!obj.status.isTrading) { // 如果 obj.status.isTrading 是 false 即 不在交易時間內, return 返回
return;
}
if (obj.task.action != ACT_IDLE) { // 如果 任務屬性的 執行動作屬性 不等於 等待標記(宏)
var retCode = obj.processTask(); // 就調用 當前obj 對象的processTask函數 執行 task 記錄的任務。
if (obj.task.action != ACT_IDLE) { // 如果 調用 processTask 函數後 task屬性的action 屬性不等於 等待標記,即證明任務沒有處理成功。
obj.setLastError("任務沒有處理成功: " + errMsg[retCode] + ", " + obj.task.desc + ", 重試: " + obj.task.retry);
// 此時調用 setLastError 記錄 並 顯示 任務 沒有處理成功, 錯誤代碼, 任務描述、重試次數
} else {
obj.setLastError(); // 調用 setLastError 不傳參數, 不傳參數 用空內容(字符串,詳見函數setLastError)刷新。
}
return; // 執行完 任務 返回
}
if (typeof(subroutine) !== 'undefined' && subroutine) { // 參數 subroutine 不爲null 且 已定義, 比如在調用 setTask 後會執行Poll,到此就返回
return; // 返回
}
// Loop
var suffix = WXPush ? '@' : ''; // 界面參數如果開啓 微信推送, suffix 會被賦值 "@"(微信推送功能 只用在API: Log函數後加 "@"字符即可), 否則空字符。
// switch symbol
_C(exchange.SetContractType, obj.symbol); // 切換 合約 爲 obj.symbol 記錄的合約代碼
var records = exchange.GetRecords(); // 獲取K線數據
if (!records) { // 如果 K線獲取到 null 值
obj.setLastError("獲取K線失敗"); // 設置失敗信息,並返回。
return;
}
obj.status.recordsLen = records.length; // 記錄K線長度
if (records.length < obj.atrLen) { // 如果 K線長度小於 ATR指標參數(小於的話 無法計算出ATR指標 即N值)
obj.setLastError("K線長度小於 " + obj.atrLen); // 設置錯誤信息,並返回。
return;
}
var opCode = 0; // 0: IDLE, 1: LONG, 2: SHORT, 3: CoverALL // 聲明一個臨時變量 操作代碼 有4種操作
var lastPrice = records[records.length - 1].Close; // 聲明一個臨時變量 用K線 最後一個柱 的收盤價給其賦值,(K線最後一個柱的收盤價是實時更新的是最新價格)
obj.lastPrice = lastPrice; // 賦值給 obj.lastPrice
if (obj.marketPosition === 0) { // 如果當前 海龜策略 控制對象的加倉次數 爲0 ,即沒持倉。
obj.status.stopPrice = '--'; // 給止損價 賦值 '--'
obj.status.leavePrice = '--'; // 用於顯示 狀態的表格 對象 status的 leavePrice屬性賦值 "--" (因爲沒有持倉,所以沒有 離市價)
obj.status.upLine = 0; // 賦值 上線,(這裏如果不明白 這些變量控制那些顯示,可以實際運行一個模擬盤 ,看下界面對比分析更好理解。)
obj.status.downLine = 0; // 賦值 下線
for (var i = 0; i < 2; i++) { // 在當前的分支條件內,是沒有持倉的,這裏循環兩次,用來檢測2個突破系統的觸發。
if (i == 0 && obj.useFilter && !obj.preBreakoutFailure) { // 如果是第一次循環,並且啓用了入市條件過濾,並且上次突破沒有失敗。
continue; // 跳過本次循環
}
var enterPeriod = i == 0 ? obj.enterPeriodA : obj.enterPeriodB; // 用 ? : 三元條件表達式,選擇使用的 突破系統 參數,即當 i == 0 時 使用 系統A
if (records.length < (enterPeriod + 1)) { // 限制 當前 K線週期 bar 長度 必須大於 突破系統的入市週期加1
continue; // 跳過本次循環
}
var highest = TA.Highest(records, enterPeriod, 'High'); // 計算enterPeriod週期內所有最高價的 最大值
var lowest = TA.Lowest(records, enterPeriod, 'Low'); // 計算enterPeriod週期內所有最低價的 最小值
obj.status.upLine = obj.status.upLine == 0 ? highest : Math.min(obj.status.upLine, highest); // 取兩次 系統A 和系統B 獲取的 highest中 最小的值
obj.status.downLine = obj.status.downLine == 0 ? lowest : Math.max(obj.status.downLine, lowest); // 取兩次 系統A 和系統B 獲取的 lowest中 最大的值
if (lastPrice > highest) { // 最新的 價格 如果向上突破 對應週期內的最高價
opCode = 1; // 操作值 賦值1
} else if (lastPrice < lowest) { // 最新的 價格 如果向下突破 對應週期內的最低價
opCode = 2; // 操作值 賦值2
}
obj.leavePeriod = (enterPeriod == obj.enterPeriodA) ? obj.leavePeriodA : obj.leavePeriodB; // 更新 最終確定使用 哪個系統?問題1
}
} else { // 如果持有倉位
var spread = obj.marketPosition > 0 ? (obj.openPrice - lastPrice) : (lastPrice - obj.openPrice); // 計算單價盈虧 做多 盈利是負值 虧損是正值,因爲要做和止損單價的對比,所以取反, 做空同理
obj.status.stopPrice = _N(obj.openPrice + (obj.N * StopLossRatio * (obj.marketPosition > 0 ? -1 : 1))); // 計算止損價 做多的時候: 用開倉價 減去 N值 乘 止損係數, 做空: 用開倉價 加上 N值 乘止 損係數。
if (spread > (obj.N * StopLossRatio)) { // 檢測 單價盈虧 是否大於 設定的 盈虧限制(即 止損係數 * N值)
opCode = 3; // 觸發 止損 操作代碼 賦值 3
obj.preBreakoutFailure = true; // 觸發止損 ,標記 上次突破失敗爲真
Log(obj.symbolDetail.InstrumentName, "止損平倉", suffix); // 打印 該品種 合約名 止損, 如果開啓微信推送,則推送到微信。
obj.status.st++; // 止損計數 累計
} else if (-spread > (IncSpace * obj.N)) { // 如果單價盈虧(取反 得 正 盈利數,負虧損數) 大於加倉係數 * N值, 觸發加倉操作
opCode = obj.marketPosition > 0 ? 1 : 2; // 0: IDLE, 1: LONG, 2: SHORT, 3: CoverALL
} else if (records.length > obj.leavePeriod) { // 只要 K線週期 長度大於 離市 週期,可以計算離市價格
obj.status.leavePrice = TA.Lowest(records, obj.leavePeriod, obj.marketPosition > 0 ? 'Low' : 'High') // 問題2
if ((obj.marketPosition > 0 && lastPrice < obj.status.leavePrice) || // 做多 或者 做空 如果觸發了 離市價
(obj.marketPosition < 0 && lastPrice > obj.status.leavePrice)) {
obj.preBreakoutFailure = false; // 上次突破失敗 賦值爲 false ,即 沒失敗
Log(obj.symbolDetail.InstrumentName, "正常平倉", suffix); // 打印信息 平倉,可微信推送
opCode = 3; // 給操作 賦值 3
obj.status.cover++; // 平倉計數累計
}
}
}
if (opCode == 0) { // 如果是 等待 代碼 則返回
return;
}
if (opCode == 3) { // 如果是 全平倉 代碼
obj.setTask(ACT_COVER, 0, function(ret) { // 調用 obj 海龜控制對象的成員函數 setTask 設置任務 (全平倉)並自定義一個回調函數(第三個參數 function(ret){...} 就是匿名函數。)
obj.reset(); // 回調函數 會在setTask 函數中 設置任務後 調用的 Poll 的函數中 通過 processTask 函數 執行該任務完成後 ,觸發回調函數。
_G(obj.symbol, null); // 回調函數 調用了 不傳參數的 reset函數,執行控制對象 變量重置工作,清空 _G 保存的 本地永久 數據(用於恢復,因爲已經平倉了,所以需要清空)
});
return; // 回調函數是在任務完成後(即 全部海龜頭寸 平倉後 才觸發,此處只是預設)
}
// Open
if (Math.abs(obj.marketPosition) >= obj.maxLots) { // 建倉 或者 加倉處理, 這裏判斷如果 加倉次數 大於等於 最大允許加倉次數
obj.setLastError("禁止開倉, 超過最大持倉 " + obj.maxLots); // 設置錯誤信息,然後返回。
return;
}
var atrs = TA.ATR(records, atrLen); // 計算ATR 指標
var N = _N(atrs[atrs.length - 1], 4); // 獲取 當前ATR指標值 ,即 N值
var account = _bot.GetAccount(); // 調用 模板 生成的 交易控制對象的 成員函數 GetAccount
var currMargin = JSON.parse(exchange.GetRawJSON()).CurrMargin; // 獲取當前 保證金數值
var unit = parseInt((account.Balance+currMargin-obj.keepBalance) * (obj.riskRatio / 100) / N / obj.symbolDetail.VolumeMultiple);
// 計算 總 可用資金對應 N值 計算出的 一個頭寸的 大小(手數)。可以看原版的海龜交易法 關於 unit 的計算,知乎上也有相關文章。
var canOpen = parseInt((account.Balance-obj.keepBalance) / (opCode == 1 ? obj.symbolDetail.LongMarginRatio : obj.symbolDetail.ShortMarginRatio) / (lastPrice * 1.2) / obj.symbolDetail.VolumeMultiple);
// 根據 要做 多倉 或者 空倉 的保證金率 計算 可用資金 可以開 的手數,可開量。
unit = Math.min(unit, canOpen); // 最終頭寸大小 取 unit, canOpen 中最小值
if (unit < obj.symbolDetail.MinLimitOrderVolume) { // 如果 計算出的 頭寸大小 小於 合約規定的限價單 最小下單量,則
obj.setLastError("可開 " + unit + " 手 無法開倉, " + (canOpen >= obj.symbolDetail.MinLimitOrderVolume ? "風控觸發" : "資金限制")); // 設置最新錯誤信息
return; // 返回
}
obj.setTask((opCode == 1 ? ACT_LONG : ACT_SHORT), unit, function(ret) { // 根據 opCode 設定, 調用 setTask 函數 設定任務
if (!ret) { // 同樣 第三個參數 是回調函數,回調函數中 ret 是觸發 調用回調函數時傳入的參數,任務的執行返回值。
obj.setLastError("下單失敗");
return;
}
Log(obj.symbolDetail.InstrumentName, obj.marketPosition == 0 ? "開倉" : "加倉", "離市週期", obj.leavePeriod, suffix); // 任務成功完成,回調函數會執行此 輸出
obj.N = N; // 開倉 或者 加倉後 更新N值
obj.openPrice = ret.price; // 更新 開倉價格
obj.holdPrice = ret.position.Price; // 更新持倉均價,根據 任務執行的ret。
if (obj.marketPosition == 0) { // 如果此時 加倉次數是0, 即代表本次是 建倉
obj.status.open++; // 開倉計數 累計
}
obj.holdAmount = ret.position.Amount; // 更新持倉量
obj.marketPosition += opCode == 1 ? 1 : -1; // 根據 做多 或者 做空 累計 加倉次數
obj.status.vm = [obj.marketPosition, obj.openPrice, N, obj.leavePeriod, obj.preBreakoutFailure]; // 更新 用於恢復的 字符串 ,屬性vm
_G(obj.symbol, obj.status.vm); // 本地持久化儲存 當前持倉信息。
});
}; // Poll 函數結束
var vm = null; // 在New 構造函數中 聲明一個 局部變量 vm 區別於obj.vm
if (RMode === 0) { // 如果進度恢復模式爲 自動,下拉框第一個索引是0 ,設置爲第一個時 下拉框參數就返回0 ,第二個 返回下一個索引1,以此類推。
vm = _G(obj.symbol); // 取回 持久化儲存的數據 賦值給 局部變量vm
} else { // 否則 恢復模式爲 手動
vm = JSON.parse(VMStatus)[obj.symbol]; // 取手動恢復字符串 JSON解析後的數組中的對應於合約類型 obj.symbol 的 數據。
}
if (vm) { // 如果獲取的有 數據
Log("準備恢復進度, 當前合約狀態爲", vm); // 輸出恢復的 合約狀態
obj.reset(vm[0], vm[1], vm[2], vm[3], vm[4]); // 調用重設 函數 重新設置 恢復狀態
} else { // 如果vm 沒有數據
if (needRestore) { // 需要恢復 則輸出 沒找到進度的信息, (有可能是 合約列表 中 有新的合約代碼,則不需要恢復)
Log("沒有找到" + obj.symbol + "的進度恢復信息");
}
obj.reset(); // reset 不傳參數 ,即重置
}
return obj; // 返回 構造完成的對象。
}
};
function onexit() { // 策略程序 退出時執行。
Log("已退出策略...");
}
function main() {
if (exchange.GetName().indexOf('CTP') == -1) { // 限定 連接的交易所 必須是 CTP 商品期貨
throw "只支持商品期貨CTP";
}
SetErrorFilter("login|ready|流控|連接失敗|初始|Timeout"); // 過濾常規錯誤
var mode = exchange.IO("mode", 0); // 設定行情模式 爲立即返回模式 參看 API 文檔: https://www.botvs.com/api
if (typeof(mode) !== 'number') { // 如果 切換模式 的API 返回的 不是 數值,即切換失敗。
throw "切換模式失敗, 請更新到最新託管者!"; // 拋出異常
}
while (!exchange.IO("status")) { // 檢測 與 行情、交易服務器連接,直到 API 函數 exchange.IO("status") 返回true 連接上,退出循環
Sleep(3000);
LogStatus("正在等待與交易服務器連接, " + new Date()); // 在未連接上時 輸出 文本和 當前時間。
}
var positions = _C(exchange.GetPosition); // 調用API GetPosition 函數 獲取 持倉信息
if (positions.length > 0) { // 返回的數組不是空數組 ,即有持倉
Log("檢測到當前持有倉位, 系統將開始嘗試恢復進度...");
Log("持倉信息", positions);
}
Log("風險係數:", RiskRatio, "N值週期:", ATRLength, "系統1: 入市週期", EnterPeriodA, "離市週期", LeavePeriodA, "系統二: 入市週期", EnterPeriodB, "離市週期", LeavePeriodB, "加倉係數:", IncSpace, "止損係數:", StopLossRatio, "單品種最多開倉:", MaxLots, "次");
// 輸出 參數信息。
var initAccount = _bot.GetAccount(); // 獲取賬戶信息
var initMargin = JSON.parse(exchange.GetRawJSON()).CurrMargin; // 調用 API GetRawJSON 函數 獲取 : "CurrMargin": "當前保證金總額",
var keepBalance = _N((initAccount.Balance + initMargin) * (KeepRatio/100), 3); // 根據預留保證金比例 計算出 需要預留的資金。
Log("資產信息", initAccount, "保留資金:", keepBalance); // 輸出信息
var tts = [];
var filter = []; // 過濾用數組
var arr = Instruments.split(','); // 合約列表按照逗號分隔 成數組
for (var i = 0; i < arr.length; i++) { // 遍歷分隔後的數組
var symbol = arr[i].replace(/^\s+/g, "").replace(/\s+$/g, ""); // 正則表達式 匹配 操作, 得出 合約代碼
if (typeof(filter[symbol]) !== 'undefined') { // 如果 在過濾數組中 存在 名爲 symbol的屬性,則顯示信息 並跳過。
Log(symbol, "已經存在, 系統已自動過濾");
continue;
}
filter[symbol] = true; // 給過濾數組 添加 名爲 symbol 的 屬性,下次 同樣的 合約代碼 會被過濾
var hasPosition = false; // 初始化 hasPosition 變量 false 代表沒有持倉
for (var j = 0; j < positions.length; j++) { // 遍歷 獲取到的持倉信息
if (positions[j].ContractType == symbol) { // 如果有持倉信息 合約 名稱 和 symbol一樣的, 給hasPosition 賦值true 代表有持倉
hasPosition = true;
break;
}
}
var obj = TTManager.New(hasPosition, symbol, keepBalance, RiskRatio, ATRLength, EnterPeriodA, LeavePeriodA, EnterPeriodB, LeavePeriodB, UseEnterFilter, IncSpace, StopLossRatio, MaxLots);
// 根據界面參數 使用 構造函數 New 構造 一個品種的海龜交易策略控制對象
tts.push(obj); // 把該對象壓入 tts 數組, 最終根據合約列表 ,生成了若干個品種的 控制對象儲存在tts數組
}
var preTotalHold = -1;
var lastStatus = '';
while (true) { // 主要循環
if (GetCommand() === "暫停/繼續") { // API GetCommand 函數 獲取 程序界面上的 命令。此處 如果 點擊了界面上的“暫停/繼續”按鈕
Log("暫停交易中...");
while (GetCommand() !== "暫停/繼續") { // 進入等待循環 ,直到再次點擊 “暫停/繼續” 按鈕 退出 等待循環
Sleep(1000);
}
Log("繼續交易中...");
}
while (!exchange.IO("status")) { // 一旦斷開服務器的連接,則嘗試重連 並等待。
Sleep(3000);
LogStatus("正在等待與交易服務器連接, " + new Date() + "\n" + lastStatus); // 輸出上一次的 狀態欄 內容,並 更新時間。
}
var tblStatus = { // 用於顯示在狀態欄表格上的 持倉信息 對象
type: "table",
title: "持倉信息",
cols: ["合約名稱", "持倉方向", "持倉均價", "持倉數量", "持倉盈虧", "加倉次數", "開倉次數", "止損次數", "成功次數", "當前價格", "N"],
rows: []
};
var tblMarket = { // 用於顯示在狀態欄表格上的 市場信息 對象
type: "table",
title: "運行狀態",
cols: ["合約名稱", "合約乘數", "保證金率", "交易時間", "柱線長度", "上線", "下線", "止損價", "離市價", "異常描述", "發生時間"],
rows: []
};
var totalHold = 0;
var vmStatus = {};
var ts = new Date().getTime(); // 當前時間戳
var holdSymbol = 0; // 持有的合約量
for (var i = 0; i < tts.length; i++) { // 遍歷tts數組
tts[i].Poll(); // 調用每個 合約的海龜管理對象的 Poll 函數
var d = tts[i].Status(); // 更新每個 海龜管理對象的 狀態 屬性 status 並返回。
if (d.holdAmount > 0) { // 如果當前索引的對象 有 持倉
vmStatus[d.symbol] = d.vm; // 給空對象 vmStatus 添加合約名稱 爲屬性名 的屬性,並給其賦值 持倉信息vm
holdSymbol++; // 給持有的合約品種數量 累計
}
tblStatus.rows.push([d.symbolDetail.InstrumentName, d.holdAmount == 0 ? '--' : (d.marketPosition > 0 ? '多' : '空'), d.holdPrice, d.holdAmount, d.holdProfit, Math.abs(d.marketPosition), d.open, d.st, d.cover, d.lastPrice, d.N]);
// 壓入當前 索引 的 海龜管理對象 的信息 到狀態分頁表格
tblMarket.rows.push([d.symbolDetail.InstrumentName, d.symbolDetail.VolumeMultiple, _N(d.symbolDetail.LongMarginRatio, 4) + '/' + _N(d.symbolDetail.ShortMarginRatio, 4), (d.isTrading ? '是#0000ff' : '否#ff0000'), d.recordsLen, d.upLine, d.downLine, d.stopPrice, d.leavePrice, d.lastErr, d.lastErrTime]);
// 壓入當前 索引 的 海龜管理對象 的信息 到行情分頁表格
totalHold += Math.abs(d.holdAmount); // 值爲回調函數 的參數ret 的屬性 更新,可以參見 回調函數的 傳入實參。processTask 函數中的 ret
// 累計 總持倉手數
}
var now = new Date(); // 獲取最新時間
var elapsed = now.getTime() - ts; // 計算主要耗時代碼 , 迭代 執行 Poll 函數的 開始與結束的 時間差。
var tblAssets = _bot.GetAccount(true); // 獲取賬戶詳細信息並返回一個表格對象。(因爲參數傳遞的是true, 參見 模板的 GetAccount 函數的 getTable 參數)
var nowAccount = _bot.Account(); // 獲取賬戶信息
if (tblAssets.rows.length > 10) { // 如果獲取的 表格的 行數 大於10
// replace AccountId
tblAssets.rows[0] = ["InitAccount", "初始資產", initAccount]; // 設置 索引 0 的行數 爲 初始資金信息。
} else {
tblAssets.rows.unshift(["NowAccount", "當前可用", nowAccount], ["InitAccount", "初始資產", initAccount]); // 往 rows 數組 中開始的位置插入2個元素
}
lastStatus = '`' + JSON.stringify([tblStatus, tblMarket, tblAssets]) + '`\n輪詢耗時: ' + elapsed + ' 毫秒, 當前時間: ' + now.toLocaleString() + ', 星期' + ['日', '一', '二', '三', '四', '五', '六'][now.getDay()] + ", 持有品種個數: " + holdSymbol;
// 組合 各種 用於顯示在界面的信息。
if (totalHold > 0) { // 在有持倉時才 顯示 手動恢復字符串(vmStatus JSON序列化)
lastStatus += "\n手動恢復字符串: " + JSON.stringify(vmStatus);
}
LogStatus(lastStatus); // 調用API 顯示在 狀態欄
if (preTotalHold > 0 && totalHold == 0) { // 當全部持倉 平掉 沒有持倉時
LogProfit(nowAccount.Balance - initAccount.Balance - initMargin); // 輸出 盈利, 顯示到收益曲線(此種情況 出現概率較低,很難有同時全部都未持倉的狀態,所以收益都是 動態的,可以看 賬戶詳細信息分析當前狀況)
}
preTotalHold = totalHold; // 每次都更新 確保 輸出收益只顯示一次。
Sleep(LoopInterval * 1000); // 輪詢等待。避免API 訪問過於頻繁
}
}
菜鳥程序員註釋,如有錯誤,歡迎斧正,感謝支持! ^。^