feat(simple): add setup activity history
This commit is contained in:
parent
7de6b236c0
commit
0b526f3499
@ -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;
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -181,6 +181,9 @@ export interface BotState {
|
||||
profileId?: string;
|
||||
userId?: string;
|
||||
symbol?: string;
|
||||
setupId?: string;
|
||||
tradeId?: string;
|
||||
orderId?: string;
|
||||
timestamp: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user