feat(simple): add setup activity history
This commit is contained in:
parent
7de6b236c0
commit
0b526f3499
@ -20,5 +20,8 @@ export interface OperationalEvent {
|
|||||||
profileId?: string;
|
profileId?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
symbol?: string;
|
symbol?: string;
|
||||||
|
setupId?: string;
|
||||||
|
tradeId?: string;
|
||||||
|
orderId?: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -246,7 +246,12 @@ async function main() {
|
|||||||
const livePrice = Number(apiServer.getState().symbols?.[symbol]?.price || 0);
|
const livePrice = Number(apiServer.getState().symbols?.[symbol]?.price || 0);
|
||||||
return Number.isFinite(livePrice) && livePrice > 0 ? livePrice : null;
|
return Number.isFinite(livePrice) && livePrice > 0 ? livePrice : null;
|
||||||
};
|
};
|
||||||
const emitSimpleSetupEvent = (entry: ManualEntryRecord, message: string, severity: 'INFO' | 'WARN' | 'ERROR' = 'INFO') => {
|
const emitSimpleSetupEvent = (
|
||||||
|
entry: ManualEntryRecord,
|
||||||
|
message: string,
|
||||||
|
severity: 'INFO' | 'WARN' | 'ERROR' = 'INFO',
|
||||||
|
metadata?: { tradeId?: string; orderId?: string }
|
||||||
|
) => {
|
||||||
observabilityService.emitEvent({
|
observabilityService.emitEvent({
|
||||||
type: 'SIMPLE_SETUP_UPDATE',
|
type: 'SIMPLE_SETUP_UPDATE',
|
||||||
severity,
|
severity,
|
||||||
@ -254,6 +259,9 @@ async function main() {
|
|||||||
profileId: String(entry.profile_id || '').trim() || undefined,
|
profileId: String(entry.profile_id || '').trim() || undefined,
|
||||||
userId: String(entry.user_id || '').trim() || undefined,
|
userId: String(entry.user_id || '').trim() || undefined,
|
||||||
symbol: String(entry.symbol || '').trim().toUpperCase() || undefined,
|
symbol: String(entry.symbol || '').trim().toUpperCase() || undefined,
|
||||||
|
setupId: String(entry.stock_instance_id || '').trim() || undefined,
|
||||||
|
tradeId: String(metadata?.tradeId || entry.linked_trade_id || '').trim() || undefined,
|
||||||
|
orderId: String(metadata?.orderId || '').trim() || undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const findSimpleEntryByTrade = async (userId: string | undefined, tradeId: string | undefined): Promise<ManualEntryRecord | null> => {
|
const findSimpleEntryByTrade = async (userId: string | undefined, tradeId: string | undefined): Promise<ManualEntryRecord | null> => {
|
||||||
@ -364,7 +372,8 @@ async function main() {
|
|||||||
linked_trade_id: result.tradeId || entry.linked_trade_id,
|
linked_trade_id: result.tradeId || entry.linked_trade_id,
|
||||||
},
|
},
|
||||||
`${symbol} entry submitted${result.adjustedQty ? ` for ${result.adjustedQty}` : ''}. Waiting for fill confirmation.`,
|
`${symbol} entry submitted${result.adjustedQty ? ` for ${result.adjustedQty}` : ''}. Waiting for fill confirmation.`,
|
||||||
'INFO'
|
'INFO',
|
||||||
|
{ tradeId: result.tradeId, orderId: result.orderId }
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -455,7 +464,8 @@ async function main() {
|
|||||||
linked_trade_id: linkedTradeId || activePosition.tradeId,
|
linked_trade_id: linkedTradeId || activePosition.tradeId,
|
||||||
},
|
},
|
||||||
`${symbol} exit submitted after hitting the configured profit target. Waiting for fill confirmation.`,
|
`${symbol} exit submitted after hitting the configured profit target. Waiting for fill confirmation.`,
|
||||||
'INFO'
|
'INFO',
|
||||||
|
{ tradeId: linkedTradeId || activePosition.tradeId }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -513,7 +523,8 @@ async function main() {
|
|||||||
profile_id: simpleEntry.profile_id || event.profileId || null,
|
profile_id: simpleEntry.profile_id || event.profileId || null,
|
||||||
},
|
},
|
||||||
`${event.symbol} entry filled. Monitoring for the configured profit exit.`,
|
`${event.symbol} entry filled. Monitoring for the configured profit exit.`,
|
||||||
'INFO'
|
'INFO',
|
||||||
|
{ tradeId: event.tradeId, orderId: event.orderId }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -531,7 +542,8 @@ async function main() {
|
|||||||
emitSimpleSetupEvent(
|
emitSimpleSetupEvent(
|
||||||
simpleEntry,
|
simpleEntry,
|
||||||
`${event.symbol} entry did not fill (${normalizedStatus}). The setup is armed again and waiting for the trigger.`,
|
`${event.symbol} entry did not fill (${normalizedStatus}). The setup is armed again and waiting for the trigger.`,
|
||||||
normalizedStatus === 'rejected' || normalizedStatus === 'unknown' ? 'WARN' : 'INFO'
|
normalizedStatus === 'rejected' || normalizedStatus === 'unknown' ? 'WARN' : 'INFO',
|
||||||
|
{ tradeId: event.tradeId, orderId: event.orderId }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -550,7 +562,8 @@ async function main() {
|
|||||||
emitSimpleSetupEvent(
|
emitSimpleSetupEvent(
|
||||||
simpleExitEntry,
|
simpleExitEntry,
|
||||||
`${event.symbol} exit did not complete (${normalizedStatus}). The setup is back to monitoring the profit target.`,
|
`${event.symbol} exit did not complete (${normalizedStatus}). The setup is back to monitoring the profit target.`,
|
||||||
normalizedStatus === 'rejected' || normalizedStatus === 'unknown' ? 'WARN' : 'INFO'
|
normalizedStatus === 'rejected' || normalizedStatus === 'unknown' ? 'WARN' : 'INFO',
|
||||||
|
{ tradeId: event.tradeId, orderId: event.orderId }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -596,7 +609,8 @@ async function main() {
|
|||||||
emitSimpleSetupEvent(
|
emitSimpleSetupEvent(
|
||||||
simpleExitEntry,
|
simpleExitEntry,
|
||||||
`${event.symbol} exit partially filled. Remaining size is still being monitored for the profit target.`,
|
`${event.symbol} exit partially filled. Remaining size is still being monitored for the profit target.`,
|
||||||
'INFO'
|
'INFO',
|
||||||
|
{ tradeId: event.tradeId, orderId: event.orderId }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -613,7 +627,8 @@ async function main() {
|
|||||||
emitSimpleSetupEvent(
|
emitSimpleSetupEvent(
|
||||||
simpleExitEntry,
|
simpleExitEntry,
|
||||||
`${event.symbol} setup completed. The linked position is closed.`,
|
`${event.symbol} setup completed. The linked position is closed.`,
|
||||||
'INFO'
|
'INFO',
|
||||||
|
{ tradeId: event.tradeId, orderId: event.orderId }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -211,6 +211,9 @@ export class ObservabilityService {
|
|||||||
profileId?: string;
|
profileId?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
symbol?: string;
|
symbol?: string;
|
||||||
|
setupId?: string;
|
||||||
|
tradeId?: string;
|
||||||
|
orderId?: string;
|
||||||
}) {
|
}) {
|
||||||
const event: OperationalEvent = {
|
const event: OperationalEvent = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
|
|||||||
@ -181,6 +181,9 @@ export interface BotState {
|
|||||||
profileId?: string;
|
profileId?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
symbol?: string;
|
symbol?: string;
|
||||||
|
setupId?: string;
|
||||||
|
tradeId?: string;
|
||||||
|
orderId?: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,8 @@ type SimpleRuntimeSnapshot = {
|
|||||||
orderId?: string;
|
orderId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SimpleOperationalEvent = NonNullable<ReturnType<typeof useAppContext>['botState']['operationalEvents']>[number];
|
||||||
|
|
||||||
type MarketPriceSource = 'live' | 'latest_close' | 'reference_price' | null;
|
type MarketPriceSource = 'live' | 'latest_close' | 'reference_price' | null;
|
||||||
|
|
||||||
const SIMPLE_AUTO_PROFILE_NAME = 'Simple Auto Profile';
|
const SIMPLE_AUTO_PROFILE_NAME = 'Simple Auto Profile';
|
||||||
@ -424,6 +426,46 @@ function formatSetupUpdatedAt(entry: ManualEntryPayload): string | null {
|
|||||||
return parsed.toLocaleString();
|
return parsed.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatEventTimestamp(timestamp?: number): string | null {
|
||||||
|
if (!timestamp || !Number.isFinite(timestamp)) return null;
|
||||||
|
const parsed = new Date(timestamp);
|
||||||
|
if (Number.isNaN(parsed.getTime())) return null;
|
||||||
|
return parsed.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveSimpleEventHistory(
|
||||||
|
entry: ManualEntryPayload,
|
||||||
|
operationalEvents: SimpleOperationalEvent[],
|
||||||
|
): SimpleOperationalEvent[] {
|
||||||
|
const setupId = String(entry.stock_instance_id || '').trim();
|
||||||
|
const tradeId = String(entry.linked_trade_id || '').trim();
|
||||||
|
const symbol = String(entry.symbol || '').trim().toUpperCase();
|
||||||
|
|
||||||
|
return operationalEvents
|
||||||
|
.filter((event): event is SimpleOperationalEvent => Boolean(event) && event.type === 'SIMPLE_SETUP_UPDATE')
|
||||||
|
.filter((event) => {
|
||||||
|
const eventSetupId = String(event.setupId || '').trim();
|
||||||
|
const eventTradeId = String(event.tradeId || '').trim();
|
||||||
|
const eventSymbol = String(event.symbol || '').trim().toUpperCase();
|
||||||
|
if (setupId && eventSetupId === setupId) return true;
|
||||||
|
if (tradeId && eventTradeId === tradeId) return true;
|
||||||
|
return !setupId && !tradeId && !!symbol && eventSymbol === symbol;
|
||||||
|
})
|
||||||
|
.sort((left, right) => right.timestamp - left.timestamp)
|
||||||
|
.slice(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventSeverityClasses(severity?: string) {
|
||||||
|
const normalized = String(severity || '').trim().toUpperCase();
|
||||||
|
if (normalized === 'ERROR') {
|
||||||
|
return 'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-300';
|
||||||
|
}
|
||||||
|
if (normalized === 'WARN') {
|
||||||
|
return 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300';
|
||||||
|
}
|
||||||
|
return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:text-sky-300';
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSimpleEntries(entries: ManualEntryPayload[]): ManualEntryPayload[] {
|
function normalizeSimpleEntries(entries: ManualEntryPayload[]): ManualEntryPayload[] {
|
||||||
return entries
|
return entries
|
||||||
.filter((entry) => String(entry.workflow_type || '').trim().toLowerCase() === 'simple')
|
.filter((entry) => String(entry.workflow_type || '').trim().toLowerCase() === 'simple')
|
||||||
@ -550,6 +592,10 @@ export function SimpleView() {
|
|||||||
return String(order.profileId || '').trim() === simpleAutoProfile.id;
|
return String(order.profileId || '').trim() === simpleAutoProfile.id;
|
||||||
});
|
});
|
||||||
}, [botState?.orders, simpleAutoProfile?.id]);
|
}, [botState?.orders, simpleAutoProfile?.id]);
|
||||||
|
const runtimeEvents = useMemo(
|
||||||
|
() => normalizeRuntimeArray<SimpleOperationalEvent>(botState?.operationalEvents),
|
||||||
|
[botState?.operationalEvents],
|
||||||
|
);
|
||||||
|
|
||||||
const matchingHolding = useMemo(
|
const matchingHolding = useMemo(
|
||||||
() => simpleHoldings.find((holding) => holding.symbol === normalizedSymbol) || null,
|
() => simpleHoldings.find((holding) => holding.symbol === normalizedSymbol) || null,
|
||||||
@ -1092,6 +1138,7 @@ export function SimpleView() {
|
|||||||
const runtimeSnapshot = deriveRuntimeSnapshot(entry, runtimeOrders, simpleHoldings);
|
const runtimeSnapshot = deriveRuntimeSnapshot(entry, runtimeOrders, simpleHoldings);
|
||||||
const nextActionText = describeNextAction(entry, runtimeSnapshot);
|
const nextActionText = describeNextAction(entry, runtimeSnapshot);
|
||||||
const updatedAt = formatSetupUpdatedAt(entry);
|
const updatedAt = formatSetupUpdatedAt(entry);
|
||||||
|
const eventHistory = deriveSimpleEventHistory(entry, runtimeEvents);
|
||||||
return (
|
return (
|
||||||
<div key={entryId} className="rounded-[1.5rem] border border-[var(--border)] bg-[var(--card-elevated)] p-5">
|
<div key={entryId} className="rounded-[1.5rem] border border-[var(--border)] bg-[var(--card-elevated)] p-5">
|
||||||
<div className="mb-3 flex items-start justify-between gap-4">
|
<div className="mb-3 flex items-start justify-between gap-4">
|
||||||
@ -1212,6 +1259,36 @@ export function SimpleView() {
|
|||||||
<span className="font-semibold text-[var(--foreground)]">Next action:</span>{' '}
|
<span className="font-semibold text-[var(--foreground)]">Next action:</span>{' '}
|
||||||
{nextActionText}
|
{nextActionText}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{eventHistory.length > 0 && (
|
||||||
|
<div className="mt-4 rounded-2xl border border-[var(--border)] bg-[var(--background)] px-4 py-4">
|
||||||
|
<div className="mb-3 flex items-center justify-between gap-3">
|
||||||
|
<div className="text-sm font-semibold text-[var(--foreground)]">Recent activity</div>
|
||||||
|
<div className="text-[11px] uppercase tracking-[0.18em] text-[var(--muted-foreground)]">
|
||||||
|
setup-level runtime history
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{eventHistory.map((event) => (
|
||||||
|
<div key={event.id} className={`rounded-2xl border px-3 py-3 ${eventSeverityClasses(event.severity)}`}>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="text-[11px] font-black uppercase tracking-[0.18em]">
|
||||||
|
{String(event.severity || 'INFO').toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] opacity-80">
|
||||||
|
{formatEventTimestamp(event.timestamp) || 'Just now'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-sm leading-6">{event.message}</div>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2 text-[11px] uppercase tracking-[0.14em] opacity-80">
|
||||||
|
{event.tradeId ? <span>Trade {event.tradeId.slice(0, 18)}…</span> : null}
|
||||||
|
{event.orderId ? <span>Order {event.orderId.slice(0, 12)}…</span> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user