min-trading-engine-improvements
· 2 min read
Problem statement (original bug scope)
- Realized PnL reported zero for closed positions despite observed TP hits.
- Exit reasons were misclassified (e.g., MANUAL) when TPs or SLs clearly applied.
- Shadow simulator used inverted trigger rules for LONG/SHORT TP/SL, producing false signals.
- TP reconciliation re‑created already filled levels, causing repeated “TP filled” cycles and skewed outcomes.
- Tournament/dashboard PnL inherited these upstream inaccuracies.
Fixes implemented (correctness first)
- Fixed shadow trigger polarity: LONG TP when high ≥ price; LONG SL when low ≤ price (inverse for SHORT).
- Reliable final PnL and exitReason for CLOSED: use exitPrice or weighted TP fills; added COMBINED.
- History/tolerance‑aware reconciliation: skip creating TPs whose price matches a filled level.
Minimal evolutionary improvements (no heavy refactor)
- Idempotent reconciliation (no migrations): target − executed (history‑aware) to decide orders; price tolerance to avoid duplicates.
- Guard rails in code: if remainingQty === 0 → no new orders; if a TP is marked filled → do not recreate at same price.
- Strong invariants in tests: trigger polarity; “no TP recreation if filled”; “CLOSED ⇒ unrealized = 0; exitReason consistent”.
- Telemetry to prevent regressions: counters for TP re‑creation attempts; CLOSED with unrealized ≠ 0.
Optional next step (still lightweight)
- Make shadow simulator side‑effect‑free: emit candidate events; a single validator applies them.
Code examples (sketches)
Idempotent reconciliation (pure diff + tolerance)
export interface OrderIntent { kind: 'TP' | 'SL'; price: number; qty: number; idKey: string }
export function computeDesiredOrders(
target: { tps: Array<{ price: number; percentage: number }>; sl?: number },
executed: { filledTps: Array<{ price: number }>; slFilled: boolean },
remainingQty: number,
tolerance = 0.001,
): OrderIntent[] {
const near = (a: number, b: number) => Math.abs(a - b) / (b || 1) < tolerance;
const openTps = target.tps.filter(tp => !executed.filledTps.some(f => near(f.price, tp.price)));
const intents: OrderIntent[] = openTps.map(tp => ({
kind: 'TP',
price: tp.price,
qty: (remainingQty * tp.percentage) / 100,
idKey: `tp:${tp.price.toFixed(6)}`,
}));
if (target.sl && !executed.slFilled) {
intents.push({ kind: 'SL', price: target.sl, qty: remainingQty, idKey: `sl:${target.sl.toFixed(6)}` });
}
return intents;
}
Finite state guard (no illegal transitions)
export enum PositionFsmState { OPEN = 'OPEN', PARTIAL = 'PARTIAL', CLOSED = 'CLOSED' }
export function deriveState(position: Position): PositionFsmState {
if (position.remainingQty === 0) return PositionFsmState.CLOSED;
if (position.remainingQty < position.baseQty) return PositionFsmState.PARTIAL;
return PositionFsmState.OPEN;
}
export function assertNoIllegalTransition(before: PositionFsmState, after: PositionFsmState) {
if (before === PositionFsmState.CLOSED && after !== PositionFsmState.CLOSED) {
throw new Error('Illegal transition: CLOSED cannot reopen');
}
}
Reliable final PnL and exitReason (CLOSED)
export function determineExitReason(p: Position): 'take_profit' | 'stop_loss' | 'combined' | 'manual' {
const anyTp = (p.tpsl?.active?.takeProfits || []).some(tp => tp.lastStatus === 'filled') || (p.tpsl?.history?.filledTPs || []).length > 0;
const sl = p.tpsl?.active?.stopLoss?.lastStatus === 'filled';
if (sl && anyTp) return 'combined';
if (sl) return 'stop_loss';
if (anyTp && p.remainingQty === 0) return 'take_profit';
return 'manual';
}
export function weightedExitPrice(p: Position): number | undefined {
const filled = (p.tpsl?.active?.takeProfits || []).filter(tp => tp.lastStatus === 'filled' && typeof tp.filledPrice === 'number');
if (filled.length === 0) return p.exitPrice;
const totalPct = filled.reduce((s, tp) => s + tp.percentage, 0);
if (totalPct <= 0) return p.exitPrice;
return filled.reduce((s, tp) => s + (tp.filledPrice as number) * tp.percentage, 0) / totalPct;
}
Shadow simulator without side effects (optional)
export async function simulateShadow(position: Position, market: MarketData) {
const events = [] as Array<{ type: 'TakeProfitFilled' | 'StopLossHit'; tpIndex?: number; price: number; at: Date; idKey: string }>;
// Emit only; apply elsewhere
return events;
}