Type/to search
8
Follow
1388
Followers
REA 火了,让我联想到:AI 能靠实盘记录分析我的策略吗?
Discussions
Created 2026-10-15 11:11:24  Updated 2026-10-16 09:03:47
 0
 2

文章配图

先说结果:
第一轮报告交上来时,看着还真像那么回事。交易周期、布林带参数、分层下单的权重,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 能不能把这些零散记录串起来,解释背后的下单规则。


第一轮,我给了什么任务?

我的要求很简单:给它机器人链接,只看页面里的运行数据,不访问原源码和策略介绍。分析完以后,别只写一段“这大概是什么策略”的解释,还要交出一份尽可能完整的候选代码。

首次提问与模型选择

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

Agent开始分析的现场


它拿到的线索,比几个买卖点丰富

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

分析中的数据与线索

往下看才发现,这些日志里已经写着不少答案。按报告的说法,30 分钟周期、用 100 根 K 线计算的布林带、2 倍标准差、四层分批下单,以及 1∶1∶1.5∶2.5 的资金权重,都能从日志中直接找到。

所以,看到它列出一串准确参数,先别急着给满分。读出日志里写明的参数,是把现成信息整理出来;拿订单数量反推资金分配公式,才是在猜背后的规则。 这两件事都有用,只是不能一股脑算成“只看买卖动作就猜中了”。

报告还称,它用一套公式解释了 216 档委托数量,并推测了退出倍数、移动止盈和约两小时的冷却时间。这些属于 Agent 给出的推断。先把它们记下来,等揭晓源码时,再看能对上多少。


交卷:它写出了一套什么策略?

它把策略概括为:“布林带反转入场、四层分批下单、按波动尺度退出。”简单说,就是价格来到布林带边缘时寻找反向入场机会,分四档挂单,再根据波动大小设置退出条件。接着,它照这个思路写了一份 JavaScript 候选代码。

首轮交付报告

翻一翻代码注释,还能看到它没弄清的地方:入场究竟怎么触发,锚点怎么定,波动尺度怎么算,冷却时间的边界在哪里。它没有把这些都说成确定的答案。比如挂单期限没识别出来,代码就先把这一项默认关掉了。

完整代码放在文末附录,方便逐行查看。这份代码默认使用 PAPER 模式,也就是先走模拟流程;默认模拟运行,并不表示它已经通过了回测或复刻验证。

写成代码以后,有些含糊的地方就藏不住了。说一句“跌了买、涨了卖”,听着谁都懂,可真要写程序,就躲不开:跌多少才买?买多少?已经有仓位怎么办?订单挂着没成交又怎么办?这些问题都有了具体写法,接下来才能和原版一条条比较。


源码揭晓:看着相像的两句话,差在哪里?

接下来就看答案。我给出原策略链接,让 Agent 把刚交上来的代码和原版逐项比较。按对照记录的说法,第一轮代码保留了原样,没有看过源码再回头补改。

交给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 }; }
Related Recommendations
Comment
All comments (0)
No data
No data
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)