Let me start with the result.
When the first report came back, it looked convincing. The AI had listed the trading timeframe, the Bollinger Band parameters, and the weights for the layered orders, with a complete implementation attached. Looking at that submission alone, it was easy to think: has it figured out this strategy completely?
I then gave it a link to the original strategy and asked it to compare its work against the source code. That was when things turned out to be more complicated: some parameters matched, but the trading rules were not entirely the same.
I wanted to understand what it had actually recognized in the records and where it had filled in the gaps with its own guesses. The differences gradually became clearer when I put the analysis process alongside the final source-code comparison.
REA Inspired a Strategy-Inference Experiment
REA has been getting attention lately. It provides reverse-engineering tools for agents to analyze programs, applications, and their runtime behavior. That gave me an idea: quantitative strategies leave plenty of records every day, too. Could an AI first observe how a strategy buys and sells, then infer the program behind it? About the REA project
So I selected a publicly viewable bot and asked a coding agent to examine its runtime records, then submit strategy code that it considered a plausible explanation. Only after it had submitted its work did I provide the link to the original strategy. REA inspired this experiment, but the coding agent did the actual analysis and programming. REA itself was not involved.
I used Codex for this experiment; the model selected in the screenshots is GPT-6 Astra Ultra. Below, I walk through the analysis, the submission, and the comparison against the source code, in that order.
Both the Test Case and the Reference Answer Are Public
I chose bot 674839, named D-Man V3 FMZ Rust on its page. The reference, strategy 546875, also has a public page, where the version is listed as v1.1.1; the full source code is available after signing in. I chose it precisely because there would be an answer to check at the end: I could compare the AI's claims against the code, item by item.

This screenshot was saved at the time of the experiment. The figures reflect that moment.
One detail needs to be made clear upfront: the analysis records mention a simulate flag in the exchange scope. Although the platform labels this a “live trading bot” page, the name alone does not establish whether it was trading real money. What I am examining here is the record of its operation. The returns shown on the page are not treated as verified real-money performance.
Runtime records are not a ready-made strategy manual, though. If you see three buys, for example, can you conclude that the strategy added to its position three times? It could just be one order filled in three parts. I wanted to see whether the AI could connect these scattered records and explain the order-placement rules behind them.
What Did I Ask It to Do in the First Round?
My request was simple: I gave it the bot link and asked it to use only the runtime data on that page, without accessing the original source code or strategy description. After analyzing the data, it was to do more than write a paragraph explaining “roughly what kind of strategy this is.” It also had to deliver candidate code that was as complete as possible.

The screenshot above shows the prompt and model selected at the time.

The Clues Went Beyond a Few Buys and Sells
It first reported a set of figures: 9,894 public log entries and 54 profit data points. I am keeping the term “profit data points” used during the analysis here. The final report called them “54 cycles,” which is an inconsistent description; they cannot simply be taken as the number of fills or complete trading cycles.

Reading further, I found that the logs already contained many of the answers. According to the report, the 30-minute timeframe, Bollinger Bands calculated from 100 candlesticks, two standard deviations, four layers of staged orders, and capital weights of 1:1:1.5:2.5 could all be found directly in the logs.
So when it lists a series of accurate parameters, do not rush to give it full marks. Reading parameters explicitly stated in the logs means organizing information that is already there; working backward from order quantities to a capital-allocation formula means inferring the rules behind them. Both are useful, but they cannot all be counted as “figuring it out from the trading actions alone.”
The report also claimed that a single formula explained 216 observed order quantities, and it inferred exit multiples, a trailing take-profit rule, and a cooldown of roughly two hours. These were the agent's inferences. I noted them down so that, once the source code was revealed, we could see how many matched.
The Submission: What Kind of Strategy Did It Write?
It summarized the strategy as “Bollinger Band reversal entries, four layers of staged orders, and volatility-scaled exits.” In simple terms, it looked for reversal entries when the price reached the edges of the Bollinger Bands, placed orders at four levels, and set exit conditions according to the size of the volatility measure. It then wrote a candidate JavaScript implementation based on that interpretation.

The code comments also reveal what it had not resolved: exactly what triggers an entry, how the anchor is set, how the volatility scale is calculated, and where the cooldown boundary lies. It did not present all of these as settled answers. For example, it could not identify an order-expiry rule, so that feature was disabled by default in the code.
The complete code is included in the appendix for line-by-line inspection. It defaults to PAPER mode, meaning it starts with simulated operation. A simulation default does not mean it has passed a backtest or been verified as a faithful reconstruction.
Once an interpretation becomes code, some ambiguities can no longer hide. “Buy when it falls, sell when it rises” sounds straightforward, but a program cannot avoid the details: how far must the price fall before buying? How much should it buy? What if there is already a position? What if an order has been placed but remains unfilled? Once these questions have explicit answers in code, they can be compared with the original one by one.
Revealing the Source: How Do Two Similar-Sounding Rules Differ?
It was time to check the answer. I provided the original strategy link and asked the agent to compare its newly submitted code against the original, item by item. According to the comparison record, the first-round code was kept unchanged; it was not revised after the source code had been viewed.

