
先说结果:
第一轮报告交上来时,看着还真像那么回事。交易周期、布林带参数、分层下单的权重,AI 都列出来了,后面还附着一份完整代码。光看这份答卷,很容易觉得:这套策略已经被它摸透了吧?
我接着把原策略的链接交给它,让它去对照源码,才发现事情没这么简单:一些参数对上了,交易规则却不完全一样。
这让我想弄清楚:它到底从记录里认出了什么,又在哪些地方补上了自己的猜测?把分析过程和最后的源码对照放在一起,差别才慢慢显出来。
从 REA 的热度,想到一道策略推理题
最近 REA 挺受关注。它为智能体提供逆向工程工具,可以用来分析程序、应用以及它们运行时的行为。我看到后冒出一个想法:量化策略每天也会留下不少记录,能不能先看它怎么买卖,再猜出背后的程序?REA 项目介绍
于是,我挑了一个公开的机器人,让编程 Agent 先看运行记录,再交一份它认为合理的策略代码。等它交卷,我才给出原策略链接。这个实验是受 REA 启发,实际分析和写代码的工作交给了编程 Agent,REA 本身没有参与。
这次用的是 Codex,截图里选的模型是 GPT-6 Astra Ultra。下面就按分析、交卷、对照源码的顺序,把过程展开。
题目公开,参考答案也公开
这次选的是机器人 674839,页面上的名字叫 D-Man V3 FMZ Rust。用来对照的策略 546875也有公开页面,标注的版本是 v1.1.1,登录后可以查看完整源码。选它,就是因为最后有答案可查:AI 说得像不像,能拿到代码里逐项核对。

这是当时保存的页面截图,数值以截图时点为准。
先交代一个细节:分析记录提到,交易所作用域里带有 simulate(模拟)标记。虽然平台把这个页面叫作“实盘机器人”,光凭名字,还不能判断它跑的是不是真实资金。所以这里研究的是它留下的运行记录,页面上的收益不作为已验证的真实资金业绩。
不过,运行记录也不是现成的策略说明书。比如看到三笔买入,就能说策略加了三次仓吗?也可能只是一张订单分三次成交。我想看的是,AI 能不能把这些零散记录串起来,解释背后的下单规则。
第一轮,我给了什么任务?
我的要求很简单:给它机器人链接,只看页面里的运行数据,不访问原源码和策略介绍。分析完以后,别只写一段“这大概是什么策略”的解释,还要交出一份尽可能完整的候选代码。

当时的提问和模型选择,见上图。

它拿到的线索,比几个买卖点丰富
它先报上来一组数字:9,894 条公开日志和 54 个收益点。这里沿用分析过程中的“收益点”叫法;完成报告把它写成了“54 个周期”,口径不一致,不能直接当成成交笔数或完整交易周期数。

往下看才发现,这些日志里已经写着不少答案。按报告的说法,30 分钟周期、用 100 根 K 线计算的布林带、2 倍标准差、四层分批下单,以及 1∶1∶1.5∶2.5 的资金权重,都能从日志中直接找到。
所以,看到它列出一串准确参数,先别急着给满分。读出日志里写明的参数,是把现成信息整理出来;拿订单数量反推资金分配公式,才是在猜背后的规则。 这两件事都有用,只是不能一股脑算成“只看买卖动作就猜中了”。
报告还称,它用一套公式解释了 216 档委托数量,并推测了退出倍数、移动止盈和约两小时的冷却时间。这些属于 Agent 给出的推断。先把它们记下来,等揭晓源码时,再看能对上多少。
交卷:它写出了一套什么策略?
它把策略概括为:“布林带反转入场、四层分批下单、按波动尺度退出。”简单说,就是价格来到布林带边缘时寻找反向入场机会,分四档挂单,再根据波动大小设置退出条件。接着,它照这个思路写了一份 JavaScript 候选代码。

翻一翻代码注释,还能看到它没弄清的地方:入场究竟怎么触发,锚点怎么定,波动尺度怎么算,冷却时间的边界在哪里。它没有把这些都说成确定的答案。比如挂单期限没识别出来,代码就先把这一项默认关掉了。
完整代码放在文末附录,方便逐行查看。这份代码默认使用 PAPER 模式,也就是先走模拟流程;默认模拟运行,并不表示它已经通过了回测或复刻验证。
写成代码以后,有些含糊的地方就藏不住了。说一句“跌了买、涨了卖”,听着谁都懂,可真要写程序,就躲不开:跌多少才买?买多少?已经有仓位怎么办?订单挂着没成交又怎么办?这些问题都有了具体写法,接下来才能和原版一条条比较。
源码揭晓:看着相像的两句话,差在哪里?
接下来就看答案。我给出原策略链接,让 Agent 把刚交上来的代码和原版逐项比较。按对照记录的说法,第一轮代码保留了原样,没有看过源码再回头补改。

对照报告里,四层权重、若干退出倍数、约两小时冷却等项目都能对上。到了入场规则,差别出来了:报告称原策略要求“首次越轨”,候选代码判断的却是“当前在轨外”。 再看候选代码,它检查的是最新一根已收盘 K 线的收盘价是否触及或越过布林带上下轨,并没有同时检查前一根是否在轨内。
这两种写法听着差不多,想象一小段行情就好理解了。为了方便比较,先把布林带下轨当成一条固定横线:价格原来在线上方,后来跌到线下,又连续两根 K 线留在线下。
- “首次越轨”要找的是跨过去的那一下,所以得比较前后状态。
- “当前在轨外”只看现在的位置,后面两根 K 线仍可能满足条件。
这里先看的是信号条件。条件满足以后,程序还要检查持仓、冷却时间和预算,才决定是否下单,所以不能把每次信号出现都当成一笔真实成交。不过,光看入场判断,已经能看出这两种写法不是同一条规则。
还有一些差别更不显眼。报告指出,原版有权益预算上限、入场挂单过期和最长持仓限制,首轮代码没有把这些设计等价还原出来。想想也好理解:如果一段行情里,预算一直没碰到上限,挂单很快成交,持仓也很快结束,这些限制就很难从交易动作中露出来。
这样一来,同一段运行记录,就可能让几套不同的规则都说得通。它们碰巧在这段行情里做了相似的事,到了某个条件改变的时候,才会作出不同选择。
这次能下什么结论?
让 Agent 对照完源码,结果多少有点让人失望。
如果你打算把一个看中的机器人链接丢给 AI,就拿到一份百分之百复刻的策略,这次并没有做到。它认出了大致思路,也找对了不少参数,可几处影响交易的关键细节,还是没还原出来。在我看来,它这次做得更像是参考、仿照,再补上自己的理解。写出来的,基本就是一套新策略了。

