[TOC]

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

滚仓策略的盈利逻辑本质上是一个复利增长模型。让我们用一个简化的例子来理解:
同样市场连续3次各涨10%的情况下: * 单次交易:盈利 99.3 USDT * 滚仓交易:盈利 119.7 USDT * 复利优势:20.4 USDT(提升约20.5%)
同样是连续3次各涨10%,单次交易盈利99.3 USDT,滚仓交易盈利119.7 USDT。这个差额就是复利的力量。
用数学公式表达:
// 传统交易:线性增长
最终资金 = 初始资金 × (1 + 杠杆 × 涨幅)
// 滚仓交易:指数增长
最终资金 = 初始资金 × (1 + 杠杆 × 单次涨幅) ^ 滚仓次数
这揭示了滚仓的本质:将线性增长转化为指数增长。但同时也暴露了风险:一次止损可能回吐之前所有复利成果。
在动手编写任何代码之前,我们需要从策略层面回答三个根本问题:
问题1:什么时候开始?(初次入场)
需要判断趋势的开始信号。
问题2:什么时候继续?(追加滚仓)
这是滚仓的核心:止盈后如何判断趋势是否延续?
问题3:什么时候停止?(退出观望)
- 主动退出:趋势转弱
- 被动退出:触发止损
这三个问题决定了整个策略的骨架,下面我们将逐一把它们转化为代码逻辑。

让我们先理解滚仓策略最理想的应用场景。
理想场景:
想象一下,如果你能在SHIB从0.000001美元起涨时入场,或者在某个山寨币暴涨前夕建仓,通过连续滚仓,100 USDT可能变成10000 USDT甚至更多。这就是滚仓策略的终极梦想——在币种起爆前入场,抓住十倍甚至百倍的收益。
残酷现实:
但问题在于,如何知道哪个币种会暴涨?什么时候会暴涨?
- 如果你是项目方或内幕人士,可能提前知道利好消息
- 如果你是普通交易者,只能通过市场信号去判断
对于我们大多数人来说,想要精准捕捉这种起爆点,说白了就是撞大运。我们无法预知未来,只能通过历史数据和技术指标,尽可能提高”撞到大运”的概率。
既然无法预知哪个币种会暴涨,我们能做的就是:建立一套可执行的入场规则,用技术指标模拟”趋势开始”的信号。
这就像在茫茫大海中捕鱼,虽然不知道哪里有大鱼,但我们可以: - 观察水面波纹(价格波动) - 分析水流方向(趋势方向) - 选择合适的工具(技术指标)
当多个信号汇聚时,我们认为”可能”有趋势要开始了,于是入场尝试。对了,就跟着趋势滚仓赚钱;错了,及时止损退出。
选择技术工具:
我们使用EMA双均线系统(EMA5和EMA10)作为趋势判断工具。选择它的原因很简单:
- 简单直观,易于验证
- 能快速反应价格变化
- 参数平衡了灵敏度和稳定性
核心逻辑:
通过检测均线的”金叉”(EMA5上穿EMA10)和”死叉”(EMA5下穿EMA10)来捕捉趋势转折点:
- 金叉 → 做多信号
- 死叉 → 做空信号
代码思路:
// 计算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);
}
这里不深入展开金叉死叉的细节,这些都是交易中的基础概念。重点是:我们需要一个明确的、可量化的入场信号来触发滚仓的起点。

