feat(simple): add setup activity history

This commit is contained in:
root 2026-05-06 17:23:45 +00:00
parent 7de6b236c0
commit 0b526f3499
5 changed files with 119 additions and 18 deletions

View File

@ -12,13 +12,16 @@ export type OperationalEventType =
export type OperationalEventSeverity = 'INFO' | 'WARN' | 'ERROR';
export interface OperationalEvent {
id: string;
type: OperationalEventType;
severity: OperationalEventSeverity;
message: string;
profileId?: string;
userId?: string;
symbol?: string;
timestamp: number;
}
export interface OperationalEvent {
id: string;
type: OperationalEventType;
severity: OperationalEventSeverity;
message: string;
profileId?: string;
userId?: string;
symbol?: string;
setupId?: string;
tradeId?: string;
orderId?: string;
timestamp: number;
}

View File

@ -246,7 +246,12 @@ async function main() {
const livePrice = Number(apiServer.getState().symbols?.[symbol]?.price || 0);
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({
type: 'SIMPLE_SETUP_UPDATE',
severity,
@ -254,6 +259,9 @@ async function main() {
profileId: String(entry.profile_id || '').trim() || undefined,
userId: String(entry.user_id || '').trim() || 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> => {
@ -364,7 +372,8 @@ async function main() {
linked_trade_id: result.tradeId || entry.linked_trade_id,
},
`${symbol} entry submitted${result.adjustedQty ? ` for ${result.adjustedQty}` : ''}. Waiting for fill confirmation.`,
'INFO'
'INFO',
{ tradeId: result.tradeId, orderId: result.orderId }
);
continue;
}
@ -455,7 +464,8 @@ async function main() {
linked_trade_id: linkedTradeId || activePosition.tradeId,
},
`${symbol} exit submitted after hitting the configured profit target. Waiting for fill confirmation.`,
'INFO'
'INFO',
{ tradeId: linkedTradeId || activePosition.tradeId }
);
}
} catch (error: any) {
@ -513,7 +523,8 @@ async function main() {
profile_id: simpleEntry.profile_id || event.profileId || null,
},
`${event.symbol} entry filled. Monitoring for the configured profit exit.`,
'INFO'
'INFO',
{ tradeId: event.tradeId, orderId: event.orderId }
);
}
return;
@ -531,7 +542,8 @@ async function main() {
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'
normalizedStatus === 'rejected' || normalizedStatus === 'unknown' ? 'WARN' : 'INFO',
{ tradeId: event.tradeId, orderId: event.orderId }
);
}
return;
@ -550,7 +562,8 @@ async function main() {
emitSimpleSetupEvent(
simpleExitEntry,
`${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;
@ -596,7 +609,8 @@ async function main() {
emitSimpleSetupEvent(
simpleExitEntry,
`${event.symbol} exit partially filled. Remaining size is still being monitored for the profit target.`,
'INFO'
'INFO',
{ tradeId: event.tradeId, orderId: event.orderId }
);
}
return;
@ -613,7 +627,8 @@ async function main() {
emitSimpleSetupEvent(
simpleExitEntry,
`${event.symbol} setup completed. The linked position is closed.`,
'INFO'
'INFO',
{ tradeId: event.tradeId, orderId: event.orderId }
);
}
};

View File

@ -211,6 +211,9 @@ export class ObservabilityService {
profileId?: string;
userId?: string;
symbol?: string;
setupId?: string;
tradeId?: string;
orderId?: string;
}) {
const event: OperationalEvent = {
id: crypto.randomUUID(),

View File

@ -181,6 +181,9 @@ export interface BotState {
profileId?: string;
userId?: string;
symbol?: string;
setupId?: string;
tradeId?: string;
orderId?: string;
timestamp: number;
}>;
}

View File

@ -50,6 +50,8 @@ type SimpleRuntimeSnapshot = {
orderId?: string;
};
type SimpleOperationalEvent = NonNullable<ReturnType<typeof useAppContext>['botState']['operationalEvents']>[number];
type MarketPriceSource = 'live' | 'latest_close' | 'reference_price' | null;
const SIMPLE_AUTO_PROFILE_NAME = 'Simple Auto Profile';
@ -424,6 +426,46 @@ function formatSetupUpdatedAt(entry: ManualEntryPayload): string | null {
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[] {
return entries
.filter((entry) => String(entry.workflow_type || '').trim().toLowerCase() === 'simple')
@ -550,6 +592,10 @@ export function SimpleView() {
return String(order.profileId || '').trim() === simpleAutoProfile.id;
});
}, [botState?.orders, simpleAutoProfile?.id]);
const runtimeEvents = useMemo(
() => normalizeRuntimeArray<SimpleOperationalEvent>(botState?.operationalEvents),
[botState?.operationalEvents],
);
const matchingHolding = useMemo(
() => simpleHoldings.find((holding) => holding.symbol === normalizedSymbol) || null,
@ -1092,6 +1138,7 @@ export function SimpleView() {
const runtimeSnapshot = deriveRuntimeSnapshot(entry, runtimeOrders, simpleHoldings);
const nextActionText = describeNextAction(entry, runtimeSnapshot);
const updatedAt = formatSetupUpdatedAt(entry);
const eventHistory = deriveSimpleEventHistory(entry, runtimeEvents);
return (
<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">
@ -1212,6 +1259,36 @@ export function SimpleView() {
<span className="font-semibold text-[var(--foreground)]">Next action:</span>{' '}
{nextActionText}
</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>
);
})}