diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 0e17fe2..85a273a 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -98,7 +98,7 @@ export const config = { COOLDOWN_MS: parseInt(process.env.COOLDOWN_MS || '3600000', 10), // Default 1 hour PROFIT_EXIT_PERCENT: parseFloat(process.env.PROFIT_EXIT_PERCENT || '1.0'), // Default 1% TRAILING_STOP_PERCENT: parseFloat(process.env.TRAILING_STOP_PERCENT || '0.001'), // Default 0.1% - PROFILE_SYNC_INTERVAL_MS: parseInt(process.env.PROFILE_SYNC_INTERVAL_MS || '60000', 10), // Default 1 min + PROFILE_SYNC_INTERVAL_MS: parseInt(process.env.PROFILE_SYNC_INTERVAL_MS || '5000', 10), // Default 5 sec MONITOR_INTERVAL_MS: parseInt(process.env.MONITOR_INTERVAL_MS || '60000', 10), // Default 1 min ORDER_SYNC_INTERVAL_MS: parseInt(process.env.ORDER_SYNC_INTERVAL_MS || '60000', 10), // Default 1 min STALE_ORDER_THRESHOLD_MINUTES: parseInt(process.env.STALE_ORDER_THRESHOLD_MINUTES || '2', 10), // Default 2 min diff --git a/web/src/views/SimpleView.test.ts b/web/src/views/SimpleView.test.ts index 87a09b4..6587d3d 100644 --- a/web/src/views/SimpleView.test.ts +++ b/web/src/views/SimpleView.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { buildSimpleTradePayload, computeProtectionPrices } from './SimpleView'; +import { + buildSimpleAutoProfilePayload, + buildSimpleTradePayload, + computeProtectionPrices, + SIMPLE_AUTO_PROFILE_NAME, +} from './SimpleView'; describe('SimpleView helpers', () => { it('computes buy-side protection prices from reference price', () => { @@ -70,4 +75,22 @@ describe('SimpleView helpers', () => { tp: 60160, }); }); + + it('builds the dedicated simple auto profile payload', () => { + expect(buildSimpleAutoProfilePayload('msft')).toEqual({ + name: SIMPLE_AUTO_PROFILE_NAME, + allocated_capital: 1000, + risk_per_trade_percent: 1, + symbols: 'MSFT', + is_active: true, + strategy_config: { + mode: 'simple-auto', + source: 'simple-tab', + execution: { + orderType: 'market', + entryMode: 'manual', + }, + }, + }); + }); }); diff --git a/web/src/views/SimpleView.tsx b/web/src/views/SimpleView.tsx index ad1e6d3..fbb92e9 100644 --- a/web/src/views/SimpleView.tsx +++ b/web/src/views/SimpleView.tsx @@ -5,7 +5,12 @@ import { fetchChartBars } from '../lib/marketApi'; import { getPlatformAccessToken } from '../lib/authSession'; import { tradingRuntime } from '../lib/runtime'; import { createRequestId } from '../../../shared/request-id.js'; -import { fetchTradeProfiles, type TradeProfilePayload } from '../lib/profileApi'; +import { + createTradeProfile, + fetchTradeProfiles, + setTradeProfileActive, + type TradeProfilePayload, +} from '../lib/profileApi'; type SimpleSide = 'buy' | 'sell'; type SimpleOrderType = 'market' | 'limit'; @@ -45,6 +50,11 @@ const DEFAULT_DRAFT: SimpleTradeDraft = { isCrypto: false, }; +export const SIMPLE_AUTO_PROFILE_NAME = 'Simple Auto Profile'; +const SIMPLE_AUTO_PROFILE_KEY = SIMPLE_AUTO_PROFILE_NAME.toLowerCase(); +const SIMPLE_PROFILE_RETRY_DELAY_MS = 2000; +const SIMPLE_PROFILE_RETRY_ATTEMPTS = 8; + function parsePositiveNumber(value: string): number | null { const trimmed = value.trim(); if (!trimmed) return null; @@ -56,6 +66,34 @@ function roundPrice(value: number): number { return Number(value.toFixed(4)); } +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function matchesSimpleAutoProfile(profile: Pick | null | undefined) { + return String(profile?.name || '').trim().toLowerCase() === SIMPLE_AUTO_PROFILE_KEY; +} + +export function buildSimpleAutoProfilePayload(symbol: string): TradeProfilePayload { + const normalizedSymbol = symbol.trim().toUpperCase(); + + return { + name: SIMPLE_AUTO_PROFILE_NAME, + allocated_capital: 1000, + risk_per_trade_percent: 1, + symbols: normalizedSymbol || 'AAPL', + is_active: true, + strategy_config: { + mode: 'simple-auto', + source: 'simple-tab', + execution: { + orderType: 'market', + entryMode: 'manual', + }, + }, + }; +} + export function computeProtectionPrices(input: { side: SimpleSide; referencePrice: number; @@ -147,7 +185,6 @@ function buildExecutionPreview(payload: SimpleTradePayload | null) { export function SimpleView() { const { botState } = useAppContext(); const [profiles, setProfiles] = useState([]); - const [selectedProfileId, setSelectedProfileId] = useState(''); const [draft, setDraft] = useState(DEFAULT_DRAFT); const [submitting, setSubmitting] = useState(false); const [loadingPrice, setLoadingPrice] = useState(false); @@ -158,8 +195,8 @@ export function SimpleView() { const livePrice = normalizedSymbol ? Number(botState.symbols?.[normalizedSymbol]?.price || 0) : 0; const resolvedMarketPrice = parsePositiveNumber(draft.currentMarketPrice); const marketPriceSource = livePrice > 0 ? 'live' : (resolvedMarketPrice !== null ? 'recent_close' : null); - const activeProfiles = useMemo( - () => profiles.filter((profile) => Boolean(profile.is_active)), + const simpleAutoProfile = useMemo( + () => profiles.find((profile) => matchesSimpleAutoProfile(profile)) || null, [profiles], ); @@ -183,19 +220,6 @@ export function SimpleView() { }; }, []); - useEffect(() => { - if (!activeProfiles.length) { - setSelectedProfileId(''); - return; - } - - if (selectedProfileId && activeProfiles.some((profile) => profile.id === selectedProfileId)) { - return; - } - - setSelectedProfileId(String(activeProfiles[0]?.id || '')); - }, [activeProfiles, selectedProfileId]); - useEffect(() => { if (!livePrice) return; setDraft(prev => ( @@ -230,6 +254,54 @@ export function SimpleView() { setDraft(prev => ({ ...prev, [key]: value })); } + async function ensureSimpleAutoProfile(symbol: string) { + const refreshProfiles = async () => { + const rows = await fetchTradeProfiles(); + setProfiles(rows); + return rows; + }; + + let rows = await refreshProfiles(); + let profile = rows.find((row) => matchesSimpleAutoProfile(row)) || null; + + if (!profile) { + profile = await createTradeProfile(buildSimpleAutoProfilePayload(symbol)); + rows = await refreshProfiles(); + profile = rows.find((row) => row.id === profile?.id) || profile; + } + + if (!profile?.is_active && profile?.id) { + profile = await setTradeProfileActive(profile.id, true); + rows = await refreshProfiles(); + profile = rows.find((row) => row.id === profile?.id) || profile; + } + + if (!profile?.id) { + throw new Error('Failed to prepare the Simple Auto Profile'); + } + + return profile; + } + + async function submitTradeRequest(accessToken: string, payload: SimpleTradePayload) { + const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/trade`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + 'x-request-id': createRequestId('web-simple-trade'), + }, + body: JSON.stringify(payload), + }); + + const result = await response.json().catch(() => ({})); + if (!response.ok || !result?.success) { + throw new Error(result?.error || `Trade request failed (${response.status})`); + } + + return result; + } + async function handleLoadMarketPrice() { if (!normalizedSymbol) { setError('Enter a symbol first'); @@ -262,31 +334,28 @@ export function SimpleView() { setMessage(null); try { - if (!activeProfiles.length) { - throw new Error('No active trade profile available. Activate a profile first.'); - } - const payload = buildSimpleTradePayload(draft); - if (selectedProfileId) { - payload.profile_id = selectedProfileId; - } + const simpleProfile = await ensureSimpleAutoProfile(payload.symbol); + payload.profile_id = String(simpleProfile.id); const accessToken = await getPlatformAccessToken(); - const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/trade`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - 'x-request-id': createRequestId('web-simple-trade'), - }, - body: JSON.stringify(payload), - }); + let result: any = null; - const result = await response.json().catch(() => ({})); - if (!response.ok || !result?.success) { - throw new Error(result?.error || `Trade request failed (${response.status})`); + for (let attempt = 1; attempt <= SIMPLE_PROFILE_RETRY_ATTEMPTS; attempt += 1) { + try { + result = await submitTradeRequest(accessToken, payload); + break; + } catch (err: any) { + const message = String(err?.message || ''); + const isManagerWarmup = message.includes('No Manual Trader available'); + if (!isManagerWarmup || attempt === SIMPLE_PROFILE_RETRY_ATTEMPTS) { + throw err; + } + setMessage(`Preparing ${SIMPLE_AUTO_PROFILE_NAME}… retrying order submission (${attempt}/${SIMPLE_PROFILE_RETRY_ATTEMPTS - 1})`); + await sleep(SIMPLE_PROFILE_RETRY_DELAY_MS); + } } - setMessage(`Order submitted${result.orderId ? ` · ${result.orderId}` : ''}`); + setMessage(`Order submitted via ${SIMPLE_AUTO_PROFILE_NAME}${result.orderId ? ` · ${result.orderId}` : ''}`); setDraft(prev => ({ ...DEFAULT_DRAFT, symbol: prev.symbol, @@ -322,7 +391,7 @@ export function SimpleView() { Submit a simple buy or sell without the full strategy builder
- This page sends a direct trade request to your configured trading profile. Market price is sourced automatically from live data or the most recent close. + This page sends direct orders through one dedicated auto-created profile. Your first order prepares that profile automatically, and every later Simple order reuses it.
@@ -363,23 +432,6 @@ export function SimpleView() { - -