滚仓策略本质上是一个理智的冒险者游戏。让我们用一个完整的场景来理解:
游戏规则:
1. 你从交易所账户中拿出100 USDT作为冒险资金
2. 这100 USDT独立管理,与账户其他资金隔离
3. 用这100 USDT开始交易:
- 赚了 → 盈利加入资金池,继续用更大的资金交易(滚仓)
- 亏了 → 触发止损,回到空仓状态
4. 重复这个过程,直到:
- 要么把100 USDT亏完(游戏结束)
- 要么滚到一个满意的金额(主动退出)
这个游戏的精妙之处在于: - 风险可控:最多亏100 USDT,不会影响账户其他资金 - 收益无限:如果趋势配合,复利可以让资金快速翻倍 - 进退有据:有明确的止盈、止损、滚仓规则
这是滚仓策略中最核心的设计思想。
传统做法的问题:
假设你的交易所账户有1000 USDT:
- 第1次开仓用100 USDT
- 盈利30 USDT后,账户变成1030 USDT
- 第2次开仓该用多少?100还是130?
- 如何区分这是滚仓策略赚的还是其他操作的收益?
资金池解决方案:
// 创建一个虚拟的"策略资金池"
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滚到了多少
这是滚仓策略的灵魂环节:当止盈单成交后,我们要做一个关键决策——继续滚还是停手?
决策场景:
假设我们做多BTC:
- 入场价:45000 USDT,用100 USDT开仓
- 止盈价:49500 USDT(涨10%)
- 止盈成交,盈利30 USDT
- 现在资金池:130 USDT
问题来了:
选项A:收手,带着130 USDT退出,回到空仓
选项B:继续,用130 USDT再次开多(滚仓)
如何选择?
这个决策不能靠”感觉”,必须有明确标准。我们的判断逻辑是:趋势是否还在延续?
判断方法:
在止盈单成交的瞬间,重新计算最新的技术指标(EMA均线):
// 止盈单成交后,获取最新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("⏸️ 决策:不滚仓,等待新信号");
}
}
如果决策是”继续滚仓”:
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();
}
}
如果决策是”停止”:
else {
// 1. 保存本轮统计
saveRollRecord(false); // false表示正常结束,非止损
// 2. 保留资金池金额
// strategyCapital 保持当前值,等待下次机会
// 3. 回到空仓状态
resetPositionState();
Log("⏳ 已平仓,等待新信号...");
}
这个流程的关键点: - 每次止盈后立即判断,不拖延 - 判断标准客观(均线关系),不主观臆测 - 继续就加大仓位,停止就保留成果
让我们通过一个完整案例感受复利的威力:
成功案例:
初始资金: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
这就是滚仓的双刃剑: - 成功时: 指数级增长,令人振奋 - 失败时: 快速回吐,甚至亏损

主动退出:趋势转弱
这种情况在”问题二”中已经处理了——止盈后判断趋势不支持继续,主动选择停止。这是理想的退出方式,带着盈利离场。
被动退出:触发止损
这是我们现在要重点讨论的——当行情走反,价格触及止损线,被迫平仓。
很多人不喜欢止损,因为: - 止损意味着认错 - 止损会产生实际亏损 - 有时候止损后价格又涨回来了
但在滚仓策略中,止损是保命的底线。想想看:
如果没有止损:
第1次:100 → 滚到 169
第2次:169 → 趋势反转,不止损
价格持续下跌:169 → 150 → 120 → 80 → 50...
最终可能全亏,甚至爆仓
如果有止损:
第1次:100 → 滚到 169
第2次:169 → 趋势反转,触发止损
止损5%:亏损 25.35
剩余:143.65
虽然亏了,但保留了大部分资金
可以等待下一个机会
止损的本质: 用小的确定性亏损,避免大的不确定性风险。
// 检查止损
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("⏳ 已止损,等待新信号...");
}
}
还记得我们说的”理智的冒险者游戏”吗?这个游戏有明确的结束条件:
条件1:资金池归零
if (strategyCapital <= 0) {
Log("💥 游戏结束:资金池已归零");
Log("本次冒险失败,100 USDT全部亏光");
throw "资金耗尽";
}
条件2:主动退出
if (strategyCapital >= 目标金额) {
Log("🎉 达到目标金额,可以选择主动退出");
Log("锁定利润,开始新一轮100 USDT的游戏");
}
条件3:达到最大滚仓次数
if (连续滚仓次数 >= 10次) {
Log("⚠️ 达到最大滚仓次数,主动退出");
Log("持续时间太长,风险累积,见好就收");
saveRollRecord(false);
resetPositionState();
}
整个滚仓策略的设计,核心就是在风险与收益之间找平衡:
收益端: - 复利增长:每次止盈后资金池变大 - 趋势捕捉:在上涨/下跌趋势中持续获利 - 无上限:理论上可以无限滚下去
风险端: - 止损保护:单次最多亏损资金池的5% - 资金隔离:最多亏100 USDT - 趋势判断:避免在震荡市中频繁止损
TRUMP_USDT 币安期货上市首日(2025年1月20日至2025年1月21日)回测参考分析:


