fix(simple): allow immediate buy triggers

This commit is contained in:
root 2026-05-06 06:09:11 +00:00
parent a436fa61e5
commit e01f38c883
2 changed files with 36 additions and 17 deletions

View File

@ -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
);

View File

@ -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() {
<Input
value={draft.dropValue}
onChange={(e) => updateDraft('dropValue', e.target.value)}
placeholder={draft.dropMode === 'dollar' ? '5.00' : '8'}
placeholder={draft.dropMode === 'dollar' ? '0.00' : '0'}
/>
</label>
</div>