按这份对照报告的结论,策略框架和多个参数认对了,但首轮代码还没有复刻出行为相同的原策略。
这里还有一个容易混在一起的问题:有些差异,可能是 AI 漏看了已有线索;有些规则,在这段记录里却未必留下了可辨认的痕迹。两者都表现为“没还原出来”,改进的办法并不相同。仅仅换一种问法,并不能保证补上缺失的证据。
本文展示的是 Agent 的源码对照结果,还不足以给出“还原了百分之多少”的数字;要量化行为上的差距,还需要逐笔回放和独立行情验证。这次的结果说明这份候选代码没有复刻成功,不能据此替所有模型下结论。
让 AI 学习交易模式,究竟要学什么?
源码揭晓以后,那些没对上的地方,反而让“学会一个策略”这件事变得具体了。认出布林带和分批下单只是开头,还要弄明白策略在什么情况下会改变决定。 这次入场条件和执行限制上的差异,正好把这个问题摆了出来。
同样跌到下轨外,为什么这次出现了成交,另一次却没有?当时已经有仓位、还在冷却,或者订单挂出后没有成交,都可能是解释。把这些情况连同当时的行情、仓位和订单状态交给 AI,才有机会分清究竟是哪种原因。
给它什么样的例子,也就有了讲究。一长串正常开平仓的记录,未必能暴露多少隐藏条件。那些挂单迟迟没成交、持仓拖得很久、预算接近上限的片段,倒值得仔细看。让 AI 围绕这些地方列出可能的解释,指出各自需要什么证据,分析就有了具体落点。
对研究策略的人来说,这些分析可以帮我们梳理入场理由、仓位安排和退出条件,再逐条判断哪些思路值得借鉴。候选代码把这套理解写成了程序;到了新的行情,它能不能作出接近原策略的选择,还得验证。
所以,当 AI 说“我懂了这个策略”,我更想接着问:同样的行情,换一种持仓或订单状态,它会不会作出不同选择?这个判断有哪些记录支持,又会在什么情况下不成立?
附录
首轮分析生成的候选代码:
javascript
/*
* FMZ robot 674839 — behavioural reconstruction, NOT the original source.
* Evidence: public live logs only (data/all_logs.json). No strategy source or
* strategy description was consulted. JavaScript implementation for FMZ.
*
* Observed: ETH_USDT.swap, 30m BB100/2, price levels [.05,.35,.75,1.25],
* weights [1,1,1.5,2.5]. 216 published quantities fit
* floor((10000 * weight / 6) / limitPrice, .01) in FMZ order units.
* A .1 ETH contract multiplier would make that a 1000 USDT nominal budget.
* Strong inference: TP=1*v, SL=3*v, trailing activation=.8*v, fixed
* trailing drawdown=.25*v, where v=scale/anchor. Cooldown is about four bars.
* UNIDENTIFIED, therefore configurable assumptions: signal=%B outside [0,1],
* anchor=last closed close, scale=.5*(upper-lower), population BB variance,
* return reference=actual average entry, cooldown current-bar difference>4.
* No order TTL was identified: disabled by default.
*
* DEFAULT PAPER. This file never creates a robot or configures account mode.
* LIVE is opt-in and requires valid contract metadata, explicit confirmation,
* an otherwise unused account/instrument, and preconfigured hedge position mode.
* PAPER fills only against subsequent observed ticks, not historical bar paths;
* it is a transparent simulator, not a claimed reconstruction backtest.
*
* Runtime interfaces (official technical documentation):
* https://www.fmz.com/syntax-guide/fun/trade/exchange.createorder
* https://www.fmz.com/syntax-guide/fun/futures
* https://www.fmz.com/syntax-guide/struct/market
* https://www.fmz.com/syntax-guide/struct/account
* https://www.fmz.com/syntax-guide/fun/storage
*/
var RECON_DEFAULTS = {
RunMode: "PAPER",
Symbol: "ETH_USDT.swap",
PeriodSeconds: 1800,
BBPeriod: 100,
BBDeviations: 2,
BBWidthMultiplier: 0.5, // UNKNOWN original definition: modelling assumption.
SignalMode: "OUTSIDE_BAND", // UNKNOWN original trigger.
Levels: [0.05, 0.35, 0.75, 1.25],
Weights: [1, 1, 1.5, 2.5],
NominalBudgetUSDT: 1000,
TakeProfitScale: 1,
StopLossScale: 3,
TrailingActivateScale: 0.8,
TrailingDrawdownScale: 0.25,
CooldownBars: 4, // Require currentBar - exitCurrentBar > CooldownBars.
OrderTTLSeconds: 0, // No evidence for expiry; off by default.
LoopMilliseconds: 2000,
MaxTickerAgeMilliseconds: 30000,
MaxClosedBarAgePeriods: 2,
FlatConfirmations: 3,
FlatConfirmationIntervalMilliseconds: 1500,
StateKeyPrefix: "fmz674839_reconstruction_v1",
AccountScope: "PAPER_DEMO", // Unique account/subaccount LABEL; never put credentials here.
LiveConfirmation: "", // Set to "I_ACCEPT_INFERRED_RULES" explicitly.
ExpectedExchangeName: "Futures_OKCoin",
ExpectedBasePerOrderUnit: 0.1, // Evidence-based candidate; LIVE validates metadata.
AllowExplicitContractMetadata: false,
ExplicitContractBasePerUnit: null,
RequireValidEquityForEntry: true,
MaxGrossNotionalMultiplier: 1.25,
MinimumMarginReserveUSDT: 0,
ReturnToMiddleBand: true, // Inferred: positive return AND mark reaches closed-bar BB middle.
PaperInitialEquity: 1000,
PaperBasePerOrderUnit: 0.1,
PaperTickSize: 0.01,
PaperAmountStep: 0.01,
PaperMinQty: 0.01,
PaperMinNotional: 0,
PaperTakerFeeRate: 0.0005,
PaperMakerFeeRate: 0.0002,
PaperMarketSlippageBps: 2,
PaperFillFractionPerTick: 1
};
function reconClone(x) { return JSON.parse(JSON.stringify(x)); }
function reconNumber(x) { return typeof x === "number" && isFinite(x); }
function reconAssert(ok, msg) { if (!ok) { throw new Error(msg); } }
function reconStable(x) {
if (Array.isArray(x)) { return "[" + x.map(reconStable).join(",") + "]"; }
if (x && typeof x === "object") {
return "{" + Object.keys(x).sort().map(function (k) {
return JSON.stringify(k) + ":" + reconStable(x[k]);
}).join(",") + "}";
}
return JSON.stringify(x);
}
function reconFingerprint(config) {
var s = reconStable(config), h = 2166136261;
for (var i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619) >>> 0;
}
return "v1-" + ("00000000" + h.toString(16)).slice(-8);
}
function reconConfig(input) {
var c = reconClone(RECON_DEFAULTS), k;
input = input || {};
for (k in input) {
reconAssert(Object.prototype.hasOwnProperty.call(c, k), "Unknown configuration: " + k);
c[k] = reconClone(input[k]);
}
reconAssert(c.RunMode === "PAPER" || c.RunMode === "LIVE", "RunMode must be PAPER or LIVE");
reconAssert(typeof c.AccountScope === "string" && /^[A-Za-z0-9_.-]{1,80}$/.test(c.AccountScope), "Invalid AccountScope label");
reconAssert(/^[A-Z0-9]+_USDT\.swap$/.test(c.Symbol), "Only USDT linear perpetual symbols supported");
["PeriodSeconds", "BBPeriod", "BBDeviations", "BBWidthMultiplier", "NominalBudgetUSDT",
"TakeProfitScale", "StopLossScale", "TrailingActivateScale", "TrailingDrawdownScale",
"LoopMilliseconds", "MaxTickerAgeMilliseconds", "FlatConfirmationIntervalMilliseconds",
"MaxGrossNotionalMultiplier", "PaperInitialEquity", "PaperBasePerOrderUnit", "PaperTickSize",
"PaperAmountStep", "PaperMinQty"].forEach(function (name) {
reconAssert(reconNumber(c[name]) && c[name] > 0, name + " must be positive");
});
["CooldownBars", "OrderTTLSeconds", "MinimumMarginReserveUSDT", "PaperMinNotional",
"PaperTakerFeeRate", "PaperMakerFeeRate", "PaperMarketSlippageBps"].forEach(function (name) {
reconAssert(reconNumber(c[name]) && c[name] >= 0, name + " must be nonnegative");
});
reconAssert(Number.isInteger(c.BBPeriod) && c.BBPeriod >= 2, "Invalid BB period");
reconAssert(Number.isInteger(c.PeriodSeconds) && c.PeriodSeconds >= 60, "Invalid candle period");
reconAssert(Number.isInteger(c.CooldownBars), "CooldownBars must be integral");
reconAssert(Number.isInteger(c.FlatConfirmations) && c.FlatConfirmations >= 3, "At least 3 flat confirmations required");
reconAssert(c.SignalMode === "OUTSIDE_BAND", "Unsupported signal model");
["AllowExplicitContractMetadata", "RequireValidEquityForEntry", "ReturnToMiddleBand"].forEach(function (name) {
reconAssert(typeof c[name] === "boolean", name + " must be a boolean");
});
reconAssert(c.Levels.length === 4 && c.Weights.length === 4, "Exactly four DCA layers required");
c.Levels.forEach(function (x, i) {
reconAssert(reconNumber(x) && x > 0 && (!i || x > c.Levels[i - 1]), "Invalid levels");
reconAssert(reconNumber(c.Weights[i]) && c.Weights[i] > 0, "Invalid weights");
});
reconAssert(c.PaperFillFractionPerTick > 0 && c.PaperFillFractionPerTick <= 1, "Invalid paper fill fraction");
return c;
}
function reconRoundStep(value, step, direction) {
reconAssert(reconNumber(value) && reconNumber(step) && step > 0, "Invalid rounding input");
var units = value / step;
var rounded = direction === "up" ? Math.ceil(units - 1e-10) : Math.floor(units + 1e-10);
return Number((rounded * step).toFixed(12));
}
function reconClosedCandles(records, now, periodSeconds) {
reconAssert(Array.isArray(records), "Candle query failed");
var result = [], seen = {}, period = periodSeconds * 1000;
records.forEach(function (r) {
reconAssert(r && reconNumber(r.Time) && reconNumber(r.Close) && r.Close > 0, "Malformed candle");
if (r.Time + period <= now) {
reconAssert(!seen[r.Time], "Duplicate candle timestamp");
seen[r.Time] = true;
result.push(r);
}
});
result.sort(function (a, b) { return a.Time - b.Time; });
return result;
}
function reconBollinger(records, period, deviations) {
reconAssert(records.length >= period, "Insufficient closed candles");
var x = records.slice(-period), mean = 0, variance = 0;
x.forEach(function (r) { mean += r.Close / period; });
x.forEach(function (r) { variance += Math.pow(r.Close - mean, 2) / period; });
var sd = Math.sqrt(variance);
return { middle: mean, upper: mean + deviations * sd, lower: mean - deviations * sd, sd: sd };
}
function reconSignal(records, now, c) {
var closed = reconClosedCandles(records, now, c.PeriodSeconds);
if (closed.length < c.BBPeriod) { return null; }
var recent = closed.slice(-c.BBPeriod), periodMs = c.PeriodSeconds * 1000;
for (var i = 1; i < recent.length; i++) {
reconAssert(recent[i].Time - recent[i - 1].Time === periodMs, "Candle gap in BB window");
}
var bar = closed[closed.length - 1];
reconAssert(now - (bar.Time + periodMs) <= c.MaxClosedBarAgePeriods * periodMs, "Closed candles are stale");
var bb = reconBollinger(closed, c.BBPeriod, c.BBDeviations), width = bb.upper - bb.lower;
if (!(width > 0)) { return { time: bar.Time, side: null, close: bar.Close, bb: bb }; }
var percentB = (bar.Close - bb.lower) / width;
return {
time: bar.Time, side: percentB <= 0 ? "LONG" : (percentB >= 1 ? "SHORT" : null),
close: bar.Close, anchor: bar.Close, scale: c.BBWidthMultiplier * width,
percentB: percentB, bb: bb
};
}
function reconMarket(raw, c) {
reconAssert(raw && typeof raw === "object", "Missing contract market metadata");
var base = c.Symbol.split("_")[0];
var multiplier = raw.CtVal, unit = raw.CtValCcy;
if (!(reconNumber(multiplier) && multiplier > 0 && unit === base)) {
reconAssert(c.AllowExplicitContractMetadata && reconNumber(c.ExplicitContractBasePerUnit) &&
c.ExplicitContractBasePerUnit > 0, "Missing/ambiguous CtVal or CtValCcy; refuses guessed LIVE quantity");
multiplier = c.ExplicitContractBasePerUnit;
}
if (c.ExpectedBasePerOrderUnit !== null) {
reconAssert(reconNumber(c.ExpectedBasePerOrderUnit) &&
Math.abs(multiplier - c.ExpectedBasePerOrderUnit) < 1e-12, "Contract multiplier does not match configured expectation");
}
["TickSize", "AmountSize", "MinQty"].forEach(function (k) {
reconAssert(reconNumber(raw[k]) && raw[k] > 0, "Missing/invalid " + k);
});
["MinNotional", "MaxQty", "MaxNotional"].forEach(function (k) {
if (raw[k] !== undefined && raw[k] !== null) {
reconAssert(reconNumber(raw[k]) && raw[k] >= 0, "Invalid " + k);
}
});
return { basePerUnit: multiplier, tick: raw.TickSize, step: raw.AmountSize, minQty: raw.MinQty,
maxQty: raw.MaxQty || Infinity, minNotional: raw.MinNotional || 0,
maxNotional: raw.MaxNotional || Infinity };
}
function reconBuildLayers(signal, market, c) {
reconAssert(signal.side === "LONG" || signal.side === "SHORT", "Invalid signal direction");
reconAssert(signal.anchor > 0 && signal.scale > 0, "Invalid signal anchor/scale");
var sum = c.Weights.reduce(function (a, b) { return a + b; }, 0);
return c.Levels.map(function (distance, i) {
var p = signal.anchor + (signal.side === "LONG" ? -1 : 1) * signal.scale * distance;
p = reconRoundStep(p, market.tick, signal.side === "LONG" ? "down" : "up");
reconAssert(p > 0, "Nonpositive DCA price");
var q = reconRoundStep(c.NominalBudgetUSDT * c.Weights[i] / sum / (p * market.basePerUnit), market.step, "down");
var nominal = q * p * market.basePerUnit;
reconAssert(q >= market.minQty && q <= market.maxQty && nominal >= market.minNotional &&
nominal <= market.maxNotional, "Layer violates exchange min/max limits");
return { index: i, price: p, quantity: q, nominal: nominal, status: "PLANNED", orderId: null };
});
}
function reconPosition(positions, symbol, longConstant, shortConstant) {
reconAssert(Array.isArray(positions), "Position query failed");
var result = { long: 0, short: 0, longCost: 0, shortCost: 0, frozen: 0 };
positions.forEach(function (p) {
reconAssert(p && typeof p.Symbol === "string", "Position lacks unambiguous symbol");
if (p.Symbol !== symbol) { return; }
reconAssert(reconNumber(p.Amount) && p.Amount >= 0 && reconNumber(p.Price), "Malformed position");
if (p.Amount === 0) { return; }
reconAssert(p.Price > 0, "Position average price unavailable");
var side = p.Type === longConstant ? "long" : p.Type === shortConstant ? "short" : null;
reconAssert(side !== null, "Unknown position direction");
result[side] += p.Amount;
result[side + "Cost"] += p.Amount * p.Price;
if (reconNumber(p.FrozenAmount) && p.FrozenAmount > 0) { result.frozen += p.FrozenAmount; }
});
result.longAverage = result.long ? result.longCost / result.long : 0;
result.shortAverage = result.short ? result.shortCost / result.short : 0;
return result;
}
function reconExit(cycle, position, price, c) {
var long = cycle.side === "LONG", amount = long ? position.long : position.short;
if (!(amount > 0)) { return null; }
var average = long ? position.longAverage : position.shortAverage;
var ret = (long ? 1 : -1) * (price - average) / average;
var v = cycle.scale / cycle.anchor;
var peak = cycle.peakReturn === null ? ret : Math.max(cycle.peakReturn, ret);
var reason = null;
if (ret <= -c.StopLossScale * v) { reason = "STOP_LOSS"; }
else if (ret >= c.TakeProfitScale * v) { reason = "TAKE_PROFIT"; }
else if (peak >= c.TrailingActivateScale * v && peak - ret >= c.TrailingDrawdownScale * v) {
reason = "TRAILING_STOP";
}
return { reason: reason, returnRate: ret, peak: peak, volatilityUnit: v };
}
function reconValidateReceipt(order, intent, market) {
reconAssert(order && order.Symbol === intent.symbol && order.Id !== undefined, "Receipt symbol/ID mismatch");
reconAssert(reconNumber(order.Amount) && Math.abs(order.Amount - intent.quantity) <= market.step * 0.01,
"Receipt quantity mismatch");
var buy = intent.side === "buy" || intent.side === "closesell";
reconAssert(order.Type === (buy ? 0 : 1), "Receipt buy/sell direction mismatch");
reconAssert(order.Offset === (intent.purpose === "CLOSE" ? 1 : 0), "Receipt open/close direction mismatch; hedge mode required");
reconAssert(reconNumber(order.Price), "Receipt price missing");
if (intent.price === -1) {
reconAssert(order.Price === 0 || order.Price === -1, "Receipt is not an identifiable market order");
} else {
reconAssert(Math.abs(order.Price - intent.price) <= market.tick * 0.1, "Receipt limit price mismatch");
}
if (order.Time !== undefined && order.Time !== null && order.Time !== 0) {
reconAssert(reconNumber(order.Time) && Math.abs(order.Time - intent.createdAt) <= 120000,
"Receipt creation time differs from intent by over two minutes");
}
return true;
}
function reconInitialState(c, now) {
return { schema: 1, fingerprint: reconFingerprint(c), config: reconClone(c), mode: c.RunMode,
symbol: c.Symbol, phase: "BOOT", previousPhase: null, paused: false, reasons: [],
createdAt: now, updatedAt: now, sequence: 0, lastSignalBar: null, exitCurrentBar: null,
flatCount: 0, lastFlatAt: 0, cycle: null, orders: {}, intents: {}, completed: 0,
initialEquity: null, lastEquity: null, lastPnl: null, paper: null, mismatch: false };
}
/* PAPER broker. Orders fill on the NEXT broker tick after creation. */
function ReconPaper(c, state) {
this.c = c;
this.s = state || { cash: c.PaperInitialEquity, long: 0, short: 0, longAvg: 0, shortAvg: 0,
orders: {}, nextId: 1, fees: 0, realized: 0, tick: 0, price: null };
}
ReconPaper.prototype.update = function (price, market) {
reconAssert(reconNumber(price) && price > 0, "Invalid PAPER tick");
var self = this;
this.s.tick++;
this.s.price = price;
Object.keys(this.s.orders).forEach(function (id) {
var o = self.s.orders[id];
if (o.Status !== 0 || o.createdTick >= self.s.tick) { return; }
var buy = o.side === "buy" || o.side === "closesell";
if (o.Price !== -1 && !(buy ? price <= o.Price : price >= o.Price)) { return; }
var remain = Math.max(0, o.Amount - o.DealAmount);
var q = reconRoundStep(Math.max(market.step, remain * self.c.PaperFillFractionPerTick), market.step, "down");
q = Math.min(remain, q);
var close = o.side === "closebuy" || o.side === "closesell";
var side = o.side === "buy" || o.side === "closebuy" ? "long" : "short";
if (close) { q = Math.min(q, self.s[side]); }
if (!(q > 1e-12)) { o.Status = 2; return; }
var px = o.Price === -1 ? price * (1 + (buy ? 1 : -1) * self.c.PaperMarketSlippageBps / 10000) : o.Price;
var feeRate = o.Price === -1 ? self.c.PaperTakerFeeRate : self.c.PaperMakerFeeRate;
var fee = q * market.basePerUnit * px * feeRate;
if (close) {
var realized = (side === "long" ? 1 : -1) * q * market.basePerUnit * (px - self.s[side + "Avg"]);
self.s.cash += realized;
self.s.realized += realized;
self.s[side] = Math.max(0, self.s[side] - q);
if (self.s[side] < market.step * 0.01) { self.s[side] = 0; self.s[side + "Avg"] = 0; }
} else {
self.s[side + "Avg"] = (self.s[side] * self.s[side + "Avg"] + q * px) / (self.s[side] + q);
self.s[side] += q;
}
self.s.cash -= fee;
self.s.fees += fee;
o.AvgPrice = (o.AvgPrice * o.DealAmount + px * q) / (o.DealAmount + q);
o.DealAmount += q;
if (o.DealAmount >= o.Amount - market.step * 0.01) { o.Status = 1; }
});
};
ReconPaper.prototype.submit = function (symbol, side, price, qty) {
var id = "PAPER," + this.s.nextId++;
this.s.orders[id] = { Id: id, Symbol: symbol, side: side, Price: price, Amount: qty,
DealAmount: 0, AvgPrice: 0, Status: 0, Type: side === "buy" || side === "closesell" ? 0 : 1,
Offset: side === "closebuy" || side === "closesell" ? 1 : 0, createdTick: this.s.tick };
return id;
};
ReconPaper.prototype.cancel = function (id) {
var o = this.s.orders[id];
if (!o) { return false; }
if (o.Status === 0) { o.Status = 2; }
return true;
};
ReconPaper.prototype.getOrder = function (id) { return this.s.orders[id] ? reconClone(this.s.orders[id]) : null; };
ReconPaper.prototype.snapshot = function (market, symbol) {
var p = [], s = this.s;
if (s.long > 0) { p.push({ Symbol: symbol, Type: 0, Amount: s.long, Price: s.longAvg, FrozenAmount: 0 }); }
if (s.short > 0) { p.push({ Symbol: symbol, Type: 1, Amount: s.short, Price: s.shortAvg, FrozenAmount: 0 }); }
var upnl = market.basePerUnit * (s.long * (s.price - s.longAvg) + s.short * (s.shortAvg - s.price));
return { positions: p, orders: Object.keys(s.orders).map(function (id) { return reconClone(s.orders[id]); })
.filter(function (o) { return o.Status === 0; }),
account: { Balance: s.cash, Equity: s.cash + upnl, UPnL: upnl } };
};
function ReconEngine(config, adapter, io) {
this.c = reconConfig(config);
this.a = adapter;
this.io = io || {};
this.now = 0;
this.market = null;
this.lastSnapshot = null;
this.lastPosition = null;
this.lastSnapshotAt = 0;
this.snapshotHealthy = false;
this.liveAuthorized = false;
this.key = this.c.StateKeyPrefix + ":" + this.c.RunMode + ":" + this.c.ExpectedExchangeName +
":" + this.c.AccountScope + ":" + this.c.Symbol;
var stored = this.io.load ? this.io.load(this.key) : null;
this.s = stored ? reconClone(stored) : reconInitialState(this.c, Date.now());
reconAssert(this.s.schema === 1 && this.s.symbol === this.c.Symbol && this.s.mode === this.c.RunMode,
"Stored state schema/symbol/mode mismatch; manual migration required");
this.paper = this.c.RunMode === "PAPER" ? new ReconPaper(this.c, this.s.paper) : null;
if (this.s.fingerprint !== reconFingerprint(this.c)) {
this.s.mismatch = true;
this.freeze("Configuration fingerprint differs from persisted state; manual migration required");
}
var self = this;
Object.keys(this.s.intents).forEach(function (id) {
if (self.s.intents[id].status === "SUBMITTING") {
self.s.intents[id].status = "UNKNOWN";
self.freeze("Interrupted submission intent " + id + "; never auto-resubmit");
}
});
}
ReconEngine.prototype.log = function (message, extra) {
if (this.io.log) { this.io.log(message, extra === undefined ? "" : extra); }
};
ReconEngine.prototype.save = function () {
this.s.updatedAt = this.now || Date.now();
if (this.paper) { this.s.paper = reconClone(this.paper.s); }
if (this.io.save) { this.io.save(this.key, reconClone(this.s)); }
};
ReconEngine.prototype.freeze = function (reason) {
if (this.s.phase !== "FROZEN") { this.s.previousPhase = this.s.phase; }
this.s.phase = "FROZEN";
this.s.paused = true;
if (this.s.cycle) { this.s.cycle.addingStopped = true; }
this.s.flatCount = 0;
if (this.s.reasons[this.s.reasons.length - 1] !== reason) {
this.s.reasons.push(reason);
this.s.reasons = this.s.reasons.slice(-30);
this.log("FROZEN", reason);
}
this.save();
};
ReconEngine.prototype.unknown = function () {
var self = this;
return Object.keys(this.s.intents).some(function (id) {
return self.s.intents[id].status === "UNKNOWN" || self.s.intents[id].status === "SUBMITTING";
});
};
ReconEngine.prototype.queryOrder = function (id) {
return this.paper ? this.paper.getOrder(id) : this.a.getOrder(id);
};
ReconEngine.prototype.submit = function (side, price, quantity, purpose, layer) {
reconAssert(this.s.phase !== "FROZEN" && !this.unknown(), "Order submission prohibited while uncertain");
reconAssert(!this.s.paused || purpose === "CLOSE", "Opening while paused is prohibited");
var intentId = "intent-" + (++this.s.sequence);
var intent = { id: intentId, symbol: this.c.Symbol, side: side, price: price, quantity: quantity,
purpose: purpose, layer: layer, createdAt: this.now, status: "SUBMITTING", orderId: null };
this.s.intents[intentId] = intent;
this.save(); // Durable write BEFORE the external side effect.
var id;
try {
id = this.paper ? this.paper.submit(this.c.Symbol, side, price, quantity) :
this.a.submit(this.c.Symbol, side, price, quantity);
} catch (e) {
intent.status = "UNKNOWN";
this.freeze("Submit outcome unknown for " + intentId + ": " + String(e));
return null;
}
if (id === null || id === undefined || id === false || id === "") {
intent.status = "UNKNOWN";
this.freeze("No reliable order receipt for " + intentId + "; automatic retries disabled");
return null;
}
id = String(id);
intent.status = "ACKNOWLEDGED";
intent.orderId = id;
this.s.orders[id] = { id: id, intentId: intentId, side: side, purpose: purpose, layer: layer,
quantity: quantity, filled: 0, average: 0, status: 0, terminal: false,
createdAt: this.now, cancelRequested: false };
if (purpose === "OPEN") {
this.s.cycle.layers[layer].status = "SENT";
this.s.cycle.layers[layer].orderId = id;
}
this.save();
this.log("ORDER_ACK", { id: id, side: side, quantity: quantity, price: price });
return id;
};
ReconEngine.prototype.cancelOpen = function () {
// A misspelled/missing LIVE opt-in or wrong account scope must not even
// cancel orders. Only read requests occur until the LIVE startup checks pass.
if (!this.paper && !this.liveAuthorized) { return; }
var self = this;
Object.keys(this.s.orders).forEach(function (id) {
var o = self.s.orders[id];
if (o.purpose !== "OPEN" || o.terminal) { return; }
o.cancelRequested = true;
self.save();
try {
var ret = self.paper ? self.paper.cancel(id) : self.a.cancel(id);
if (ret === null || ret === false || ret === undefined) {
self.freeze("Cancellation acknowledgement unavailable: " + id);
}
// A cancellation ACK is NOT final. Reconcile GetOrder on later tick.
} catch (e) { self.freeze("Cancellation query failed: " + id + " " + String(e)); }
});
};
ReconEngine.prototype.reconcile = function (snapshot) {
reconAssert(snapshot && Array.isArray(snapshot.orders) && Array.isArray(snapshot.positions), "Incomplete account snapshot");
var self = this, liveIds = {};
snapshot.orders.forEach(function (o) {
reconAssert(o && o.Symbol === self.c.Symbol && o.Id !== undefined, "Unexpected/malformed open order");
liveIds[String(o.Id)] = true;
reconAssert(!!self.s.orders[String(o.Id)], "Unowned order on strategy instrument: " + o.Id);
});
Object.keys(this.s.orders).forEach(function (id) {
var tracked = self.s.orders[id];
if (tracked.terminal && !liveIds[id]) { return; }
var o = self.queryOrder(id);
reconAssert(o && o.Symbol === self.c.Symbol && String(o.Id) === id, "Order lookup failed/mismatched: " + id);
reconValidateReceipt(o, self.s.intents[tracked.intentId], self.market);
reconAssert(o.Status === 0 || o.Status === 1 || o.Status === 2, "Order status unknown: " + id);
reconAssert(reconNumber(o.DealAmount) && o.DealAmount >= tracked.filled - 1e-9 &&
o.DealAmount <= tracked.quantity + self.market.step * 0.01, "Inconsistent cumulative fill: " + id);
if (o.Status === 1) {
reconAssert(Math.abs(o.DealAmount - tracked.quantity) <= self.market.step * 0.01,
"Filled status lacks full cumulative fill quantity: " + id);
}
if (o.DealAmount > 0) {
reconAssert(reconNumber(o.AvgPrice) && o.AvgPrice > 0, "Fill average unavailable: " + id);
}
tracked.filled = o.DealAmount;
tracked.average = o.AvgPrice || 0;
tracked.status = o.Status;
tracked.terminal = o.Status === 1 || o.Status === 2;
reconAssert(!(tracked.terminal && liveIds[id]), "Snapshot races final order; retry requires explicit resume");
if (tracked.purpose === "OPEN" && self.s.cycle) {
self.s.cycle.layers[tracked.layer].status = tracked.terminal ? (tracked.filled > 0 ? "FILLED" : "CANCELED") : "SENT";
}
});
var p = reconPosition(snapshot.positions, this.c.Symbol, this.paper ? 0 : this.a.longType,
this.paper ? 1 : this.a.shortType);
if (!this.s.cycle) {
reconAssert(p.long === 0 && p.short === 0, "Existing position has no persisted ownership record");
} else {
reconAssert(this.s.cycle.side === "LONG" ? p.short === 0 : p.long === 0, "Opposite/unowned position detected");
var expected = 0;
Object.keys(this.s.orders).forEach(function (id) {
var o = self.s.orders[id];
expected += (o.purpose === "OPEN" ? 1 : -1) * o.filled;
});
var actual = this.s.cycle.side === "LONG" ? p.long : p.short;
// Do not invent ownership or trade while fill and position snapshots disagree.
reconAssert(Math.abs(expected - actual) <= this.market.step * 0.1, "Fill/position reconciliation mismatch");
}
this.lastSnapshot = snapshot;
this.lastPosition = p;
this.lastSnapshotAt = this.now;
this.snapshotHealthy = true;
var account = snapshot.account;
if (account && reconNumber(account.Equity) && account.Equity > 0) {
if (this.s.initialEquity === null) { this.s.initialEquity = account.Equity; }
this.s.lastEquity = account.Equity;
this.s.lastPnl = account.Equity - this.s.initialEquity;
} else { this.s.lastEquity = null; this.s.lastPnl = null; }
this.save();
return p;
};
ReconEngine.prototype.hasPending = function (purpose) {
var self = this;
return Object.keys(this.s.orders).some(function (id) {
var o = self.s.orders[id];
return !o.terminal && (!purpose || o.purpose === purpose);
});
};
ReconEngine.prototype.beginExit = function (reason) {
if (!this.s.cycle || this.unknown()) { return; }
this.s.cycle.exitReason = reason;
this.s.phase = "EXIT_CANCEL";
this.s.flatCount = 0;
this.save();
this.cancelOpen();
};
ReconEngine.prototype.manageRisk = function (position, ticker) {
var cycle = this.s.cycle;
if (!cycle || this.unknown() || this.s.mismatch || !this.snapshotHealthy) { return false; }
var risk = reconExit(cycle, position, ticker.Last, this.c);
if (!risk) { return false; }
cycle.peakReturn = risk.peak;
cycle.lastReturn = risk.returnRate;
if (risk.reason) { this.save(); this.beginExit(risk.reason); return true; }
if ((position.long + position.short) * this.market.basePerUnit * ticker.Last >
this.c.NominalBudgetUSDT * this.c.MaxGrossNotionalMultiplier) {
this.beginExit("EXPOSURE_LIMIT"); return true;
}
if (this.c.ReturnToMiddleBand) {
var currentBar = Math.floor(this.now / (this.c.PeriodSeconds * 1000));
if (cycle.middleBar !== currentBar) {
try {
var records = this.a.getRecords(this.c.Symbol, this.c.PeriodSeconds, this.c.BBPeriod + 5);
var observation = reconSignal(records, this.now, this.c);
reconAssert(observation && observation.bb && reconNumber(observation.bb.middle), "Middle band unavailable");
cycle.middle = observation.bb.middle;
cycle.middleBar = currentBar;
} catch (e) {
// A K-line outage freezes additions but does not disable the ticker-
// based stop loss / take profit / trailing checks above or next tick.
this.freeze("Middle-band data failed: " + String(e));
this.cancelOpen();
return false;
}
}
if (risk.returnRate > 0 && (cycle.side === "LONG" ? ticker.Last >= cycle.middle : ticker.Last <= cycle.middle)) {
this.beginExit("RETURN_TO_MID_BAND"); return true;
}
}
return false;
};
ReconEngine.prototype.submitPlanned = function () {
if (!this.s.cycle || this.s.cycle.addingStopped || this.s.paused || this.s.phase !== "ACTIVE") { return; }
var layers = this.s.cycle.layers;
for (var i = 0; i < layers.length; i++) {
if (layers[i].status !== "PLANNED") { continue; }
if (this.unknown() || this.s.phase !== "ACTIVE" || this.s.cycle.addingStopped) { break; }
this.submit(this.s.cycle.side === "LONG" ? "buy" : "sell", layers[i].price,
layers[i].quantity, "OPEN", layers[i].index);
}
};
ReconEngine.prototype.confirmFlat = function (position, snapshot) {
var flat = position.long === 0 && position.short === 0 && snapshot.orders.length === 0 &&
!this.hasPending() && !this.unknown();
if (!flat) { this.s.flatCount = 0; return false; }
if (!this.s.lastFlatAt || this.now - this.s.lastFlatAt >= this.c.FlatConfirmationIntervalMilliseconds) {
this.s.flatCount++;
this.s.lastFlatAt = this.now;
}
return this.s.flatCount >= this.c.FlatConfirmations;
};
ReconEngine.prototype.settle = function () {
if (this.s.cycle) {
this.log("CYCLE_SETTLED", { id: this.s.cycle.id, reason: this.s.cycle.exitReason,
equityChange: this.s.lastEquity !== null && this.s.cycle.startEquity !== null ?
this.s.lastEquity - this.s.cycle.startEquity : null,
note: "Equity change includes fees/funding/deposits; not an isolated realized-trade PnL" });
this.s.completed++;
this.s.exitCurrentBar = Math.floor(this.now / (this.c.PeriodSeconds * 1000));
}
this.s.cycle = null;
this.s.orders = {};
this.s.intents = {};
this.s.phase = "IDLE";
this.s.flatCount = 0;
this.s.lastFlatAt = 0;
this.save();
};
ReconEngine.prototype.command = function (text) {
if (!text) { return; }
var cmd = String(text).trim();
if (cmd === "status" || cmd === "diagnose") { this.log("DIAGNOSTICS", this.diagnostics()); return; }
if (cmd === "pause") { this.s.paused = true; this.save(); this.cancelOpen(); return; }
if (cmd === "close") {
if (this.unknown() || this.s.mismatch || !this.snapshotHealthy) {
this.log("Close blocked: resolve unknown intent/fingerprint/unhealthy snapshot first"); return;
}
this.s.paused = true;
this.beginExit("MANUAL_CLOSE"); return;
}
if (cmd === "resume") {
if (this.s.mismatch || this.unknown() || !this.lastSnapshot || !this.lastPosition || !this.snapshotHealthy ||
this.now - this.lastSnapshotAt > this.c.MaxTickerAgeMilliseconds) {
this.log("Resume blocked: fingerprint/unknown intent/stale snapshot"); return;
}
try { this.reconcile(this.lastSnapshot); } catch (e) { this.freeze(String(e)); return; }
this.s.paused = false;
this.s.phase = this.s.cycle ? (this.s.cycle.exitReason ? "EXIT_CANCEL" : "ACTIVE") : "BOOT";
this.s.reasons = [];
this.s.flatCount = 0;
this.save(); return;
}
// resolve:intent-N:<full FMZ order ID>; the receipt is verified, not trusted blindly.
if (cmd.indexOf("resolve:") === 0) {
var split = cmd.indexOf(":", 8), intentId = cmd.slice(8, split), orderId = cmd.slice(split + 1);
var intent = this.s.intents[intentId];
if (split < 0 || !intent || intent.status !== "UNKNOWN") { this.log("No such unknown intent"); return; }
try {
var o = this.queryOrder(orderId);
reconAssert(o && String(o.Id) === orderId, "Receipt ID differs from supplied ID");
reconValidateReceipt(o, intent, this.market);
reconAssert(!this.s.orders[orderId], "Receipt already owned");
intent.status = "ACKNOWLEDGED"; intent.orderId = orderId;
this.s.orders[orderId] = { id: orderId, intentId: intentId, side: intent.side,
purpose: intent.purpose, layer: intent.layer, quantity: intent.quantity,
filled: 0, average: 0, status: 0, terminal: false, createdAt: intent.createdAt, cancelRequested: false };
if (intent.purpose === "OPEN") {
this.s.cycle.layers[intent.layer].orderId = orderId;
this.s.cycle.layers[intent.layer].status = "SENT";
}
this.save(); this.log("Receipt linked; wait for healthy reconciliation, then resume explicitly");
} catch (e) { this.log("Receipt rejected", String(e)); }
return;
}
this.log("Commands: pause | resume | close | status | diagnose | resolve:intent-N:FULL_ORDER_ID");
};
ReconEngine.prototype.diagnostics = function () {
return { mode: this.c.RunMode, phase: this.s.phase, paused: this.s.paused,
fingerprint: this.s.fingerprint, mismatch: this.s.mismatch, reasons: this.s.reasons,
unknownIntent: this.unknown(), cycle: this.s.cycle, positions: this.lastPosition,
orders: this.s.orders, intents: this.s.intents, flatConfirmations: this.s.flatCount,
accountEquity: this.s.lastEquity, equityChangeSinceStart: this.s.lastPnl,
paperFees: this.paper ? this.paper.s.fees : null,
warning: "Inferred model; anchor/signal/scale definition are not identified by live logs" };
};
ReconEngine.prototype.step = function (now) {
this.now = now;
this.snapshotHealthy = false;
this.liveAuthorized = false;
var self = this, ticker, records, snapshot, p, signal;
try {
if (this.c.RunMode === "LIVE") {
reconAssert(this.c.LiveConfirmation === "I_ACCEPT_INFERRED_RULES", "LIVE confirmation is missing");
reconAssert(this.c.AccountScope !== "PAPER_DEMO", "LIVE needs a unique explicit account/subaccount scope label");
reconAssert(this.a.name() === this.c.ExpectedExchangeName, "Unexpected LIVE exchange adapter");
if (this.a.assertScope) { this.a.assertScope(this.c.Symbol); }
this.liveAuthorized = true;
}
if (!this.market) {
this.market = reconMarket(this.paper ? {
CtVal: this.c.PaperBasePerOrderUnit, CtValCcy: this.c.Symbol.split("_")[0],
TickSize: this.c.PaperTickSize, AmountSize: this.c.PaperAmountStep,
MinQty: this.c.PaperMinQty, MinNotional: this.c.PaperMinNotional
} : this.a.getMarket(this.c.Symbol), this.c);
}
try {
ticker = this.a.getTicker(this.c.Symbol);
reconAssert(ticker && reconNumber(ticker.Last) && ticker.Last > 0, "Ticker query failed");
if (ticker.Time !== undefined && ticker.Time !== null && ticker.Time !== 0) {
reconAssert(reconNumber(ticker.Time) && now - ticker.Time <= this.c.MaxTickerAgeMilliseconds &&
ticker.Time <= now + 5000, "Stale/future ticker timestamp");
}
} catch (tickerError) {
// A previously latched LIVE close depends on fresh owned positions/orders,
// not another price trigger. PAPER still needs a tick to simulate fills.
var latchedClose = !this.paper && !this.unknown() && !this.s.mismatch &&
(this.s.phase === "VERIFY_FLAT" || (this.s.cycle && this.s.cycle.exitReason));
if (!latchedClose) { throw tickerError; }
ticker = null;
}
if (this.paper) { this.paper.update(ticker.Last, this.market); this.save(); }
snapshot = this.paper ? this.paper.snapshot(this.market, this.c.Symbol) : this.a.snapshot(this.c.Symbol);
p = this.reconcile(snapshot);
} catch (e) {
this.freeze(String(e));
this.cancelOpen(); // Cancelling known opening orders cannot increase exposure.
return this.diagnostics();
}
if (this.s.phase === "FROZEN") {
this.cancelOpen();
// Fresh ownership/fill reconciliation permits risk reduction while the
// opening circuit breaker stays latched for the rest of this cycle.
if (!this.unknown() && !this.s.mismatch && this.s.cycle && this.snapshotHealthy) {
if (this.s.cycle.exitReason) {
// A partially completed exit remains an exit even if price later
// recovers; do not abandon its residual position on reconnection.
this.s.phase = "EXIT_CANCEL";
} else { this.manageRisk(p, ticker); }
if (this.s.phase === "FROZEN" && p.long === 0 && p.short === 0 && !this.hasPending()) {
this.s.cycle.exitReason = this.s.cycle.exitReason || "FROZEN_FLAT";
this.s.phase = "VERIFY_FLAT"; this.s.flatCount = 0; this.s.lastFlatAt = 0;
}
}
this.save(); return this.diagnostics();
}
if (this.s.phase === "BOOT" || this.s.phase === "VERIFY_FLAT") {
if (this.confirmFlat(p, snapshot)) { this.settle(); }
this.save(); return this.diagnostics();
}
if (this.s.phase === "EXIT_CANCEL") {
this.cancelOpen();
if (this.s.phase === "FROZEN") { return this.diagnostics(); }
if (!this.hasPending("OPEN") && snapshot.orders.length === 0) {
this.s.phase = "EXIT_CLOSE"; this.save();
}
return this.diagnostics();
}
if (this.s.phase === "EXIT_CLOSE") {
if (this.hasPending("CLOSE")) { return this.diagnostics(); }
var qty = this.s.cycle.side === "LONG" ? p.long : p.short;
if (qty === 0) {
this.s.phase = "VERIFY_FLAT"; this.s.flatCount = 0; this.s.lastFlatAt = 0; this.save();
return this.diagnostics();
}
qty = Math.min(qty, this.market.maxQty);
if (isFinite(this.market.maxNotional)) {
if (!ticker) {
this.freeze("Latched exit needs a fresh price to enforce finite MaxNotional");
return this.diagnostics();
}
qty = Math.min(qty, this.market.maxNotional / (ticker.Last * this.market.basePerUnit));
}
qty = reconRoundStep(qty, this.market.step, "down");
if (qty < this.market.minQty || p.frozen > 0) {
this.freeze("Residual/frozen position cannot be safely closed automatically"); return this.diagnostics();
}
// Explicit closebuy/closesell preserves FMZ close direction. NEVER reverse
// with an opening sell/buy. Preconfigured hedge mode is required for LIVE.
this.submit(this.s.cycle.side === "LONG" ? "closebuy" : "closesell", -1, qty, "CLOSE", null);
return this.diagnostics();
}
if (this.s.phase === "ACTIVE") {
var cycle = this.s.cycle;
if (this.manageRisk(p, ticker)) { return this.diagnostics(); }
if (this.s.phase === "FROZEN") { return this.diagnostics(); }
var gross = (p.long + p.short) * this.market.basePerUnit * ticker.Last;
if (gross > this.c.NominalBudgetUSDT * this.c.MaxGrossNotionalMultiplier) {
this.beginExit("EXPOSURE_LIMIT"); return this.diagnostics();
}
if (this.c.OrderTTLSeconds > 0 && now - cycle.startedAt >= this.c.OrderTTLSeconds * 1000) {
this.beginExit("OPTIONAL_TTL"); return this.diagnostics();
}
if (this.s.paused) { this.cancelOpen(); this.save(); return this.diagnostics(); }
var planned = cycle.layers.filter(function (layer) { return layer.status === "PLANNED"; });
if (planned.length) {
this.submitPlanned();
} else if (!this.hasPending() && p.long === 0 && p.short === 0) {
this.s.phase = "VERIFY_FLAT";
this.s.flatCount = 0;
cycle.exitReason = cycle.exitReason || "UNFILLED_OR_EXTERNALLY_FLAT";
}
this.save(); return this.diagnostics();
}
if (this.s.phase === "IDLE" && !this.s.paused) {
try {
records = this.a.getRecords(this.c.Symbol, this.c.PeriodSeconds, this.c.BBPeriod + 5);
signal = reconSignal(records, now, this.c);
} catch (e) { this.freeze(String(e)); return this.diagnostics(); }
if (!signal || signal.time === this.s.lastSignalBar) { return this.diagnostics(); }
this.s.lastSignalBar = signal.time; this.save();
var currentBar = Math.floor(now / (this.c.PeriodSeconds * 1000));
if (this.s.exitCurrentBar !== null && currentBar - this.s.exitCurrentBar <= this.c.CooldownBars) {
this.log("COOLDOWN", { currentBar: currentBar, exitBar: this.s.exitCurrentBar }); return this.diagnostics();
}
if (!signal.side) { return this.diagnostics(); }
if (this.c.RequireValidEquityForEntry && this.s.lastEquity === null) {
this.freeze("Cannot establish valid Equity; opening disabled"); return this.diagnostics();
}
if (snapshot.account && this.c.MinimumMarginReserveUSDT > 0 &&
(!reconNumber(snapshot.account.Balance) || snapshot.account.Balance < this.c.MinimumMarginReserveUSDT)) {
this.log("Insufficient available margin reserve"); return this.diagnostics();
}
try {
var layers = reconBuildLayers(signal, this.market, this.c);
this.s.cycle = { id: "cycle-" + (++this.s.sequence), side: signal.side, signalBar: signal.time,
anchor: signal.anchor, scale: signal.scale, percentB: signal.percentB, bb: signal.bb,
startedAt: now, startEquity: this.s.lastEquity, peakReturn: null, lastReturn: null,
exitReason: null, layers: layers, addingStopped: false,
middle: signal.bb.middle, middleBar: currentBar };
this.s.phase = "ACTIVE"; this.save();
this.log("CYCLE_CREATED", this.s.cycle);
this.submitPlanned();
} catch (e) { this.freeze(String(e)); }
}
return this.diagnostics();
};
/* FMZ adapter: local query context only; no leverage/account-mode changes. */
function reconFMZAdapter(ex) {
var contextPrepared = false;
return {
longType: typeof PD_LONG === "undefined" ? 0 : PD_LONG,
shortType: typeof PD_SHORT === "undefined" ? 1 : PD_SHORT,
name: function () { return ex.GetName(); },
assertScope: function (symbol) {
// GetAccount has no symbol argument. Initialize this strategy's local
// FMZ quote/contract context once. These setters do not trade or change
// the exchange account's leverage, hedge mode or margin mode.
var pair = symbol.split(".")[0];
if (!contextPrepared) {
ex.SetCurrency(pair);
ex.SetContractType("swap");
contextPrepared = true;
}
reconAssert(ex.GetCurrency() === pair && ex.GetContractType() === "swap",
"FMZ account query context changed unexpectedly");
},
getMarket: function (symbol) {
var markets = ex.GetMarkets();
reconAssert(markets && markets[symbol], "GetMarkets has no requested instrument");
return markets[symbol];
},
getTicker: function (symbol) { return ex.GetTicker(symbol); },
getRecords: function (symbol, period, limit) { return ex.GetRecords(symbol, period, limit); },
snapshot: function (symbol) {
var positions = ex.GetPositions(symbol), orders = ex.GetOrders(symbol), account = null;
reconAssert(Array.isArray(positions) && Array.isArray(orders), "Position/order query failed");
try { account = ex.GetAccount(); } catch (accountError) { account = null; }
// Missing equity disables new entries and PnL reporting. It must not
// disable risk reduction of independently verified owned positions.
return { positions: positions, orders: orders, account: account };
},
getOrder: function (id) { return ex.GetOrder(id); },
submit: function (symbol, side, price, amount) { return ex.CreateOrder(symbol, side, price, amount); },
cancel: function (id) { return ex.CancelOrder(id); }
};
}
var reconRunningEngine = null;
function main() {
var overrides = {};
// Optional FMZ string parameter, e.g. {"RunMode":"PAPER"}; also editable
// RECON_DEFAULTS above. No strategy template or external dependency needed.
if (typeof ReconstructionConfigJSON !== "undefined" && ReconstructionConfigJSON) {
overrides = JSON.parse(ReconstructionConfigJSON);
}
if (typeof RunMode !== "undefined") { overrides.RunMode = RunMode; }
var config = reconConfig(overrides);
reconRunningEngine = new ReconEngine(config, reconFMZAdapter(exchange), {
load: function (key) { return _G(key); },
save: function (key, value) { _G(key, value); },
log: function (message, detail) { Log(message, detail); }
});
Log("Behavioural reconstruction started", { mode: config.RunMode, symbol: config.Symbol,
fingerprint: reconFingerprint(config), unverified: "signal, anchor and BB scale definition" });
while (true) {
var now = Date.now();
var diagnostics = reconRunningEngine.step(now);
var command = GetCommand();
if (command) { reconRunningEngine.command(command); diagnostics = reconRunningEngine.diagnostics(); }
LogStatus("模式 " + config.RunMode + " | " + diagnostics.phase + " | " + config.Symbol + "\n" +
"参数指纹 " + diagnostics.fingerprint + "\n" + JSON.stringify(diagnostics));
if (diagnostics.accountEquity !== null && typeof LogProfit === "function") {
LogProfit(diagnostics.equityChangeSinceStart);
}
Sleep(config.LoopMilliseconds);
}
}
function onexit() {
if (reconRunningEngine) {
reconRunningEngine.s.paused = true;
reconRunningEngine.save();
reconRunningEngine.cancelOpen();
// A stop callback cannot prove fills/cancels are final. Persist and require
// the normal reconciliation path on restart; do not claim liquidation.
reconRunningEngine.save();
}
}
if (typeof module !== "undefined" && module.exports) {
module.exports = {
defaults: RECON_DEFAULTS, config: reconConfig, fingerprint: reconFingerprint,
roundStep: reconRoundStep, closedCandles: reconClosedCandles, bollinger: reconBollinger,
signal: reconSignal, market: reconMarket, buildLayers: reconBuildLayers,
position: reconPosition, exitDecision: reconExit, initialState: reconInitialState,
validateReceipt: reconValidateReceipt,
PaperBroker: ReconPaper, Engine: ReconEngine, fmzAdapter: reconFMZAdapter
};
}
- 1