In the comparison report, the four layer weights, several exit multiples, and the roughly two-hour cooldown all matched. The entry rule was where a difference emerged: the report said the original strategy required an “initial crossing outside the band,” whereas the candidate code checked whether the price was “currently outside the band.” Looking at the candidate code, it checks whether the closing price of the latest completed candlestick touches or moves beyond either Bollinger Band. It does not also check whether the preceding candlestick was inside the bands.
Those two formulations may sound similar, but a short price sequence makes the difference clear. For this comparison, imagine the lower Bollinger Band as a fixed horizontal line: the price starts above it, falls below it, and then stays below it for two more candlesticks.
- “Initial crossing outside the band” looks for the crossing itself, so it needs to compare the states before and after.
- “Currently outside the band” looks only at the present position, so the next two candlesticks may still satisfy the condition.
Here, we are looking only at the signal condition. Even when that condition is met, the program must still check positions, cooldown, and budget before deciding whether to place an order. Each signal therefore cannot be treated as an actual fill. Nevertheless, the entry checks alone already show that these are different rules.
Some differences were less obvious. The report noted that the original had an equity-based budget cap, entry-order expiry, and a maximum holding period, and that the first-round code had not reproduced equivalent versions of these controls. It is easy to see why: if the budget never reaches its cap during a stretch of trading, orders fill quickly, and positions close quickly, these constraints can be difficult to detect from trading actions.
As a result, several different sets of rules may all explain the same runtime records. They happen to do similar things in that stretch of the market, and only make different choices when some condition changes.
What Can We Conclude from This Experiment?
After the agent finished comparing its work against the source code, the result was somewhat disappointing.
If the goal was to give an AI the link to a bot you liked and receive a strategy that reproduced it completely, this experiment did not achieve that. It recognized the broad approach and correctly identified quite a few parameters, but failed to reconstruct several key details that affect trading. To me, it looked more like taking inspiration from the original, imitating it, and filling in the rest with its own interpretation. What it wrote was essentially a new strategy.

According to the comparison report, the strategy framework and several parameters were identified correctly, but the first-round code did not reproduce the original strategy's behavior.
Another distinction can easily get blurred here. Some differences may have resulted from the AI overlooking available clues; other rules may not have left any identifiable trace in these records. Both show up as a “failure to reconstruct,” but they call for different improvements. Simply phrasing the question differently cannot guarantee that missing evidence will appear.
This article presents the agent's source-code comparison results. They do not justify a numerical claim about “what percentage was reconstructed.” Quantifying the behavioral gap would require a trade-by-trade replay and validation against independent market data. This result shows that this candidate code failed to reproduce the original strategy; it does not support a conclusion about all models.
What Does It Mean for AI to Learn a Trading Pattern?
Once the source was revealed, the mismatches made the idea of “learning a strategy” more concrete. Recognizing Bollinger Bands and staged orders is only a starting point; the AI also needs to understand under what conditions the strategy changes its decision. The differences in entry conditions and execution constraints brought that question into focus.
If the price falls below the lower band in two different instances, why does one result in a fill while the other does not? There may already have been a position, the strategy may still have been in cooldown, or an order may have been placed but never filled. Giving the AI these cases together with the market conditions, positions, and order states at the time gives it a chance to distinguish between the possible explanations.
That makes the choice of examples important, too. A long series of ordinary position entries and exits may reveal few hidden conditions. Cases where an order remains unfilled for a long time, a position stays open for a long time, or the budget approaches its cap deserve a closer look. Asking the AI to list possible explanations for these cases and identify the evidence each would require gives the analysis a concrete focus.
For people researching strategies, this analysis can help organize the reasons for entering trades, the position sizing, and the exit conditions, then assess which ideas are worth learning from. The candidate code turns that understanding into a program. Whether it will make choices close to the original strategy's in new market conditions still needs to be tested.
So when an AI says, “I understand this strategy,” I would rather follow up with: given the same market conditions but a different position or order state, would it make a different choice? Which records support that judgment, and under what circumstances would it fail to hold?
Appendix
Candidate code generated in the first round of analysis:
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

