Type/to search
2
Follow
496
Followers
冒险者的游戏:滚仓策略的代码实现和应用
Discussions
Created 2025-12-19 17:06:01  Updated 2025-12-29 09:04:46
 0
 732

img

引言

在量化交易领域,滚仓(Rolling Position)策略是一个颇具吸引力但也充满挑战的话题。这个策略的核心理念是:在趋势行情中,通过将已实现的盈利再次投入交易,实现复利增长。本文将深入探讨如何将滚仓这个交易想法,一步步转化为可执行的代码逻辑,重点关注思维转换的过程而非技术细节。需要特别说明的是,滚仓策略在放大收益的同时也会放大风险,本文仅供学习交流参考。


一、滚仓策略的盈利逻辑深度解析

1.1 滚仓的数学本质

滚仓示意图

滚仓策略的盈利逻辑本质上是一个复利增长模型。让我们用一个简化的例子来理解:

传统单次交易(连续3次各涨10%):

  • 初始资金:100 USDT,杠杆3倍
  • 市场涨幅:(1+10%) × (1+10%) × (1+10%) - 1 = 33.1%
  • 盈利:100 × 3 × 33.1% = 99.3 USDT
  • 最终:199.3 USDT

滚仓交易(连续3次各涨10%):

  • 第1次:100 USDT → 盈利30 USDT → 资金变130 USDT
    • 计算:100 × 3倍杠杆 × 10%涨幅 = 30
  • 第2次:130 USDT → 盈利39 USDT → 资金变169 USDT
    • 计算:130 × 3倍杠杆 × 10%涨幅 = 39
  • 第3次:169 USDT → 盈利50.7 USDT → 资金变219.7 USDT
    • 计算:169 × 3倍杠杆 × 10%涨幅 = 50.7

对比结果:

同样市场连续3次各涨10%的情况下:

  • 单次交易:盈利 99.3 USDT
  • 滚仓交易:盈利 119.7 USDT
  • 复利优势:20.4 USDT(提升约20.5%)

同样是连续3次各涨10%,单次交易盈利99.3 USDT,滚仓交易盈利119.7 USDT。这个差额就是复利的力量

用数学公式表达:

javascript
// 传统交易:线性增长 最终资金 = 初始资金 × (1 + 杠杆 × 涨幅) // 滚仓交易:指数增长 最终资金 = 初始资金 × (1 + 杠杆 × 单次涨幅) ^ 滚仓次数

这揭示了滚仓的本质:将线性增长转化为指数增长。但同时也暴露了风险:一次止损可能回吐之前所有复利成果

1.2 滚仓策略的三个核心问题

在动手编写任何代码之前,我们需要从策略层面回答三个根本问题:

问题1:什么时候开始?(初次入场)
需要判断趋势的开始信号。

问题2:什么时候继续?(追加滚仓)
这是滚仓的核心:止盈后如何判断趋势是否延续?

问题3:什么时候停止?(退出观望)

  • 主动退出:趋势转弱
  • 被动退出:触发止损

这三个问题决定了整个策略的骨架,下面我们将逐一把它们转化为代码逻辑。


二、问题一:什么时候开始?——寻找入场的起爆点

入场信号

2.1 滚仓策略的理想与现实

让我们先理解滚仓策略最理想的应用场景。

理想场景:
想象一下,如果你能在SHIB从0.000001美元起涨时入场,或者在某个山寨币暴涨前夕建仓,通过连续滚仓,100 USDT可能变成10000 USDT甚至更多。这就是滚仓策略的终极梦想——在币种起爆前入场,抓住十倍甚至百倍的收益

残酷现实:
但问题在于,如何知道哪个币种会暴涨?什么时候会暴涨?

  • 如果你是项目方或内幕人士,可能提前知道利好消息
  • 如果你是普通交易者,只能通过市场信号去判断

对于我们大多数人来说,想要精准捕捉这种起爆点,说白了就是撞大运。我们无法预知未来,只能通过历史数据和技术指标,尽可能提高"撞到大运"的概率。

2.2 从理想回归现实:技术指标的模拟入场

既然无法预知哪个币种会暴涨,我们能做的就是:建立一套可执行的入场规则,用技术指标模拟"趋势开始"的信号

这就像在茫茫大海中捕鱼,虽然不知道哪里有大鱼,但我们可以:

  • 观察水面波纹(价格波动)
  • 分析水流方向(趋势方向)
  • 选择合适的工具(技术指标)

当多个信号汇聚时,我们认为"可能"有趋势要开始了,于是入场尝试。对了,就跟着趋势滚仓赚钱;错了,及时止损退出。

2.3 入场信号的技术实现

选择技术工具:
我们使用EMA双均线系统(EMA5和EMA10)作为趋势判断工具。选择它的原因很简单:

  • 简单直观,易于验证
  • 能快速反应价格变化
  • 参数平衡了灵敏度和稳定性

核心逻辑:
通过检测均线的"金叉"(EMA5上穿EMA10)和"死叉"(EMA5下穿EMA10)来捕捉趋势转折点:

  • 金叉 → 做多信号
  • 死叉 → 做空信号

代码思路:

javascript
// 计算EMA指标 var emaFast = TA.EMA(records, FastEMA); // EMA5 var emaSlow = TA.EMA(records, SlowEMA); // EMA10 // 获取当前和前一根K线的EMA值 var ema5_current = emaFast[emaFast.length - 1]; var ema5_prev = emaFast[emaFast.length - 2]; var ema10_current = emaSlow[emaSlow.length - 1]; var ema10_prev = emaSlow[emaSlow.length - 2]; // 检测金叉:前一根K线EMA5<=EMA10,当前K线EMA5>EMA10 var bullCross = ema5_prev <= ema10_prev && ema5_current > ema10_current; // 检测死叉:前一根K线EMA5>=EMA10,当前K线EMA5<EMA10 var bearCross = ema5_prev >= ema10_prev && ema5_current < ema10_current; // 空仓时等待信号入场 if (bullCross) { Log("📈 金叉信号 - 做多"); openPosition("LONG", currentPrice); } else if (bearCross) { Log("📉 死叉信号 - 做空"); openPosition("SHORT", currentPrice); }

