fix(portfolio): refresh plan metadata from runtime events
This commit is contained in:
parent
ac353e8de5
commit
ac17525124
@ -436,6 +436,58 @@ describe('PositionsTab DOM behavior', () => {
|
|||||||
}), 'create-exit-plan');
|
}), 'create-exit-plan');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses runtime simple setup events to expose open-plan actions before bootstrap catches up', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
fetchPositionsBootstrapMock.mockResolvedValue({
|
||||||
|
entries: [],
|
||||||
|
orders: [{
|
||||||
|
id: 'entry-order',
|
||||||
|
order_id: 'entry-order',
|
||||||
|
profile_id: 'p1',
|
||||||
|
symbol: 'BTC/USDT',
|
||||||
|
type: 'Market',
|
||||||
|
side: 'BUY',
|
||||||
|
qty: 1,
|
||||||
|
price: 100,
|
||||||
|
status: 'filled',
|
||||||
|
timestamp: now - 1_000,
|
||||||
|
created_at: new Date(now - 1_000).toISOString(),
|
||||||
|
trade_id: 'TRD-POS-1',
|
||||||
|
action: 'ENTRY',
|
||||||
|
source: 'BOT'
|
||||||
|
}],
|
||||||
|
historyTradeKeys: [],
|
||||||
|
profiles: [{ id: 'p1', name: 'High Risk Scalper' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
const onManageHolding = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const botState = buildBotState(now);
|
||||||
|
botState.operationalEvents = [{
|
||||||
|
id: 'evt-1',
|
||||||
|
type: 'SIMPLE_SETUP_UPDATE',
|
||||||
|
severity: 'INFO',
|
||||||
|
message: 'BTC/USDT entry filled. Monitoring for the configured profit exit.',
|
||||||
|
symbol: 'BTC/USDT',
|
||||||
|
setupId: 'simple-setup-live',
|
||||||
|
tradeId: 'TRD-POS-1',
|
||||||
|
timestamp: now,
|
||||||
|
}];
|
||||||
|
|
||||||
|
render(<PositionsTab botState={botState} onManageHolding={onManageHolding} />);
|
||||||
|
|
||||||
|
await user.click(await screen.findByRole('button', { name: 'High Risk Scalper' }));
|
||||||
|
const manageButtons = await screen.findAllByRole('button', { name: 'Open Plan' });
|
||||||
|
await user.click(manageButtons[0]);
|
||||||
|
|
||||||
|
expect(onManageHolding).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
symbol: 'BTC/USDT',
|
||||||
|
profileId: 'p1',
|
||||||
|
tradeId: 'TRD-POS-1',
|
||||||
|
planEntryId: 'simple-setup-live'
|
||||||
|
}), 'open-plan');
|
||||||
|
});
|
||||||
|
|
||||||
it('handles non-admin bootstrap failures with empty-state fallback', async () => {
|
it('handles non-admin bootstrap failures with empty-state fallback', async () => {
|
||||||
authState.user = { id: 'user-2' };
|
authState.user = { id: 'user-2' };
|
||||||
authState.profile = { role: 'trader' };
|
authState.profile = { role: 'trader' };
|
||||||
|
|||||||
@ -13,6 +13,12 @@ interface PositionsTabProps {
|
|||||||
onManageHolding?: (position: HybridPosition, action: 'open-plan' | 'create-exit-plan') => void;
|
onManageHolding?: (position: HybridPosition, action: 'open-plan' | 'create-exit-plan') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SimplePlanMeta = {
|
||||||
|
entryId?: string;
|
||||||
|
holdingMode: 'short_term' | 'long_term';
|
||||||
|
automationState?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
interface HybridPosition {
|
interface HybridPosition {
|
||||||
source: 'BOT' | 'MANUAL';
|
source: 'BOT' | 'MANUAL';
|
||||||
id: string;
|
id: string;
|
||||||
@ -33,6 +39,19 @@ interface HybridPosition {
|
|||||||
planEntryId?: string;
|
planEntryId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inferAutomationStateFromSimpleEvent(message: string): string | null {
|
||||||
|
const normalized = String(message || '').trim().toLowerCase();
|
||||||
|
if (!normalized) return null;
|
||||||
|
if (normalized.includes('entry submitted')) return 'entry_submitted';
|
||||||
|
if (normalized.includes('entry filled')) return 'holding_managed';
|
||||||
|
if (normalized.includes('exit submitted')) return 'exit_submitted';
|
||||||
|
if (normalized.includes('setup completed')) return 'closed';
|
||||||
|
if (normalized.includes('entry did not fill')) return 'armed';
|
||||||
|
if (normalized.includes('exit did not complete')) return 'holding_managed';
|
||||||
|
if (normalized.includes('exit partially filled')) return 'holding_managed';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
interface Profile {
|
interface Profile {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -419,7 +438,7 @@ export const assignLifecycleTradeIds = (
|
|||||||
export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) => {
|
export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) => {
|
||||||
const { user, profile } = useAuth();
|
const { user, profile } = useAuth();
|
||||||
const [manualPositions, setManualPositions] = useState<HybridPosition[]>([]);
|
const [manualPositions, setManualPositions] = useState<HybridPosition[]>([]);
|
||||||
const [simplePlanMetaByTradeId, setSimplePlanMetaByTradeId] = useState<Record<string, { entryId?: string; holdingMode: 'short_term' | 'long_term'; automationState?: string | null }>>({});
|
const [simplePlanMetaByTradeId, setSimplePlanMetaByTradeId] = useState<Record<string, SimplePlanMeta>>({});
|
||||||
const [dbOrders, setDbOrders] = useState<RawOrderRecord[]>([]);
|
const [dbOrders, setDbOrders] = useState<RawOrderRecord[]>([]);
|
||||||
const [historyTradeKeys, setHistoryTradeKeys] = useState<string[]>([]);
|
const [historyTradeKeys, setHistoryTradeKeys] = useState<string[]>([]);
|
||||||
const [profiles, setProfiles] = useState<Profile[]>([]);
|
const [profiles, setProfiles] = useState<Profile[]>([]);
|
||||||
@ -520,7 +539,7 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
|||||||
automationState: String(entry.automation_state || '').trim() || null,
|
automationState: String(entry.automation_state || '').trim() || null,
|
||||||
}] as const;
|
}] as const;
|
||||||
})
|
})
|
||||||
.filter(Boolean) as Array<readonly [string, { entryId?: string; holdingMode: 'short_term' | 'long_term'; automationState?: string | null }]>
|
.filter(Boolean) as Array<readonly [string, SimplePlanMeta]>
|
||||||
);
|
);
|
||||||
setSimplePlanMetaByTradeId(nextSimplePlanMetaByTradeId);
|
setSimplePlanMetaByTradeId(nextSimplePlanMetaByTradeId);
|
||||||
}
|
}
|
||||||
@ -539,6 +558,36 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
|||||||
};
|
};
|
||||||
}, [user, profile?.role]);
|
}, [user, profile?.role]);
|
||||||
|
|
||||||
|
const runtimeSimplePlanMetaByTradeId = useMemo(() => {
|
||||||
|
const merged: Record<string, Partial<SimplePlanMeta>> = {};
|
||||||
|
const events = Array.isArray(botState.operationalEvents) ? botState.operationalEvents : [];
|
||||||
|
for (const event of events) {
|
||||||
|
if (!event || event.type !== 'SIMPLE_SETUP_UPDATE') continue;
|
||||||
|
const tradeId = String(event.tradeId || '').trim();
|
||||||
|
if (!tradeId) continue;
|
||||||
|
const nextAutomationState = inferAutomationStateFromSimpleEvent(event.message);
|
||||||
|
const existing = merged[tradeId] || {};
|
||||||
|
merged[tradeId] = {
|
||||||
|
entryId: String(event.setupId || '').trim() || existing.entryId,
|
||||||
|
automationState: nextAutomationState ?? existing.automationState ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}, [botState.operationalEvents]);
|
||||||
|
|
||||||
|
const effectiveSimplePlanMetaByTradeId = useMemo(() => {
|
||||||
|
const merged: Record<string, SimplePlanMeta> = { ...simplePlanMetaByTradeId };
|
||||||
|
for (const [tradeId, runtimeMeta] of Object.entries(runtimeSimplePlanMetaByTradeId)) {
|
||||||
|
const existing = merged[tradeId];
|
||||||
|
merged[tradeId] = {
|
||||||
|
entryId: runtimeMeta.entryId || existing?.entryId,
|
||||||
|
holdingMode: existing?.holdingMode || 'short_term',
|
||||||
|
automationState: runtimeMeta.automationState ?? existing?.automationState ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}, [runtimeSimplePlanMetaByTradeId, simplePlanMetaByTradeId]);
|
||||||
|
|
||||||
// 2. Build bot positions from real-time Socket.IO data
|
// 2. Build bot positions from real-time Socket.IO data
|
||||||
const botPositionsRaw: HybridPosition[] = useMemo(() => {
|
const botPositionsRaw: HybridPosition[] = useMemo(() => {
|
||||||
const deduped = new Map<string, HybridPosition>();
|
const deduped = new Map<string, HybridPosition>();
|
||||||
@ -566,14 +615,14 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
|||||||
profileId: p.profileId,
|
profileId: p.profileId,
|
||||||
profileName: p.profileName,
|
profileName: p.profileName,
|
||||||
tradeId: p.tradeId,
|
tradeId: p.tradeId,
|
||||||
planMode: p.tradeId && simplePlanMetaByTradeId[p.tradeId]
|
planMode: p.tradeId && effectiveSimplePlanMetaByTradeId[p.tradeId]
|
||||||
? simplePlanMetaByTradeId[p.tradeId].holdingMode
|
? effectiveSimplePlanMetaByTradeId[p.tradeId].holdingMode
|
||||||
: undefined,
|
: undefined,
|
||||||
planState: p.tradeId && simplePlanMetaByTradeId[p.tradeId]
|
planState: p.tradeId && effectiveSimplePlanMetaByTradeId[p.tradeId]
|
||||||
? simplePlanMetaByTradeId[p.tradeId].automationState || null
|
? effectiveSimplePlanMetaByTradeId[p.tradeId].automationState || null
|
||||||
: null,
|
: null,
|
||||||
planEntryId: p.tradeId && simplePlanMetaByTradeId[p.tradeId]
|
planEntryId: p.tradeId && effectiveSimplePlanMetaByTradeId[p.tradeId]
|
||||||
? simplePlanMetaByTradeId[p.tradeId].entryId
|
? effectiveSimplePlanMetaByTradeId[p.tradeId].entryId
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -598,7 +647,7 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Array.from(deduped.values());
|
return Array.from(deduped.values());
|
||||||
}, [botState.positions, simplePlanMetaByTradeId]);
|
}, [botState.positions, effectiveSimplePlanMetaByTradeId]);
|
||||||
|
|
||||||
const managedSymbols = useMemo(() => {
|
const managedSymbols = useMemo(() => {
|
||||||
return new Set(Object.keys(botState.symbols || {}).map((symbol) => String(symbol).toUpperCase()));
|
return new Set(Object.keys(botState.symbols || {}).map((symbol) => String(symbol).toUpperCase()));
|
||||||
@ -1084,14 +1133,14 @@ export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) =
|
|||||||
profileId: position.profileId,
|
profileId: position.profileId,
|
||||||
profileName: position.profileName,
|
profileName: position.profileName,
|
||||||
tradeId: position.tradeId,
|
tradeId: position.tradeId,
|
||||||
planMode: position.tradeId && simplePlanMetaByTradeId[position.tradeId]
|
planMode: position.tradeId && effectiveSimplePlanMetaByTradeId[position.tradeId]
|
||||||
? simplePlanMetaByTradeId[position.tradeId].holdingMode
|
? effectiveSimplePlanMetaByTradeId[position.tradeId].holdingMode
|
||||||
: undefined,
|
: undefined,
|
||||||
planState: position.tradeId && simplePlanMetaByTradeId[position.tradeId]
|
planState: position.tradeId && effectiveSimplePlanMetaByTradeId[position.tradeId]
|
||||||
? simplePlanMetaByTradeId[position.tradeId].automationState || null
|
? effectiveSimplePlanMetaByTradeId[position.tradeId].automationState || null
|
||||||
: null,
|
: null,
|
||||||
planEntryId: position.tradeId && simplePlanMetaByTradeId[position.tradeId]
|
planEntryId: position.tradeId && effectiveSimplePlanMetaByTradeId[position.tradeId]
|
||||||
? simplePlanMetaByTradeId[position.tradeId].entryId
|
? effectiveSimplePlanMetaByTradeId[position.tradeId].entryId
|
||||||
: undefined
|
: undefined
|
||||||
} as HybridPosition));
|
} as HybridPosition));
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user