fix(api): reconcile simple setup state on order sync

This commit is contained in:
root 2026-05-06 17:07:13 +00:00
parent 62804ed4e5
commit e853ffc0c5

View File

@ -256,6 +256,16 @@ async function main() {
symbol: String(entry.symbol || '').trim().toUpperCase() || undefined,
});
};
const findSimpleEntryByTrade = async (userId: string | undefined, tradeId: string | undefined): Promise<ManualEntryRecord | null> => {
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<boolean> => {
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'
);
}
};