从回测结果可以看到:
亮点表现: - 策略成功捕捉到了TRUMP上市初期的剧烈波动 - 通过多次滚仓,实现了资金的快速增长 - 止盈机制有效锁定了趋势中的利润
风险暴露: - 在趋势反转时,止损导致部分利润回吐 - 震荡行情中出现了假突破信号 - 单一币种的集中风险较高
关键数据: - 总滚仓次数:X次 - 最大单轮滚仓:X次 - 最大回撤:X% - 最终收益率:X%
通过上述分析,我们可以清楚地看到,这个策略本质上是在模拟:
一个理智的冒险者的交易行为: - 有明确的入场规则(不是冲动交易) - 有止盈目标(不贪婪) - 有止损纪律(不死扛) - 有滚仓判断(会利用盈利) - 有资金限制(控制风险)
它的核心逻辑是: 1. 拿出固定资金(100 USDT)进行尝试 2. 遇到趋势,跟随趋势赚钱 3. 赚到钱后,用盈利继续交易(利滚利) 4. 如果趋势转弱,及时停止 5. 如果判断错误,快速止损 6. 直到资金亏完或滚到满意金额
局限1:依赖趋势市场
这个策略在震荡市中表现很差,因为:
- 频繁出现假突破
- 止盈后又回调,无法滚仓
- 反复止损,消耗资金池
局限2:参数敏感
止盈10%、止损5%这些参数并非最优:
- 不同币种波动率不同
- 不同市况需要不同参数
- 固定参数难以适应所有情况
局限3:无法预测起爆点
正如前面所说,我们用技术指标入场,本质上是”撞大运”:
- 可能错过真正的大行情
- 可能在假突破时入场
- 无法像内幕人士那样提前布局
方向1:结合工作流筛选币种
- 不是随便找个币种就滚
- 而是先用工作流筛选出热门、爆发力强的币种
- 比如:社交媒体讨论度激增、交易量异常、链上数据活跃等
- 在这些币种上使用滚仓策略,成功率会更高
方向2:动态调整参数
- 根据币种的历史波动率调整止盈止损比例
- 波动大的币种,适当放大止损空间
- 波动小的币种,可以降低止盈目标
方向3:多资金池并行
- 不是把100 USDT放在一个币种
- 而是分成5个20 USDT,在5个潜力币种上同时滚
- 分散风险,提高”撞到大运”的概率
通过三个核心问题的推演,我们完整地展示了如何将滚仓这个交易想法转化为代码逻辑。这个过程的本质是:把一个理智的冒险者的交易思维,用精确的规则和数据结构表达出来。
重要提示:
这只是一个滚仓思路的模拟实现,实际上滚仓是一个非常需要市场经验的交易策略。本策略只是一个工具,后期可以结合工作流判断热门或者极具爆发力的币种,使用该工具,会为我们带来更多惊喜。
记住: - 最多亏100 USDT,风险可控 - 如果运气好遇到大趋势,可能翻几倍甚至几十倍 - 但更多时候,可能是小赚小亏,反复试探 - 这是一场需要耐心和纪律的游戏
没有稳赚不赔的策略,滚仓只是一个工具。真正决定成败的,是你能否: - 找到有潜力的币种(结合工作流筛选) - 坚持执行止损(不要死扛) - 在大趋势来临时敢于滚仓(不要过早退出) - 保持理智(不被情绪左右)
祝各位在量化交易的道路上,找到属于自己的”大运”!
完整策略地址:**策略源码 -> ** https://www.fmz.com/strategy/521864
完整策略代码:
/*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) + "`"
);
}