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,
|
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 bindSimpleBoughtPosition = async (entry: ManualEntryRecord, ctx: UserContext): Promise<boolean> => {
|
||||||
const linkedTradeId = String(entry.linked_trade_id || '').trim();
|
const linkedTradeId = String(entry.linked_trade_id || '').trim();
|
||||||
const activePosition = linkedTradeId
|
const activePosition = linkedTradeId
|
||||||
@ -467,7 +477,85 @@ async function main() {
|
|||||||
return;
|
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;
|
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;
|
if (normalizedStatus !== 'filled' && normalizedStatus !== 'partially_filled') return;
|
||||||
|
|
||||||
const active = executor.getActivePosition(event.symbol, event.tradeId);
|
const active = executor.getActivePosition(event.symbol, event.tradeId);
|
||||||
@ -498,6 +586,35 @@ async function main() {
|
|||||||
|
|
||||||
if (!applied.fullyClosed) {
|
if (!applied.fullyClosed) {
|
||||||
logger.info(`[OrderSync] Applied partial EXIT for ${event.symbol} in ${scopeLabel}: filled=${applied.appliedQty}, remaining=${applied.remainingSize}`);
|
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