该策略是很久以前适用数字货币796期货交易所的一个策略,期货合约是币本位,即保证金扣除的是币(例如BTC合约扣除BTC),合约下单量可以是小数,类似币安的币本位合约。 该策略拿出来学习策略设计,逻辑处理还是很不错的,策略以学习为主。
策略代码注释
var FirstTradeType = [ORDER_TYPE_BUY, ORDER_TYPE_SELL][OpType]; // 首次开仓方向,根据参数OpType开仓方向确定,全局变量FirstTradeType的值为ORDER_TYPE_BUY或者ORDER_TYPE_SELL
var OrgAccount = null; // 全局变量,记录账户资产
var Counter = {s : 0, f: 0}; // 声明一个变量Counter,值初始化为一个对象(类似的结构python叫字典),s代表胜的次数,f代表负的次数
var LastProfit = 0; // 最近盈亏
var AllProfit = 0; // 总盈亏
var _Failed = 0; // 止损次数
// 取消挂单列表中某个ID的订单之外的所有订单,当不传orderId参数时,取消当前交易对所有挂单,参数e为交易所对象的引用,例如传入exchange作为参数,此刻e就是exchange的别名
function StripOrders(e, orderId) {
var order = null; // 初始化变量 order 为空
if (typeof(orderId) == 'undefined') { // 如果参数 orderId 传入时,没有写,则typeof(orderId) == 'undefined'成立,执行if语句的代码块,orderId 赋值为null
orderId = null;
}
while (true) { // 处理循环
var dropped = 0; // 处理标记次数
var orders = _C(e.GetOrders); // 调用GetOrders获取当前挂单(未完全成交的订单),赋值给orders
for (var i = 0; i < orders.length; i++) { // 遍历未成交订单列表 orders
if (orders[i].Id == orderId) { // 如果订单ID和参数上传入的订单ID orderId 相同则给函数内局部变量order赋值orders[i],orders[i]即遍历时当前的订单结构
order = orders[i];
} else { // 如果ID不相同,执行撤销操作
var extra = ""; // 根据部分成交情况,设置扩展信息extra
if (orders[i].DealAmount > 0) {
extra = "成交: " + orders[i].DealAmount;
} else {
extra = "未成交";
}
e.SetDirection(orders[i].Type == ORDER_TYPE_BUY ? "buy" : "sell");
e.CancelOrder(orders[i].Id, orders[i].Type == ORDER_TYPE_BUY ? "买单" : "卖单", extra); // 撤单操作,附带输出extra信息,在日志上会显示
dropped++; // dropped 计数累计
}
}
if (dropped == 0) { // 当遍历完成时,dropped 等于0,即遍历时没有一次撤销处理(没有需要撤销的订单了),即为撤销处理工作完成,跳出while循环
break;
}
Sleep(300); // 防止轮转频率过快,每次间隔一定时间
}
return order; // 返回要查找的订单order
}
var preMsg = ""; // 记录缓存信息的变量
function GetAccount(e, waitFrozen) { // 获取账户资产信息,参数e亦是exchange的引用,参数waitFrozen控制是否等待冻结
if (typeof(waitFrozen) == 'undefined') { // 如果调用时不传入waitFrozen参数,给参数waitFrozen赋值false,即默认不等待冻结
waitFrozen = false;
}
var account = null;
var alreadyAlert = false; // 标记是否已经提醒过的变量
while (true) { // 获取当前账户信息,检测冻结,如果不等待冻结,则会直接跳出while循环
account = _C(e.GetAccount);
if (!waitFrozen || account.FrozenStocks < MinStock) {
break;
}
if (!alreadyAlert) {
alreadyAlert = true; // 触发提醒一次,就重置alreadyAlert,避免重复不停的提醒
Log("发现账户有冻结的钱或币", account); // 输出提醒日志
}
Sleep(Interval);
}
msg = "成功: " + Counter.s + " 次, 失败: " + Counter.f + " 次, 当前账户 币: " + account.Stocks;
if (account.FrozenStocks > 0) {
msg += " 冻结的币: " + account.FrozenStocks;
}
if (msg != preMsg) { // 检测当前信息是否和上次信息不同,不同的话更新在状态栏上
preMsg = msg;
LogStatus(msg, "#ff0000");
}
return account; // 函数返回账户信息 account结构
}
function GetPosition(e, orderType) { // 获取持仓,或者获取指定方向的持仓
var positions = _C(e.GetPosition); // 获取持仓
if (typeof(orderType) == 'undefined') { // orderType 参数为指定要获取的持仓类型,如果没有传入orderType参数,直接返回所有持仓
return positions;
}
for (var i = 0; i < positions.length; i++) { // 遍历持仓列表
if (positions[i].Type == orderType) { // 如果当前遍历的持仓数据是需要找的方向(orderType)
return positions[i]; // 返回 orderType 类型的持仓
}
}
return null;
}
function GetTicker(e) { // 获取ticker 行情数据
while (true) {
var ticker = _C(e.GetTicker); // 获取tick行情
if (ticker.Buy > 0 && ticker.Sell > 0 && ticker.Sell > ticker.Buy) { // 检查行情数据可靠性
return ticker; // 返回 ticker数据
}
Sleep(100);
}
}
// mode = 0 : direct buy, 1 : buy as buy1
function Trade(e, tradeType, tradeAmount, mode, slidePrice, maxSpace, retryDelay) { // 交易函数
// e 交易所对象引用, tradeType 交易方向(买/卖), tradeAmount 交易数量, mode 交易模式, slidePrice 滑价, maxSpace 最大挂单距离, retryDelay 重试时间间隔
var initPosition = GetPosition(e, tradeType); // 获取指定方向的持仓数据,记作 initPosition
var nowPosition = initPosition; // 声明另一个变量nowPosition 用initPosition赋值
var orderId = null;
var prePrice = 0; // 上次循环时的下单价格
var dealAmount = 0; // 已经交易的数量
var diffMoney = 0;
var isFirst = true; // 循环首次执行标记
var tradeFunc = tradeType == ORDER_TYPE_BUY ? e.Buy : e.Sell; // 下单函数,根据参数 tradeType 而定是调用 e.Buy 还是 e.Sell
var isBuy = tradeType == ORDER_TYPE_BUY; // 是否是买入的标记
while (true) { // while循环
var account = _C(e.GetAccount); // 获取当前账户资产数据
var ticker = GetTicker(e); // 获取当前行情数据
var tradePrice = 0; // 根据 mode 参数制定交易价格
if (isBuy) {
tradePrice = _N((mode == 0 ? ticker.Sell : ticker.Buy) + slidePrice, 4);
} else {
tradePrice = _N((mode == 0 ? ticker.Buy : ticker.Sell) - slidePrice, 4);
}
if (orderId == null) {
if (isFirst) { // 根据 isFirst 标记变量判断,如果是第一次执行,什么都不做
isFirst = false; // isFirst 标记设置为false ,代表已经不是第一次执行
} else { // 非第一次执行,更新持仓数据
nowPosition = GetPosition(e, tradeType);
}
dealAmount = _N((nowPosition ? nowPosition.Amount : 0) - (initPosition ? initPosition.Amount : 0), 6); // 根据最初的持仓数据和当前的持仓数据,计算已经成交的数量
var doAmount = Math.min(tradeAmount - dealAmount, account.Stocks * MarginLevel, 4); // 根据已经成交的数量、账户可用资产,计算剩余需要交易的数量
if (doAmount < MinStock) { // 如果算出的交易数量小于最小交易数量,终止逻辑,跳出while循环
break;
}
prePrice = tradePrice; // 缓存当前循环时的交易价格
e.SetDirection(tradeType == ORDER_TYPE_BUY ? "buy" : "sell"); // 设置期货交易方向
orderId = tradeFunc(tradePrice, doAmount); // 下单交易,参数为算出的价格,本次下单数量
} else { // 当记录订单的变量 orderId 不为null时,则说明已经下过订单
if (mode == 0 || Math.abs(tradePrice - prePrice) > maxSpace) { // 如果是挂单模式,当前价格与上一次缓存的价格超出最大挂单区间
orderId = null; // 重置orderId 为空值,就会在下一轮循环重新下单
}
var order = StripOrders(exchange, orderId); // 调用StripOrders查找挂单列表中的ID为orderId的订单
if (order == null) { // 如果查找不到,也重置orderId 为空值,继续下一轮的下单操作
orderId = null;
}
}
Sleep(retryDelay); // 暂定一定时间,起到控制循环频率的效果
}
if (dealAmount <= 0) { // 在while循环结束后,如果已经交易的量dealAmount小于等于0,说明交易失败返回空值
return null;
}
return nowPosition; // 正常情况返回最新的持仓数据
}
function coverFutures(e, orderType) { // 平仓函数
var coverAmount = 0; // 声明一个变量coverAmount,初始赋值0,用来记录已经平仓的数量
while (true) {
var positions = _C(e.GetPosition); // 获取持仓
var ticker = GetTicker(e); // 获取当前行情
var found = 0; // 查找标记
for (var i = 0; i < positions.length; i++) { // 遍历持仓数组positions
if (positions[i].Type == orderType) { // 找到需要的持仓
if (coverAmount == 0) {
coverAmount = positions[i].Amount; // 初始时记录持仓数量,即要平仓的数量
}
if (positions[i].Type == ORDER_TYPE_BUY) { // 根据持仓类型,执行平仓操作
e.SetDirection("closebuy"); // 设置期货交易方向
e.Sell(ticker.Buy, positions[i].Amount); // 下单函数
} else {
e.SetDirection("closesell");
e.Buy(ticker.Sell, positions[i].Amount);
}
found++; // 标记累计
}
}
if (found == 0) { // 如果标记变量found为0,则没有仓位需要处理,跳出while循环
break;
}
Sleep(2000); // 间隔2秒
StripOrders(e); // 撤销当前所有挂单
}
return coverAmount; // 返回平仓的数量
}
function loop(pos) {
var tradeType = null; // 初始化交易方向
if (typeof(pos) == 'undefined' || !pos) { // 判断是否是首轮执行
tradeType = FirstTradeType;
pos = Trade(exchange, tradeType, OpAmount, OpMode, SlidePrice, MaxSpace, Interval); // 首笔交易
if (!pos) {
throw "出师不利, 开仓失败";
} else {
Log(tradeType == ORDER_TYPE_BUY ? "开多仓完成" : "开空仓完成", "均价:", pos.Price, "数量:", pos.Amount);
}
} else {
tradeType = pos.Type; // 根据持仓方向继续指定交易方向
}
var holdPrice = pos.Price; // 持仓价格
var holdAmount = pos.Amount; // 持仓数量
var openFunc = tradeType == ORDER_TYPE_BUY ? exchange.Buy : exchange.Sell; // 多头持仓,开仓为买,否则开仓为卖
var coverFunc = tradeType == ORDER_TYPE_BUY ? exchange.Sell : exchange.Buy; // 多头持仓,平仓为卖,否则平仓位买
var reversePrice = 0; // 反手价格
var coverPrice = 0; // 平仓价格
var canOpen = true; // 可开仓标记
if (tradeType == ORDER_TYPE_BUY) {
reversePrice = _N(holdPrice * (1 - StopLoss), 4); // 止损价格
coverPrice = _N(holdPrice * (1 + StopProfit), 4); // 止盈价格
} else {
reversePrice = _N(holdPrice * (1 + StopLoss), 4);
coverPrice = _N(holdPrice * (1 - StopProfit), 4);
}
var coverId = null;
var msg = "持仓价: " + holdPrice + " 止损价: " + reversePrice;
for (var i = 0; i < 10; i++) { // 控制最多下单10次
if (coverId) { // 订单ID为空,不触发break,继续循环,直到10次
break;
}
if (tradeType == ORDER_TYPE_BUY) { // 根据方向下单,挂平仓单,即止盈的订单
exchange.SetDirection("closebuy");
coverId = exchange.Sell(coverPrice, holdAmount, msg);
} else {
exchange.SetDirection("closesell");
coverId = exchange.Buy(coverPrice, holdAmount, msg);
}
Sleep(Interval);
}
if (!coverId) { // 10次下单还失败抛出错误,策略停止
StripOrders(exchange); // 撤销所有挂单
Log("下单失败", "@") // 增加推送提醒
throw "下单失败"; // 抛出错误,让机器人停止
}
while (true) { // 进入检测反手的循环
Sleep(Interval);
var ticker = GetTicker(exchange); // 获取最新的行情
if ((tradeType == ORDER_TYPE_BUY && ticker.Last < reversePrice) || (tradeType == ORDER_TYPE_SELL && ticker.Last > reversePrice)) { // 检测触发止损即反手
StripOrders(exchange); // 挂单全部撤单
var coverAmount = coverFutures(exchange, tradeType); // 持仓全平
if (_Failed >= MaxLoss) { // 如果超过最大止损次数(反手次数),跳出循环重新开始
Counter.f++; // 计一次失败
Log("超过最大失败次数", MaxLoss);
break; // 跳出循环
}
var reverseAmount = _N(coverAmount * ReverseRate, 4); // 根据平仓的量,进行交易量加倍
var account = GetAccount(exchange, true); // 更新账户信息,此时不能有资产冻结
// 检测账户资产是否足够,不足跳出循环,重新开始,如同_Failed >= MaxLoss
if (_N(account.Stocks * MarginLevel, 4) < reverseAmount) { // 检测资产是否足够
Log("没有币反手开仓, 需要开仓: ", reverseAmount, "个, 只有", account.Stocks, "个币");
Counter.f++;
break;
}
var reverseType = tradeType; // 记录反转操作类型,默认顺仓
if (ReverseMode == 0) { // 反手模式影响的调整,即如果参数设置了反仓,这里调整
reverseType = tradeType == ORDER_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; // 反仓就是指,刚才持仓是多,这次反手就做空,刚才是做空,这次就做多
}
var pos = Trade(exchange, reverseType, reverseAmount, OpMode, SlidePrice, MaxSpace, Interval); // 加倍的下单操作
if (pos) { // 检测交易逻辑执行之后的持仓
Log(reverseType == ORDER_TYPE_BUY ? "多仓" : "空仓", "加倍开仓完成");
}
return pos; // 返回持仓结构
} else { // 没有触发反手时执行的逻辑
var orders = _C(exchange.GetOrders); // 当止盈挂单成交,记录胜次数加1
if (orders.length == 0) {
Counter.s++;
var account = GetAccount(exchange, true); // 更新账户资产
LogProfit(account.Stocks, account); // 打印账户资产
break;
}
}
}
// 非while循环内正常return 的,返回null,例如止盈成功,超过失败次数,资产不足
return null;
}
function onexit() { // 机器人停止时,执行扫尾函数onexit
StripOrders(exchange); // 撤销所有挂单
Log("Exit");
}
function main() {
if (exchange.GetName().indexOf("Futures") == -1) { // 检测当前添加的第一个交易所对象是不是期货交易所
throw "只支持期货, 现货暂不支持";
}
// EnableLogLocal(SaveLocal);
if (exchange.GetRate() != 1) { // 不启用汇率转换
Log("已禁用汇率转换");
exchange.SetRate(1);
}
StopProfit /= 100; // 参数处理为小数,假设StopProfit为1表示要1%止盈,重新计算赋值,StopProfit的值为0.01即1%
StopLoss /= 100; // 止损(反手)同上
var eName = exchange.GetName();
if (eName == "Futures_CTP") { // 检测添加的第一个交易所对象是否为商品期货,如果是,抛出错误信息,让机器人停止
throw "暂只支持数字货币期货"
}
exchange.SetContractType(Symbol); // 设置数字货币合约代码,即要交易、操作的合约
exchange.SetMarginLevel(MarginLevel); // 设置杠杆
Interval *= 1000; // 轮询间隔参数由秒转换为毫秒
SetErrorFilter("502:|503:|unexpected|network|timeout|WSARecv|Connect|GetAddr|no such|reset|http|received|EOF"); // 设置屏蔽的错误类型
StripOrders(exchange); // 取消所有挂单
OrgAccount = GetAccount(exchange, true); // 获取当前账户信息
LogStatus("启动成功"); // 更新状态栏信息
var pos = null; // 初始化main函数内的局部变量pos为null,用来记录持仓数据结构
var positions = GetPosition(exchange); // 获取当前持仓,调用的是封装后的GetPosition不带orderType参数是要获取全部持仓,注意调用的并非是API接口exchange.GetPosition
if (positions.length == 1) { // 如果开始时有持仓,赋值给pos变量
pos = positions[0];
Log("发现一个仓位, 已经自动恢复进度");
} else if (positions.length > 1) { // 有多个持仓时,为策略不可运行状态,策略抛出错误让机器人停止
throw "发现持仓超过1个";
}
while (true) { // 策略主循环
pos = loop(pos); // 执行交易逻辑主要的函数loop,pos作为参数,并且返回新的持仓数据结构
if (!pos) { // 该条件触发,返回null的情况,例如止盈成功,超过失败次数,资产不足
_Failed = 0; // 重置止损次数为0,重来
} else {
_Failed++; // 累计止损次数
}
Sleep(Interval);
}
}
读完策略代码可以发现,策略逻辑其实并不复杂,代码也并不算多,但是设计上可谓匠心独运,很多地方可以借鉴参考。
策略交易逻辑的主要函数为loop
函数,在main
函数的主循环中反复调用,当loop
函数开始执行时,首先下单持仓,然后挂单止盈,等待止盈订单成交。之后进入检测状态,检测两项内容。
策略逻辑简单描述如此,但是还是有一些其它细节的,比如最大反手次数的设置,账户资产可用的检测,下单失败最大次数10次的处理等。
策略中有些函数都做了根据参数不同而行为差异化的设计,例如:StripOrders
函数,GetAccount
函数,GetPosition
函数。这些函数根据参数传入差异,有不同的行为。这样很好的复用了代码,避免了代码冗余,让策略设计简洁易懂。
原策略:https://www.fmz.com/strategy/3648
反手加倍有一定的风险,特别是在期货上,策略仅为学习,实盘慎用,欢迎留言讨论。