fix(simple): allow immediate buy triggers
This commit is contained in:
parent
a436fa61e5
commit
e01f38c883
@ -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
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user