From e853ffc0c58091851ccf70674a5b503cf2822c89 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 6 May 2026 17:07:13 +0000 Subject: [PATCH] fix(api): reconcile simple setup state on order sync --- backend/src/index.ts | 117 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/backend/src/index.ts b/backend/src/index.ts index eefcae9..affe68a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -256,6 +256,16 @@ async function main() { symbol: String(entry.symbol || '').trim().toUpperCase() || undefined, }); }; + const findSimpleEntryByTrade = async (userId: string | undefined, tradeId: string | undefined): Promise => { + const normalizedUserId = String(userId || '').trim(); + const normalizedTradeId = String(tradeId || '').trim(); + if (!normalizedUserId || !normalizedTradeId) return null; + const entries = await listManualEntries({ userId: normalizedUserId }); + return entries.find((entry) => + isSimpleWorkflowEntry(entry) + && String(entry.linked_trade_id || '').trim() === normalizedTradeId + ) || null; + }; const bindSimpleBoughtPosition = async (entry: ManualEntryRecord, ctx: UserContext): Promise => { const linkedTradeId = String(entry.linked_trade_id || '').trim(); const activePosition = linkedTradeId @@ -467,7 +477,85 @@ async function main() { return; } + if (normalizedAction === 'ENTRY') { + const simpleEntry = await findSimpleEntryByTrade(event.userId, event.tradeId); + if (!simpleEntry) return; + + if (normalizedStatus === 'filled' || normalizedStatus === 'partially_filled') { + const reboundExistingPosition = await bindSimpleBoughtPosition(simpleEntry, { + userId: String(event.userId || '').trim(), + email: '', + profileId: String(simpleEntry.profile_id || event.profileId || '').trim(), + profileName: 'Simple Auto Profile', + profileSettings: null, + monitoredSymbols: [event.symbol], + executor, + autoTrader: null as any, + manualTrader: null as any, + monitor: null as any, + orderSync: null as any, + }); + if (!reboundExistingPosition) { + await saveManualEntryForUser(simpleEntry.user_id, { + ...simpleEntry, + profile_id: simpleEntry.profile_id || event.profileId || null, + linked_trade_id: event.tradeId || simpleEntry.linked_trade_id, + entry_price: event.fillPrice || simpleEntry.entry_price, + filled_quantity: event.fillQty || simpleEntry.filled_quantity || simpleEntry.quantity, + buy_time: simpleEntry.buy_time || new Date().toISOString(), + status: 'simple_bought', + active: true, + }); + emitSimpleSetupEvent( + { + ...simpleEntry, + linked_trade_id: event.tradeId || simpleEntry.linked_trade_id, + profile_id: simpleEntry.profile_id || event.profileId || null, + }, + `${event.symbol} entry filled. Monitoring for the configured profit exit.`, + 'INFO' + ); + } + return; + } + + if (['canceled', 'expired', 'rejected', 'unknown'].includes(normalizedStatus)) { + await saveManualEntryForUser(simpleEntry.user_id, { + ...simpleEntry, + status: 'simple_armed_buy', + active: true, + buy_time: null, + entry_price: null, + filled_quantity: null, + }); + emitSimpleSetupEvent( + simpleEntry, + `${event.symbol} entry did not fill (${normalizedStatus}). The setup is armed again and waiting for the trigger.`, + normalizedStatus === 'rejected' || normalizedStatus === 'unknown' ? 'WARN' : 'INFO' + ); + } + return; + } + if (normalizedAction !== 'EXIT') return; + + const simpleExitEntry = await findSimpleEntryByTrade(event.userId, event.tradeId); + if (['canceled', 'expired', 'rejected', 'unknown'].includes(normalizedStatus)) { + if (simpleExitEntry) { + await saveManualEntryForUser(simpleExitEntry.user_id, { + ...simpleExitEntry, + status: 'simple_bought', + active: true, + }); + emitSimpleSetupEvent( + simpleExitEntry, + `${event.symbol} exit did not complete (${normalizedStatus}). The setup is back to monitoring the profit target.`, + normalizedStatus === 'rejected' || normalizedStatus === 'unknown' ? 'WARN' : 'INFO' + ); + } + return; + } + if (normalizedStatus !== 'filled' && normalizedStatus !== 'partially_filled') return; const active = executor.getActivePosition(event.symbol, event.tradeId); @@ -498,6 +586,35 @@ async function main() { if (!applied.fullyClosed) { logger.info(`[OrderSync] Applied partial EXIT for ${event.symbol} in ${scopeLabel}: filled=${applied.appliedQty}, remaining=${applied.remainingSize}`); + if (simpleExitEntry) { + await saveManualEntryForUser(simpleExitEntry.user_id, { + ...simpleExitEntry, + status: 'simple_bought', + active: true, + filled_quantity: applied.remainingSize, + }); + emitSimpleSetupEvent( + simpleExitEntry, + `${event.symbol} exit partially filled. Remaining size is still being monitored for the profit target.`, + 'INFO' + ); + } + return; + } + + if (simpleExitEntry) { + await saveManualEntryForUser(simpleExitEntry.user_id, { + ...simpleExitEntry, + active: false, + status: 'sellCompleted', + sell_time: simpleExitEntry.sell_time || new Date().toISOString(), + sell_price: event.fillPrice || active.entryPrice, + }); + emitSimpleSetupEvent( + simpleExitEntry, + `${event.symbol} setup completed. The linked position is closed.`, + 'INFO' + ); } };