diff --git a/backend/src/index.ts b/backend/src/index.ts index 7abbdfa..dfabd52 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -31,6 +31,11 @@ const toPositiveNumber = (value: unknown): number | null => { return Number.isFinite(numeric) && numeric > 0 ? numeric : null; }; +const toNonNegativeNumber = (value: unknown): number | null => { + const numeric = Number(value); + return Number.isFinite(numeric) && numeric >= 0 ? numeric : null; +}; + const normalizeTriggerMode = (value: unknown): 'dollar' | 'percent' | null => { const normalized = String(value || '').trim().toLowerCase(); if (normalized === 'dollar' || normalized === 'percent') return normalized; @@ -71,9 +76,9 @@ const shouldArmSimpleBuy = (entry: ManualEntryRecord): boolean => { const computeSimpleBuyTriggerPrice = (entry: ManualEntryRecord): number | null => { const referencePrice = toPositiveNumber(entry.reference_price); - const threshold = toPositiveNumber(entry.drop_threshold_for_buy); + const threshold = toNonNegativeNumber(entry.drop_threshold_for_buy); const mode = normalizeTriggerMode(entry.drop_trigger_mode); - if (!referencePrice || !threshold || !mode) return null; + if (!referencePrice || threshold === null || !mode) return null; if (mode === 'dollar') { const triggerPrice = referencePrice - threshold; @@ -238,6 +243,7 @@ async function main() { if (shouldArmSimpleBuy(entry)) { const triggerPrice = computeSimpleBuyTriggerPrice(entry); const desiredQty = toPositiveNumber(entry.quantity); + const threshold = toNonNegativeNumber(entry.drop_threshold_for_buy); if (!triggerPrice || !desiredQty) continue; if (currentPrice > triggerPrice) continue; @@ -245,8 +251,8 @@ async function main() { symbol, 'buy', desiredQty, - 'limit', - triggerPrice, + threshold === 0 ? 'market' : 'limit', + threshold === 0 ? undefined : triggerPrice, currentPrice, entry.user_id ); diff --git a/web/src/views/SimpleView.tsx b/web/src/views/SimpleView.tsx index afa6b4d..736de20 100644 --- a/web/src/views/SimpleView.tsx +++ b/web/src/views/SimpleView.tsx @@ -62,6 +62,13 @@ function parsePositiveNumber(value: string): number | null { return Number.isFinite(parsed) && parsed > 0 ? parsed : null; } +function parseNonNegativeNumber(value: string): number | null { + const trimmed = value.trim(); + if (!trimmed) return null; + const parsed = Number(trimmed); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : null; +} + function roundPrice(value: number): number { return Number(value.toFixed(4)); } @@ -80,8 +87,8 @@ function normalizeMode(value: unknown, fallback: TriggerMode = 'percent'): Trigg function computeBuyTriggerPrice(draft: SimpleSetupDraft): number | null { const currentMarketPrice = parsePositiveNumber(draft.currentMarketPrice); - const dropValue = parsePositiveNumber(draft.dropValue); - if (!currentMarketPrice || !dropValue) return null; + const dropValue = parseNonNegativeNumber(draft.dropValue); + if (!currentMarketPrice || dropValue === null) return null; if (draft.dropMode === 'dollar') { const trigger = currentMarketPrice - dropValue; @@ -136,7 +143,7 @@ export function buildSimpleSetupPayload(input: { throw new Error('Sell setups require an existing Simple holding for this symbol.'); } - if (side === 'buy' && !parsePositiveNumber(input.draft.dropValue)) { + if (side === 'buy' && parseNonNegativeNumber(input.draft.dropValue) === null) { throw new Error('Drop trigger is required for buy setups'); } @@ -154,7 +161,7 @@ export function buildSimpleSetupPayload(input: { entry_price: side === 'sell' ? holding!.entryPrice : null, reference_price: currentMarketPrice, gain_threshold_for_sell: profitValue, - drop_threshold_for_buy: side === 'buy' ? parsePositiveNumber(input.draft.dropValue) : null, + drop_threshold_for_buy: side === 'buy' ? parseNonNegativeNumber(input.draft.dropValue) : null, workflow_type: 'simple', simple_side: side, drop_trigger_mode: side === 'buy' ? input.draft.dropMode : null, @@ -177,9 +184,13 @@ function buildPreviewText(draft: SimpleSetupDraft, holding: SimpleHolding | null const profitTargetPrice = computeProfitTargetPrice(triggerPrice, draft.profitMode, draft.profitValue); if (!triggerPrice) return `Buy ${symbol} after the configured drop trigger is hit.`; - const dropText = draft.dropMode === 'dollar' - ? `$${Number(draft.dropValue || 0).toFixed(2)} below current price` - : `${draft.dropValue || '0'}% below current price`; + const dropMagnitude = parseNonNegativeNumber(draft.dropValue); + const isImmediate = dropMagnitude === 0; + const dropText = isImmediate + ? 'at the current market reference' + : draft.dropMode === 'dollar' + ? `$${Number(draft.dropValue || 0).toFixed(2)} below current price` + : `${draft.dropValue || '0'}% below current price`; const profitText = draft.profitMode === 'dollar' ? `$${Number(draft.profitValue || 0).toFixed(2)} above purchase` : `${draft.profitValue || '0'}% above purchase`; @@ -212,9 +223,9 @@ function buildDraftFromEntry(entry: ManualEntryPayload): SimpleSetupDraft { quantity: entry.quantity ? String(entry.quantity) : '', currentMarketPrice: entry.reference_price ? Number(entry.reference_price).toFixed(4) : '', dropMode: normalizeMode(entry.drop_trigger_mode, 'percent'), - dropValue: entry.drop_threshold_for_buy ? String(entry.drop_threshold_for_buy) : '', + dropValue: entry.drop_threshold_for_buy !== null && entry.drop_threshold_for_buy !== undefined ? String(entry.drop_threshold_for_buy) : '', profitMode: normalizeMode(entry.profit_target_mode, 'percent'), - profitValue: entry.gain_threshold_for_sell ? String(entry.gain_threshold_for_sell) : '', + profitValue: entry.gain_threshold_for_sell !== null && entry.gain_threshold_for_sell !== undefined ? String(entry.gain_threshold_for_sell) : '', notes: String(entry.notes || ''), }; } @@ -261,9 +272,11 @@ function describeSavedSetup(entry: ManualEntryPayload): string { if (side === 'buy') { const triggerPrice = computeBuyTriggerPrice(buildDraftFromEntry(entry)); - const dropText = dropMode === 'dollar' - ? `$${dropValue.toFixed(2)} below` - : `${dropValue}% below`; + const dropText = dropValue === 0 + ? 'at current reference' + : dropMode === 'dollar' + ? `$${dropValue.toFixed(2)} below` + : `${dropValue}% below`; const profitText = profitMode === 'dollar' ? `$${profitValue.toFixed(2)} above purchase` : `${profitValue}% above purchase`; @@ -590,7 +603,7 @@ export function SimpleView() { updateDraft('dropValue', e.target.value)} - placeholder={draft.dropMode === 'dollar' ? '5.00' : '8'} + placeholder={draft.dropMode === 'dollar' ? '0.00' : '0'} />