这里不深入展开金叉死叉的细节,这些都是交易中的基础概念。重点是:我们需要一个明确的、可量化的入场信号来触发滚仓的起点


三、问题二:什么时候继续?——复利滚仓的核心机制

滚仓机制

3.1 理解滚仓的本质:一个理智的冒险者游戏

滚仓策略本质上是一个理智的冒险者游戏。让我们用一个完整的场景来理解:

游戏规则:

1. 你从交易所账户中拿出100 USDT作为冒险资金 2. 这100 USDT独立管理,与账户其他资金隔离 3. 用这100 USDT开始交易: - 赚了 → 盈利加入资金池,继续用更大的资金交易(滚仓) - 亏了 → 触发止损,回到空仓状态 4. 重复这个过程,直到: - 要么把100 USDT亏完(游戏结束) - 要么滚到一个满意的金额(主动退出)

这个游戏的精妙之处在于:

  • 风险可控:最多亏100 USDT,不会影响账户其他资金
  • 收益无限:如果趋势配合,复利可以让资金快速翻倍
  • 进退有据:有明确的止盈、止损、滚仓规则

3.2 资金池的设计:实现复利的关键

这是滚仓策略中最核心的设计思想。

传统做法的问题:
假设你的交易所账户有1000 USDT:

  • 第1次开仓用100 USDT
  • 盈利30 USDT后,账户变成1030 USDT
  • 第2次开仓该用多少?100还是130?
  • 如何区分这是滚仓策略赚的还是其他操作的收益?

资金池解决方案:

javascript
// 创建一个虚拟的"策略资金池" var strategyCapital = InitialCapital; // 初始100 USDT // 第1次交易 // 开仓金额 = 100 USDT // 止盈后盈利 = 30 USDT strategyCapital = strategyCapital + 30; // 资金池变为130 USDT // 第2次交易(滚仓) var positionValue = strategyCapital * Leverage; // 130 × 3 = 390 var amount = positionValue / price / ctVal; // 计算开仓数量 // 自动使用了第1次的盈利,这就是复利的关键 // 止盈后盈利 = 39 USDT strategyCapital = strategyCapital + 39; // 资金池变为169 USDT // 第3次交易(滚仓) // 开仓金额 = 169 USDT(继续利滚利)

这个设计的优势:

  • 资金隔离: 策略只动用指定的100 USDT,不影响账户其他资金
  • 自动复利: 每次盈利自动加入资金池,下次开仓自然用更大金额
  • 风险可控: 最坏情况亏完100 USDT,不会超出预期
  • 清晰追踪: 可以精确知道策略从100 USDT滚到了多少

3.3 滚仓决策:止盈后继续还是停止?

这是滚仓策略的灵魂环节:当止盈单成交后,我们要做一个关键决策——继续滚还是停手?

决策场景:

假设我们做多BTC: - 入场价:45000 USDT,用100 USDT开仓 - 止盈价:49500 USDT(涨10%) - 止盈成交,盈利30 USDT - 现在资金池:130 USDT 问题来了: 选项A:收手,带着130 USDT退出,回到空仓 选项B:继续,用130 USDT再次开多(滚仓)

如何选择?

这个决策不能靠"感觉",必须有明确标准。我们的判断逻辑是:趋势是否还在延续?

判断方法:
在止盈单成交的瞬间,重新计算最新的技术指标(EMA均线):

javascript
// 止盈单成交后,获取最新K线数据 var records = _C(exchange.GetRecords, PERIOD_M1); var emaFast = TA.EMA(records, FastEMA); var emaSlow = TA.EMA(records, SlowEMA); var ema5_current = emaFast[emaFast.length - 1]; var ema10_current = emaSlow[emaSlow.length - 1]; var shouldRoll = false; if (currentDirection == "LONG") { // 多头止盈后,如果EMA5仍在EMA10上方,继续做多(滚仓) if (ema5_current > ema10_current) { shouldRoll = true; Log("✅ EMA5 > EMA10,上升趋势未破坏"); Log("🔄 决策:继续做多(滚仓)"); } else { Log("❌ EMA5 <= EMA10,趋势可能转弱"); Log("⏸️ 决策:不滚仓,等待新信号"); } } else if (currentDirection == "SHORT") { // 空头止盈后,如果EMA5仍在EMA10下方,继续做空(滚仓) if (ema5_current < ema10_current) { shouldRoll = true; Log("✅ EMA5 < EMA10,下降趋势未破坏"); Log("🔄 决策:继续做空(滚仓)"); } else { Log("❌ EMA5 >= EMA10,趋势可能转弱"); Log("⏸️ 决策:不滚仓,等待新信号"); } }

3.4 滚仓执行流程

如果决策是"继续滚仓":

