feat(simple): improve setup status and market fallback
This commit is contained in:
parent
92747b76a7
commit
fc4d4c85d1
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { Pencil, RefreshCw, Trash2 } from 'lucide-react';
|
||||
import { useAppContext } from '../context/AppContext';
|
||||
import { fetchChartBars } from '../lib/marketApi';
|
||||
import { fetchChartBars, fetchResearchProfile } from '../lib/marketApi';
|
||||
import {
|
||||
createManualEntry,
|
||||
deleteManualEntry,
|
||||
@ -42,6 +42,14 @@ type SimpleHolding = {
|
||||
tradeId?: string;
|
||||
};
|
||||
|
||||
type SimpleRuntimeSnapshot = {
|
||||
stage: 'armed' | 'entry_submitted' | 'filled' | 'exit_submitted' | 'closed' | 'unknown';
|
||||
label: string;
|
||||
tone: 'neutral' | 'info' | 'success';
|
||||
tradeId?: string;
|
||||
orderId?: string;
|
||||
};
|
||||
|
||||
const SIMPLE_AUTO_PROFILE_NAME = 'Simple Auto Profile';
|
||||
const SIMPLE_AUTO_PROFILE_KEY = SIMPLE_AUTO_PROFILE_NAME.toLowerCase();
|
||||
const SIMPLE_SYMBOL_DATALIST_ID = 'simple-supported-symbols';
|
||||
@ -247,27 +255,44 @@ function buildPreviewText(draft: SimpleSetupDraft, holding: SimpleHolding | null
|
||||
return `Exit ${symbol} when price reaches ${profitTargetPrice.toFixed(4)} (${profitText}).`;
|
||||
}
|
||||
|
||||
function deriveRuntimeOrderStatus(
|
||||
function deriveRuntimeSnapshot(
|
||||
entry: ManualEntryPayload,
|
||||
orders: Array<Record<string, any>>,
|
||||
holdings: SimpleHolding[],
|
||||
): string | null {
|
||||
): SimpleRuntimeSnapshot | null {
|
||||
const linkedTradeId = String(entry.linked_trade_id || '').trim();
|
||||
if (linkedTradeId) {
|
||||
const matchingOrder = orders.find((order) => String(order?.trade_id || order?.tradeId || '').trim() === linkedTradeId);
|
||||
if (matchingOrder) {
|
||||
const status = String(matchingOrder.status || '').trim();
|
||||
return status ? `Order: ${status}` : null;
|
||||
const status = String(matchingOrder.status || '').trim().toLowerCase();
|
||||
const action = String(matchingOrder.action || '').trim().toUpperCase();
|
||||
const orderId = String(matchingOrder.orderId || matchingOrder.order_id || matchingOrder.id || '').trim() || undefined;
|
||||
if (action === 'EXIT') {
|
||||
if (status === 'filled') {
|
||||
return { stage: 'closed', label: 'Exit filled', tone: 'success', tradeId: linkedTradeId, orderId };
|
||||
}
|
||||
return { stage: 'exit_submitted', label: `Exit ${status || 'submitted'}`, tone: 'info', tradeId: linkedTradeId, orderId };
|
||||
}
|
||||
if (status === 'filled') {
|
||||
return { stage: 'filled', label: 'Entry filled', tone: 'success', tradeId: linkedTradeId, orderId };
|
||||
}
|
||||
return { stage: 'entry_submitted', label: `Entry ${status || 'submitted'}`, tone: 'info', tradeId: linkedTradeId, orderId };
|
||||
}
|
||||
}
|
||||
|
||||
const symbol = String(entry.symbol || '').trim().toUpperCase();
|
||||
const side = normalizeSetupSide(entry.simple_side);
|
||||
if (side === 'buy' && holdings.some((holding) => holding.symbol === symbol)) {
|
||||
return 'Portfolio: open holding';
|
||||
const matchingHolding = holdings.find((holding) => holding.symbol === symbol && (!linkedTradeId || holding.tradeId === linkedTradeId || !holding.tradeId));
|
||||
if (side === 'buy' && matchingHolding) {
|
||||
return {
|
||||
stage: 'filled',
|
||||
label: 'Portfolio holding open',
|
||||
tone: 'success',
|
||||
tradeId: matchingHolding.tradeId || linkedTradeId || undefined,
|
||||
};
|
||||
}
|
||||
if (String(entry.status || '').trim().toLowerCase() === 'sellcompleted') {
|
||||
return 'Portfolio: closed';
|
||||
return { stage: 'closed', label: 'Position closed', tone: 'success', tradeId: linkedTradeId || undefined };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -308,6 +333,25 @@ function formatSetupStatus(status?: string | null): string {
|
||||
}
|
||||
}
|
||||
|
||||
function statusToneClasses(tone: SimpleRuntimeSnapshot['tone']): string {
|
||||
switch (tone) {
|
||||
case 'success':
|
||||
return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
|
||||
case 'info':
|
||||
return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:text-sky-300';
|
||||
default:
|
||||
return 'border-[var(--border)] text-[var(--muted-foreground)]';
|
||||
}
|
||||
}
|
||||
|
||||
function formatSetupUpdatedAt(entry: ManualEntryPayload): string | null {
|
||||
const raw = String(entry.sell_time || entry.buy_time || '').trim();
|
||||
if (!raw) return null;
|
||||
const parsed = new Date(raw);
|
||||
if (Number.isNaN(parsed.getTime())) return null;
|
||||
return parsed.toLocaleString();
|
||||
}
|
||||
|
||||
function normalizeSimpleEntries(entries: ManualEntryPayload[]): ManualEntryPayload[] {
|
||||
return entries
|
||||
.filter((entry) => String(entry.workflow_type || '').trim().toLowerCase() === 'simple')
|
||||
@ -339,6 +383,23 @@ function isLikelySymbolCandidate(symbol: string): boolean {
|
||||
return /^[A-Z0-9./-]{1,20}$/.test(symbol);
|
||||
}
|
||||
|
||||
function extractReferencePriceFromResearchProfile(profile: any): number | null {
|
||||
const candidates = [
|
||||
profile?.price,
|
||||
profile?.lastPrice,
|
||||
profile?.currentPrice,
|
||||
profile?.previousClose,
|
||||
profile?.prevClose,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const value = Number(candidate);
|
||||
if (Number.isFinite(value) && value > 0) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function describeSavedSetup(entry: ManualEntryPayload): string {
|
||||
const side = normalizeSetupSide(entry.simple_side);
|
||||
const symbol = String(entry.symbol || '').trim().toUpperCase();
|
||||
@ -535,7 +596,13 @@ export function SimpleView() {
|
||||
updateDraft('currentMarketPrice', lastClose.toFixed(4));
|
||||
}
|
||||
} else {
|
||||
throw new Error('No recent market data is available for this symbol right now.');
|
||||
const researchProfile = await fetchResearchProfile(requestSymbol).catch(() => null);
|
||||
const fallbackReferencePrice = extractReferencePriceFromResearchProfile(researchProfile);
|
||||
if (fallbackReferencePrice && marketPriceRequestSymbolRef.current === requestSymbol) {
|
||||
updateDraft('currentMarketPrice', fallbackReferencePrice.toFixed(4));
|
||||
return;
|
||||
}
|
||||
throw new Error('No live price or latest close is available for this symbol right now.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (marketPriceRequestSymbolRef.current === requestSymbol) {
|
||||
@ -899,7 +966,8 @@ export function SimpleView() {
|
||||
const entryId = String(entry.stock_instance_id || '');
|
||||
const side = normalizeSetupSide(entry.simple_side);
|
||||
const isEditing = editingSetupId === entryId;
|
||||
const runtimeStatus = deriveRuntimeOrderStatus(entry, runtimeOrders, simpleHoldings);
|
||||
const runtimeSnapshot = deriveRuntimeSnapshot(entry, runtimeOrders, simpleHoldings);
|
||||
const updatedAt = formatSetupUpdatedAt(entry);
|
||||
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">
|
||||
@ -946,6 +1014,16 @@ export function SimpleView() {
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
{formatSetupStatus(entry.status)}
|
||||
</span>
|
||||
{runtimeSnapshot ? (
|
||||
<span className={`rounded-full border px-3 py-1 ${statusToneClasses(runtimeSnapshot.tone)}`}>
|
||||
{runtimeSnapshot.label}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
{String(entry.sizing_mode || '').trim().toLowerCase() === 'amount'
|
||||
? `Budget $${Number(entry.amount_usd || 0).toFixed(2)}`
|
||||
: `Qty ${entry.quantity || entry.filled_quantity || 0}`}
|
||||
</span>
|
||||
{entry.reference_price ? (
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
Ref {Number(entry.reference_price).toFixed(4)}
|
||||
@ -956,14 +1034,19 @@ export function SimpleView() {
|
||||
Entry {Number(entry.entry_price).toFixed(4)}
|
||||
</span>
|
||||
) : null}
|
||||
{entry.linked_trade_id ? (
|
||||
{(runtimeSnapshot?.tradeId || entry.linked_trade_id) ? (
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
Trade linked
|
||||
Trade {String(runtimeSnapshot?.tradeId || entry.linked_trade_id).slice(0, 18)}…
|
||||
</span>
|
||||
) : null}
|
||||
{runtimeStatus ? (
|
||||
{runtimeSnapshot?.orderId ? (
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
{runtimeStatus}
|
||||
Order {runtimeSnapshot.orderId.slice(0, 12)}…
|
||||
</span>
|
||||
) : null}
|
||||
{updatedAt ? (
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1 normal-case tracking-normal">
|
||||
Updated {updatedAt}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user