fix(api): reconcile simple setup state on order sync
This commit is contained in:
parent
62804ed4e5
commit
e853ffc0c5
@ -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'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user