javascript
if (shouldRoll) { // 1. 增加滚仓计数 currentRoundRolls++; Log("🔄 执行滚仓操作... (本轮第", currentRoundRolls, "次滚仓)"); // 2. 获取最新价格 var ticker = _C(exchange.GetTicker); var newPrice = ticker.Last; // 3. 基于新资金池重新开仓 if (openPosition(currentDirection, newPrice)) { Log("✅ 滚仓成功!"); // 4. 挂新的止盈单(在openPosition函数中完成) // 5. 设置新的止损价(在checkStopLoss函数中监控) } else { Log("❌ 滚仓失败,等待新信号"); saveRollRecord(false); resetPositionState(); } }

如果决策是"停止":

javascript
else { // 1. 保存本轮统计 saveRollRecord(false); // false表示正常结束,非止损 // 2. 保留资金池金额 // strategyCapital 保持当前值,等待下次机会 // 3. 回到空仓状态 resetPositionState(); Log("⏳ 已平仓,等待新信号..."); }

这个流程的关键点:

  • 每次止盈后立即判断,不拖延
  • 判断标准客观(均线关系),不主观臆测
  • 继续就加大仓位,停止就保留成果

3.5 复利的威力与代价

让我们通过一个完整案例感受复利的威力:

成功案例:

初始资金:100 USDT 止盈比例:10% 杠杆:3倍 第1次:100 USDT → 盈利30 → 资金池130 第2次:130 USDT → 盈利39 → 资金池169 第3次:169 USDT → 盈利50.7 → 资金池219.7 第4次:219.7 USDT → 盈利65.9 → 资金池285.6 第5次:285.6 USDT → 盈利85.7 → 资金池371.3 连续滚5次,100变成371.3,增长271%!

失败案例:

第1次:100 USDT → 盈利30 → 资金池130 第2次:130 USDT → 盈利39 → 资金池169 第3次:169 USDT → 趋势反转 → 触发止损 止损比例5%,亏损:169 × 3 × 5% = 25.35 USDT 剩余资金:169 - 25.35 = 143.65 USDT 原本从100滚到169,一次止损后只剩143.65

这就是滚仓的双刃剑:

  • 成功时: 指数级增长,令人振奋
  • 失败时: 快速回吐,甚至亏损

四、问题三:什么时候停止?——止损是最后的防线

止损机制

4.1 两种退出方式

主动退出:趋势转弱
这种情况在"问题二"中已经处理了——止盈后判断趋势不支持继续,主动选择停止。这是理想的退出方式,带着盈利离场。

被动退出:触发止损
这是我们现在要重点讨论的——当行情走反,价格触及止损线,被迫平仓。

4.2 止损的必要性

很多人不喜欢止损,因为:

  • 止损意味着认错
  • 止损会产生实际亏损
  • 有时候止损后价格又涨回来了

但在滚仓策略中,止损是保命的底线。想想看:

如果没有止损: 第1次:100 → 滚到 169 第2次:169 → 趋势反转,不止损 价格持续下跌:169 → 150 → 120 → 80 → 50... 最终可能全亏,甚至爆仓
如果有止损: 第1次:100 → 滚到 169 第2次:169 → 趋势反转,触发止损 止损5%:亏损 25.35 剩余:143.65 虽然亏了,但保留了大部分资金 可以等待下一个机会

止损的本质: 用小的确定性亏损,避免大的不确定性风险。

4.3 止损的代码实现

javascript
// 检查止损 function checkStopLoss(currentPrice, position) { var totalDrawdown = 0; // 计算当前回撤 if (currentDirection == "LONG") { totalDrawdown = (currentPrice - entryPrice) / entryPrice; } else { totalDrawdown = (entryPrice - currentPrice) / entryPrice; } // 判断是否触发止损 if (totalDrawdown < -StopLossPercent) { Log("❌ 触发止损!回撤:", (totalDrawdown * 100).toFixed(2), "%"); // 1. 取消止盈单 if (takeProfitOrderId) { Log("取消止盈单:", takeProfitOrderId); exchange.CancelOrder(takeProfitOrderId); takeProfitOrderId = null; Sleep(500); } // 2. 市价平仓(循环重试直到成功) var profit = closePositionMarketWithRetry(currentPrice, position); // 3. 更新策略资金池 strategyCapital += profit; // profit是负数 totalProfitRealized += profit; Log("止损亏损:", profit.toFixed(2), "U"); Log("策略剩余资金:", strategyCapital.toFixed(2), "U"); // 4. 记录本轮止损亏损 currentRoundLoss = Math.abs(profit); Log("本轮止损亏损:", currentRoundLoss.toFixed(2), "U"); // 5. 保存本轮滚仓记录(被止损中断) saveRollRecord(true); // true表示止损结束 // 6. 重置状态 resetPositionState(); // 7. 检查资金是否充足 if (strategyCapital < 10) { Log("💥 策略资金不足10U,停止运行"); throw "资金不足"; } Log("⏳ 已止损,等待新信号..."); } }

4.4 游戏结束的条件

还记得我们说的"理智的冒险者游戏"吗?这个游戏有明确的结束条件:

条件1:资金池归零

javascript
if (strategyCapital <= 0) { Log("💥 游戏结束:资金池已归零"); Log("本次冒险失败,100 USDT全部亏光"); throw "资金耗尽"; }

条件2:主动退出

javascript
if (strategyCapital >= 目标金额) { Log("🎉 达到目标金额,可以选择主动退出"); Log("锁定利润,开始新一轮100 USDT的游戏"); }

条件3:达到最大滚仓次数

javascript
if (连续滚仓次数 >= 10次) { Log("⚠️ 达到最大滚仓次数,主动退出"); Log("持续时间太长,风险累积,见好就收"); saveRollRecord(false); resetPositionState(); }

4.5 风险与收益的平衡

整个滚仓策略的设计,核心就是在风险与收益之间找平衡

收益端:

  • 复利增长:每次止盈后资金池变大
  • 趋势捕捉:在上涨/下跌趋势中持续获利
  • 无上限:理论上可以无限滚下去

风险端:

  • 止损保护:单次最多亏损资金池的5%
  • 资金隔离:最多亏100 USDT
  • 趋势判断:避免在震荡市中频繁止损

五、实战回测:TRUMP_USDT案例分析

TRUMP_USDT 币安期货上市首日(2025年1月20日至2025年1月21日)回测参考分析:

回测权益曲线

滚仓统计表格

从回测结果可以看到:

亮点表现:

  • 策略成功捕捉到了TRUMP上市初期的剧烈波动
  • 通过多次滚仓,实现了资金的快速增长
  • 止盈机制有效锁定了趋势中的利润

风险暴露:

  • 在趋势反转时,止损导致部分利润回吐
  • 震荡行情中出现了假突破信号
  • 单一币种的集中风险较高

关键数据:

  • 总滚仓次数:X次
  • 最大单轮滚仓:X次
  • 最大回撤:X%
  • 最终收益率:X%

六、策略的本质与局限

6.1 这个策略在模拟什么?

通过上述分析,我们可以清楚地看到,这个策略本质上是在模拟:

一个理智的冒险者的交易行为:

  • 有明确的入场规则(不是冲动交易)
  • 有止盈目标(不贪婪)
  • 有止损纪律(不死扛)
  • 有滚仓判断(会利用盈利)
  • 有资金限制(控制风险)

它的核心逻辑是:

  1. 拿出固定资金(100 USDT)进行尝试
  2. 遇到趋势,跟随趋势赚钱
  3. 赚到钱后,用盈利继续交易(利滚利)
  4. 如果趋势转弱,及时停止
  5. 如果判断错误,快速止损
  6. 直到资金亏完或滚到满意金额

6.2 策略的局限性

局限1:依赖趋势市场
这个策略在震荡市中表现很差,因为:

  • 频繁出现假突破
  • 止盈后又回调,无法滚仓
  • 反复止损,消耗资金池

局限2:参数敏感
止盈10%、止损5%这些参数并非最优:

  • 不同币种波动率不同
  • 不同市况需要不同参数
  • 固定参数难以适应所有情况

局限3:无法预测起爆点
正如前面所说,我们用技术指标入场,本质上是"撞大运":

  • 可能错过真正的大行情
  • 可能在假突破时入场
  • 无法像内幕人士那样提前布局

6.3 改进方向

方向1:结合工作流筛选币种

  • 不是随便找个币种就滚
  • 而是先用工作流筛选出热门、爆发力强的币种
  • 比如:社交媒体讨论度激增、交易量异常、链上数据活跃等
  • 在这些币种上使用滚仓策略,成功率会更高

方向2:动态调整参数

  • 根据币种的历史波动率调整止盈止损比例
  • 波动大的币种,适当放大止损空间
  • 波动小的币种,可以降低止盈目标

方向3:多资金池并行

  • 不是把100 USDT放在一个币种
  • 而是分成5个20 USDT,在5个潜力币种上同时滚
  • 分散风险,提高"撞到大运"的概率

结语

通过三个核心问题的推演,我们完整地展示了如何将滚仓这个交易想法转化为代码逻辑。这个过程的本质是:把一个理智的冒险者的交易思维,用精确的规则和数据结构表达出来。

重要提示:

这只是一个滚仓思路的模拟实现,实际上滚仓是一个非常需要市场经验的交易策略。本策略只是一个工具,后期可以结合工作流判断热门或者极具爆发力的币种,使用该工具,会为我们带来更多惊喜。

记住:

  • 最多亏100 USDT,风险可控
  • 如果运气好遇到大趋势,可能翻几倍甚至几十倍
  • 但更多时候,可能是小赚小亏,反复试探
  • 这是一场需要耐心和纪律的游戏

没有稳赚不赔的策略,滚仓只是一个工具。真正决定成败的,是你能否:

  • 找到有潜力的币种(结合工作流筛选)
  • 坚持执行止损(不要死扛)
  • 在大趋势来临时敢于滚仓(不要过早退出)
  • 保持理智(不被情绪左右)

祝各位在量化交易的道路上,找到属于自己的"大运"!

完整策略地址:**策略源码 -> ** https://www.fmz.com/strategy/521864

完整策略代码:

javascript
/*backtest start: 2025-01-20 00:00:00 end: 2025-01-21 00:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_Binance","currency":"TRUMP_USDT","balance":5000}] */ // ============================================ // 滚仓策略 - EMA5/EMA10 简化版 // 使用 CreateOrder 统一下单 // 持续检测订单状态 // 止盈后根据EMA关系决定是否滚仓 // 新增:滚仓统计功能(三个两行表格) // 修复:方向记录、亏损记录、入场价格记录 // 优化:市价平仓循环重试直到成功 // 优化:滚仓统计表格新增开始/结束时间 // ============================================ // ========== 策略参数(可调整)========== var Symbol = "TRUMP_USDT.swap"; // 交易币种 var InitialCapital = 100; // 策略初始资金 100U var Leverage = 3; // 杠杆倍数 var RollProfitPercent = 0.10; // 滚仓盈利系数(10% = 0.10) var StopLossPercent = 0.05; // 止损系数(10% = 0.10) // EMA参数 var FastEMA = 5; var SlowEMA = 10; // 全局变量 var strategyCapital = InitialCapital; var entryPrice = 0; var lastRollPrice = 0; var rollCount = 0; var totalProfitRealized = 0; var currentDirection = ""; var takeProfitOrderId = null; // 止盈单ID var amountPrecision = 0; // 数量精度 var pricePrecision = 2; // 价格精度 var ctVal = 1; // 合约面值 // ========== 滚仓统计变量 ========== var currentRoundRolls = 0; // 本轮滚仓次数(连续滚仓) var currentRoundStartTime = 0; // 本轮开始时间 var currentRoundDirection = ""; // 本轮方向 var currentRoundTotalProfit = 0; // 本轮累计盈利(每次止盈累加) var currentRoundLoss = 0; // 本轮亏损(止损时记录) var currentRoundEntryPrice = 0; // 本轮入场价格 var rollHistory = []; // 滚仓历史记录 var maxHistoryRecords = 10; // 保留最近10次滚仓记录 function main() { Log("=== EMA滚仓策略启动(CreateOrder模式 + 滚仓统计)==="); Log("交易币种:", Symbol); Log("━━━━━━━━━━━━━━━━━━━━"); // 获取市场信息 var markets = exchange.GetMarkets(); if (!markets || !markets[Symbol]) { Log("❌ 错误:无法获取", Symbol, "的市场信息"); return; } var marketInfo = markets[Symbol]; amountPrecision = marketInfo.AmountPrecision; pricePrecision = marketInfo.PricePrecision || 2; ctVal = marketInfo.CtVal; Log("市场信息:"); Log(" - 数量精度:", amountPrecision); Log(" - 价格精度:", pricePrecision); Log(" - 合约面值:", ctVal); var account = _C(exchange.GetAccount); Log("账户总资金:", account.Balance.toFixed(2), "U"); Log("策略使用资金:", InitialCapital, "U"); Log("杠杆倍数:", Leverage, "倍"); Log("滚仓系数:", (RollProfitPercent * 100), "%"); Log("止损系数:", (StopLossPercent * 100), "%"); Log("━━━━━━━━━━━━━━━━━━━━"); if (account.Balance < InitialCapital) { Log("❌ 错误:账户余额不足"); return; } exchange.SetContractType("swap"); exchange.SetMarginLevel(Leverage); var lastBarTime = 0; while (true) { var records = _C(exchange.GetRecords, PERIOD_M1); if (records.length < SlowEMA + 5) { Sleep(3000); continue; } var currentBarTime = records[records.length - 1].Time; if (currentBarTime == lastBarTime) { Sleep(1000); continue; } lastBarTime = currentBarTime; var ticker = _C(exchange.GetTicker); var currentPrice = ticker.Last; var account = _C(exchange.GetAccount); var position = _C(exchange.GetPositions); // 计算EMA var emaFast = TA.EMA(records, FastEMA); var emaSlow = TA.EMA(records, SlowEMA); if (emaFast.length < 3 || emaSlow.length < 3) { Sleep(3000); continue; } var ema5_current = emaFast[emaFast.length - 1]; var ema5_prev = emaFast[emaFast.length - 2]; var ema10_current = emaSlow[emaSlow.length - 1]; var ema10_prev = emaSlow[emaSlow.length - 2]; var isBullTrend = ema5_current > ema10_current; var isBearTrend = ema5_current < ema10_current; var bullCross = ema5_prev <= ema10_prev && ema5_current > ema10_current; var bearCross = ema5_prev >= ema10_prev && ema5_current < ema10_current; if(takeProfitOrderId){ checkTakeProfitOrder(); } // ========== 持仓逻辑 ========== if (position.length > 0) { var pos = position[0]; currentDirection = pos.Type == PD_LONG ? "LONG" : "SHORT"; if (entryPrice == 0) { entryPrice = pos.Price; lastRollPrice = pos.Price; } // 检查止损 checkStopLoss(currentPrice, pos); } else { // ========== 空仓:等待信号 ========== if (bullCross) { Log("📈 金叉信号 - 做多"); openPosition("LONG", currentPrice); } else if (bearCross) { Log("📉 死叉信号 - 做空"); openPosition("SHORT", currentPrice); } } showStatus(account, position, currentPrice, ema5_current, ema10_current, isBullTrend, currentBarTime); Sleep(1000); } } // 开仓(持续检测订单状态) function openPosition(direction, price) { Log("🚀 开仓", direction == "LONG" ? "做多" : "做空"); Log("使用资金:", strategyCapital.toFixed(2), "U"); var positionValue = strategyCapital * Leverage; var amount = _N(positionValue / price / ctVal, amountPrecision); Log("计算数量:", amount, "| 持仓价值:", positionValue.toFixed(2), "U"); if (amount <= 0) { Log("❌ 数量无效"); return false; } // 使用 CreateOrder 市价开仓 var orderId = exchange.CreateOrder(Symbol, direction == "LONG" ? "buy" : "sell", -1, amount); if (!orderId) { Log("❌ 下单失败"); return false; } Log("订单ID:", orderId, "开始持续检测..."); // 持续检测订单状态,直到成交或超时 var maxWaitTime = 30000; // 最多等待30秒 var startTime = Date.now(); var checkCount = 0; while (Date.now() - startTime < maxWaitTime) { Sleep(500); checkCount++; var order = exchange.GetOrder(orderId); if (!order) { Log("❌ 无法获取订单信息"); continue; } if (order.Status == 1) { // 订单已成交 var avgPrice = order.AvgPrice; entryPrice = avgPrice; lastRollPrice = avgPrice; currentDirection = direction; // ========== 修改:无论是否第一次,都要初始化/更新统计数据 ========== if (currentRoundRolls == 0) { // 第一次开仓:初始化所有统计数据 currentRoundStartTime = Date.now(); currentRoundDirection = direction; currentRoundTotalProfit = 0; currentRoundLoss = 0; currentRoundEntryPrice = avgPrice; Log("🆕 开始新一轮交易统计"); Log(" - 开始时间:", _D(currentRoundStartTime)); Log(" - 方向:", direction == "LONG" ? "🟢 多头" : "🔴 空头"); Log(" - 入场价格:", avgPrice.toFixed(pricePrecision)); } else { // 滚仓时:更新方向(理论上应该相同,但为了健壮性还是更新) currentRoundDirection = direction; Log("🔄 滚仓操作 (第", currentRoundRolls, "次)"); Log(" - 方向:", direction == "LONG" ? "🟢 多头" : "🔴 空头"); Log(" - 入场价格:", avgPrice.toFixed(pricePrecision)); } Log("✅ 开仓成功!"); Log(" - 成交均价:", avgPrice.toFixed(pricePrecision)); Log(" - 成交数量:", order.DealAmount); Log(" - 成交金额:", (order.DealAmount * avgPrice * ctVal).toFixed(2), "U"); // 挂止盈单 Sleep(1000); placeTakeProfitOrder(direction, avgPrice, order.DealAmount); return true; } else if (order.Status == 2) { // 订单已取消 Log("❌ 订单已取消"); return false; } // Status == 0 表示未成交,继续等待 } // 超时未成交 Log("⚠️ 订单超时,尝试取消订单"); exchange.CancelOrder(orderId); return false; } // 挂止盈单 function placeTakeProfitOrder(direction, entryPrice, amount) { var takeProfitPrice = 0; if (direction == "LONG") { takeProfitPrice = _N(entryPrice * 1.1, pricePrecision); // 多头止盈:+10% } else { takeProfitPrice = _N(entryPrice * 0.9, pricePrecision); // 空头止盈:-10% } Log("📌 挂止盈单"); Log(" - 入场价格:", entryPrice.toFixed(pricePrecision)); Log(" - 止盈价格:", takeProfitPrice); Log(" - 数量:", amount); // 使用 CreateOrder 挂限价止盈单 if (direction == "LONG") { takeProfitOrderId = exchange.CreateOrder(Symbol, "closebuy", takeProfitPrice, amount); } else { takeProfitOrderId = exchange.CreateOrder(Symbol, "closesell", takeProfitPrice, amount); } if (takeProfitOrderId) { Log("✅ 止盈单已挂,订单ID:", takeProfitOrderId); } else { Log("❌ 止盈单挂单失败"); } } // 检查止盈单状态 function checkTakeProfitOrder() { if (!takeProfitOrderId) { return; } var order = exchange.GetOrder(takeProfitOrderId); if (!order) { return; } if (order.Status == 1) { // 止盈单成交 Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Log("💰 止盈单成交!"); Log(" - 成交价格:", order.AvgPrice.toFixed(pricePrecision)); Log(" - 成交数量:", order.DealAmount); // 使用订单数据精确计算盈利 var profit = 0; if (currentDirection == "LONG") { // 多头盈利 = (止盈价 - 入场价) * 数量 * 合约面值 profit = (order.AvgPrice - entryPrice) * order.DealAmount * ctVal; } else { // 空头盈利 = (入场价 - 止盈价) * 数量 * 合约面值 profit = (entryPrice - order.AvgPrice) * order.DealAmount * ctVal; } // 计算盈利率 var profitRate = profit / strategyCapital; Log("📊 盈利统计:"); Log(" - 入场价格:", entryPrice.toFixed(pricePrecision)); Log(" - 止盈价格:", order.AvgPrice.toFixed(pricePrecision)); Log(" - 本次盈利:", profit.toFixed(2), "U"); Log(" - 盈利率:", (profitRate * 100).toFixed(2), "%"); Log(" - 策略资金(盈利前):", strategyCapital.toFixed(2), "U"); // 更新资金 strategyCapital += profit; totalProfitRealized += profit; rollCount++; Log(" - 策略资金(盈利后):", strategyCapital.toFixed(2), "U"); Log(" - 累计盈利:", totalProfitRealized.toFixed(2), "U"); Log(" - 滚仓次数:", rollCount, "次"); // ========== 累加本轮盈利 ========== currentRoundTotalProfit += profit; Log(" - 本轮累计盈利:", currentRoundTotalProfit.toFixed(2), "U"); // 重置止盈单ID takeProfitOrderId = null; // 获取最新K线计算EMA Sleep(1000); var records = _C(exchange.GetRecords, PERIOD_M1); var emaFast = TA.EMA(records, FastEMA); var emaSlow = TA.EMA(records, SlowEMA); if (emaFast.length < 2 || emaSlow.length < 2) { Log("⚠️ EMA数据不足,无法判断是否滚仓"); // 记录本轮滚仓结束(正常结束,之前有盈利) saveRollRecord(false); resetPositionState(); Log("⏳ 等待新信号..."); Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); return; } var ema5_current = emaFast[emaFast.length - 1]; var ema10_current = emaSlow[emaSlow.length - 1]; Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Log("📈 EMA滚仓判断:"); Log(" - EMA5:", ema5_current.toFixed(pricePrecision)); Log(" - EMA10:", ema10_current.toFixed(pricePrecision)); Log(" - 原持仓方向:", currentDirection); var shouldRoll = false; if (currentDirection == "LONG") { // 多头止盈后,如果EMA5仍在EMA10上方,继续做多(滚仓) if (ema5_current > ema10_current) { shouldRoll = true; Log(" - 判断结果: ✅ EMA5 > EMA10,趋势延续"); Log(" - 决策: 🔄 继续做多(滚仓)"); } else { Log(" - 判断结果: ❌ EMA5 <= EMA10,趋势转弱"); Log(" - 决策: ⏸️ 不滚仓,等待新信号"); } } else if (currentDirection == "SHORT") { // 空头止盈后,如果EMA5仍在EMA10下方,继续做空(滚仓) if (ema5_current < ema10_current) { shouldRoll = true; Log(" - 判断结果: ✅ EMA5 < EMA10,趋势延续"); Log(" - 决策: 🔄 继续做空(滚仓)"); } else { Log(" - 判断结果: ❌ EMA5 >= EMA10,趋势转弱"); Log(" - 决策: ⏸️ 不滚仓,等待新信号"); } } Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); if (shouldRoll) { // ========== 滚仓:增加本轮滚仓次数 ========== currentRoundRolls++; Log("🔄 执行滚仓操作... (本轮第", currentRoundRolls, "次滚仓)"); Sleep(1000); var ticker = _C(exchange.GetTicker); var newPrice = ticker.Last; if (openPosition(currentDirection, newPrice)) { Log("✅ 滚仓成功!"); } else { Log("❌ 滚仓失败,等待新信号"); // 记录本轮滚仓结束(滚仓失败,但之前有盈利) saveRollRecord(false); resetPositionState(); } } else { // ========== 不滚仓:记录本轮滚仓结束 ========== saveRollRecord(false); resetPositionState(); Log("⏳ 已平仓,等待新信号..."); } Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); } } // ========== 保存滚仓记录 ========== function saveRollRecord(isStopLoss) { // 必须有开始时间才记录(防止异常情况) if (currentRoundStartTime == 0) { Log("⚠️ 本轮未正确初始化,跳过记录"); currentRoundRolls = 0; currentRoundTotalProfit = 0; currentRoundLoss = 0; currentRoundDirection = ""; currentRoundEntryPrice = 0; return; } var endTime = Date.now(); var duration = endTime - currentRoundStartTime; // 计算总体盈利 = 累计盈利 - 亏损 var netProfit = currentRoundTotalProfit - currentRoundLoss; var record = { direction: currentRoundDirection, // 本轮方向 roundRolls: currentRoundRolls, // 本轮滚仓次数 totalProfit: currentRoundTotalProfit, // 累计盈利(止盈累加) loss: currentRoundLoss, // 亏损金额(止损) netProfit: netProfit, // 总体盈利 duration: duration, // 持续时间(毫秒) isStopLoss: isStopLoss, // 是否止损结束 startTime: currentRoundStartTime, // 开始时间 endTime: endTime, // 结束时间 entryPrice: currentRoundEntryPrice // 入场价格 }; rollHistory.push(record); // 只保留最近N条记录 if (rollHistory.length > maxHistoryRecords) { rollHistory.shift(); } Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Log("📝 保存滚仓记录:"); Log(" - 方向:", currentRoundDirection == "LONG" ? "🟢 多头" : "🔴 空头"); Log(" - 入场价格:", currentRoundEntryPrice.toFixed(pricePrecision)); Log(" - 开始时间:", _D(currentRoundStartTime)); Log(" - 结束时间:", _D(endTime)); Log(" - 持续时间:", formatDuration(duration)); Log(" - 本轮滚仓次数:", currentRoundRolls); Log(" - 累计盈利:", currentRoundTotalProfit.toFixed(2), "U"); Log(" - 亏损金额:", currentRoundLoss.toFixed(2), "U"); Log(" - 总体盈利:", netProfit.toFixed(2), "U"); Log(" - 结束方式:", isStopLoss ? "❌ 止损" : "✅ 正常"); Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); // 重置本轮统计数据 currentRoundRolls = 0; currentRoundStartTime = 0; currentRoundDirection = ""; currentRoundTotalProfit = 0; currentRoundLoss = 0; currentRoundEntryPrice = 0; } // 格式化时长 function formatDuration(ms) { var seconds = Math.floor(ms / 1000); var minutes = Math.floor(seconds / 60); var hours = Math.floor(minutes / 60); var days = Math.floor(hours / 24); if (days > 0) { return days + "天" + (hours % 24) + "时" + (minutes % 60) + "分"; } else if (hours > 0) { return hours + "时" + (minutes % 60) + "分"; } else if (minutes > 0) { return minutes + "分" + (seconds % 60) + "秒"; } else { return seconds + "秒"; } } // 重置持仓状态函数 function resetPositionState() { entryPrice = 0; lastRollPrice = 0; currentDirection = ""; // 注意:不重置 rollCount、strategyCapital 和 currentRoundRolls } // 检查止损 function checkStopLoss(currentPrice, position) { var totalDrawdown = 0; if (currentDirection == "LONG") { totalDrawdown = (currentPrice - entryPrice) / entryPrice; } else { totalDrawdown = (entryPrice - currentPrice) / entryPrice; } if (totalDrawdown < -StopLossPercent) { Log("❌ 触发止损!回撤:", (totalDrawdown * 100).toFixed(2), "%"); // 取消止盈单 if (takeProfitOrderId) { Log("取消止盈单:", takeProfitOrderId); exchange.CancelOrder(takeProfitOrderId); takeProfitOrderId = null; Sleep(500); } // ========== 市价平仓(循环重试直到成功) ========== var profit = closePositionMarketWithRetry(currentPrice, position); // 更新策略资金池 strategyCapital += profit; totalProfitRealized += profit; Log("止损亏损:", profit.toFixed(2), "U"); Log("策略剩余资金:", strategyCapital.toFixed(2), "U"); Log("累计盈利:", totalProfitRealized.toFixed(2), "U"); // ========== 记录本轮止损亏损 ========== currentRoundLoss = Math.abs(profit); // 转为正数保存 Log("本轮止损亏损:", currentRoundLoss.toFixed(2), "U"); // ========== 记录本轮滚仓结束(被止损中断) ========== saveRollRecord(true); // 重置状态 resetPositionState(); if (strategyCapital < 10) { Log("💥 策略资金不足10U,停止运行"); throw "资金不足"; } Log("⏳ 已止损,等待新信号..."); } } // ========== 市价平仓(带重试机制,直到成功) ========== function closePositionMarketWithRetry(currentPrice, position) { Log("🔴 市价平仓(循环重试模式)"); var maxRetries = 10; // 最多重试10次 var retryCount = 0; while (retryCount < maxRetries) { retryCount++; Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Log("🔄 第", retryCount, "次平仓尝试"); var profit = closePositionMarket(currentPrice, position); // 如果返回值不为0,说明平仓成功 if (profit !== 0) { Log("✅ 平仓成功!"); Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); return profit; } // 平仓失败,检查持仓是否还存在 Sleep(2000); var newPosition = _C(exchange.GetPosition); if (newPosition.length == 0) { Log("⚠️ 持仓已不存在,可能已被其他途径平仓"); Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); return 0; } // 更新position和currentPrice position = newPosition[0]; var ticker = _C(exchange.GetTicker); currentPrice = ticker.Last; Log("⚠️ 平仓失败,", (maxRetries - retryCount), "次重试机会剩余"); Log("等待3秒后重试..."); Sleep(3000); } // 所有重试都失败 Log("❌ 平仓失败!已达到最大重试次数"); Log("⚠️ 请手动检查持仓状态!"); Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); return 0; } // 市价平仓(持续检测订单状态) function closePositionMarket(currentPrice, position) { Log("📤 发起市价平仓订单"); var pos = position; var amount = pos.Amount; if (pos.Type == PD_LONG) { exchange.SetDirection("closebuy"); } else { exchange.SetDirection("closesell"); } // 市价平仓 var orderType = pos.Type == PD_LONG ? "closebuy" : "closesell"; var orderId = exchange.CreateOrder(Symbol, orderType, -1, amount); if (!orderId) { Log("❌ 平仓下单失败"); return 0; } Log("平仓订单ID:", orderId, "开始持续检测..."); // 持续检测订单状态 var maxWaitTime = 30000; // 单次等待最多30秒 var startTime = Date.now(); var checkCount = 0; while (Date.now() - startTime < maxWaitTime) { Sleep(500); checkCount++; var order = exchange.GetOrder(orderId); if (!order) { Log("❌ 无法获取订单信息(检测", checkCount, "次)"); continue; } if (order.Status == 1) { // 平仓成功 Log("✅ 订单成交,成交价:", order.AvgPrice.toFixed(pricePrecision)); var profit = calculateProfit(pos, order.AvgPrice); Log("盈亏:", profit.toFixed(2), "U"); return profit; } else if (order.Status == 2) { Log("❌ 平仓订单已被取消"); return 0; } // Status == 0 表示未成交,继续等待 if (checkCount % 10 == 0) { Log("⏳ 订单未成交,已检测", checkCount, "次..."); } } // 超时,取消订单 Log("⚠️ 平仓订单超时(等待30秒未成交)"); Log("尝试取消订单:", orderId); var cancelResult = exchange.CancelOrder(orderId); if (cancelResult) { Log("✅ 订单已取消"); } else { Log("⚠️ 取消订单失败,订单可能已成交或已取消"); } Sleep(1000); // 再次检查订单状态(可能取消期间成交了) var finalOrder = exchange.GetOrder(orderId); if (finalOrder && finalOrder.Status == 1) { Log("✅ 订单在取消期间成交,成交价:", finalOrder.AvgPrice.toFixed(pricePrecision)); var profit = calculateProfit(pos, finalOrder.AvgPrice); Log("盈亏:", profit.toFixed(2), "U"); return profit; } return 0; } // 计算盈亏 function calculateProfit(position, closePrice) { var profit = 0; if (position.Type == PD_LONG) { // 多头盈亏 = (平仓价 - 开仓价) * 数量 * 合约面值 profit = (closePrice - position.Price) * position.Amount * ctVal; } else { // 空头盈亏 = (开仓价 - 平仓价) * 数量 * 合约面值 profit = (position.Price - closePrice) * position.Amount * ctVal; } return profit; } // ========== 显示状态(三个两行表格) ========== function showStatus(account, position, price, ema5, ema10, isBullTrend, barTime) { // ========== 表格1:基本信息 ========== var table1 = { type: "table", title: "策略基本信息", cols: ["更新时间", "交易币种", "账户总资金", "策略初始资金", "策略当前资金", "策略累计盈利", "策略收益率"], rows: [] }; table1.rows.push([ _D(barTime), Symbol, account.Balance.toFixed(2) + " U", InitialCapital + " U", strategyCapital.toFixed(2) + " U", totalProfitRealized.toFixed(2) + " U", ((strategyCapital / InitialCapital - 1) * 100).toFixed(2) + "%" ]); // ========== 表格2:持仓信息 ========== var table2 = { type: "table", title: "持仓信息", cols: [], rows: [] }; if (position.length > 0) { var pos = position[0]; var totalPL = 0; if (pos.Type == PD_LONG) { totalPL = (price - entryPrice) / entryPrice; } else { totalPL = (entryPrice - price) / entryPrice; } var stopLossPrice = 0; var takeProfitPrice = 0; if (pos.Type == PD_LONG) { stopLossPrice = entryPrice * (1 - StopLossPercent); takeProfitPrice = entryPrice * 1.1; } else { stopLossPrice = entryPrice * (1 + StopLossPercent); takeProfitPrice = entryPrice * 0.9; } table2.cols = [ "持仓状态", "持仓方向", "持仓数量", "入场价格", "持仓均价", "止盈价格", "止损价格", "浮动盈亏", "盈亏比例", "止盈单ID", "本轮滚仓次数" ]; table2.rows.push([ "✅ 持仓中", pos.Type == PD_LONG ? "🟢 多头" : "🔴 空头", pos.Amount.toFixed(amountPrecision), entryPrice.toFixed(pricePrecision), pos.Price.toFixed(pricePrecision), takeProfitPrice.toFixed(pricePrecision), stopLossPrice.toFixed(pricePrecision), pos.Profit.toFixed(2) + " U", (totalPL * 100).toFixed(2) + "%", takeProfitOrderId ? takeProfitOrderId : "未挂单", currentRoundRolls + " 次" ]); } else { table2.cols = ["持仓状态", "等待信号", "本轮滚仓次数"]; table2.rows.push([ "⏳ 空仓", isBullTrend ? "📈 等待金叉" : "📉 等待死叉", currentRoundRolls + " 次" ]); } // ========== 表格3:滚仓统计(近10次详细记录) ========== var table3 = { type: "table", title: "滚仓统计(近10次)", cols: ["序号", "方向", "入场价格", "开始时间", "结束时间", "滚仓次数", "盈利金额", "亏损金额", "总体盈利", "持续时间"], rows: [] }; if (rollHistory.length == 0) { table3.rows.push(["暂无", "暂无", "暂无", "暂无", "暂无", "暂无", "暂无", "暂无", "暂无", "暂无"]); } else { // 倒序显示(最新的在前) for (var i = rollHistory.length - 1; i >= 0; i--) { var record = rollHistory[i]; var index = rollHistory.length - i; var directionText = record.direction == "LONG" ? "🟢 多头" : "🔴 空头"; // 总体盈利的显示(带颜色标识) var netProfitText = record.netProfit.toFixed(2) + " U"; if (record.netProfit > 0) { netProfitText = "+" + netProfitText; } // 格式化时间显示(只显示月-日 时:分) var startTimeStr = _D(record.startTime).substring(5, 16); // "MM-DD HH:mm" var endTimeStr = _D(record.endTime).substring(5, 16); table3.rows.push([ "#" + index, directionText, record.entryPrice ? record.entryPrice.toFixed(pricePrecision) : "未知", startTimeStr, endTimeStr, record.roundRolls + " 次", record.totalProfit.toFixed(2) + " U", record.loss.toFixed(2) + " U", netProfitText, formatDuration(record.duration) ]); } } // ========== 组合所有表格 ========== LogStatus( "`" + JSON.stringify(table1) + "`\n\n" + "`" + JSON.stringify(table2) + "`\n\n" + "`" + JSON.stringify(table3) + "`" ); }
Comment
All comments (0)
No data
No data
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)