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;
|
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 normalizeTriggerMode = (value: unknown): 'dollar' | 'percent' | null => {
|
||||||
const normalized = String(value || '').trim().toLowerCase();
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
if (normalized === 'dollar' || normalized === 'percent') return normalized;
|
if (normalized === 'dollar' || normalized === 'percent') return normalized;
|
||||||
@ -71,9 +76,9 @@ const shouldArmSimpleBuy = (entry: ManualEntryRecord): boolean => {
|
|||||||
|
|
||||||
const computeSimpleBuyTriggerPrice = (entry: ManualEntryRecord): number | null => {
|
const computeSimpleBuyTriggerPrice = (entry: ManualEntryRecord): number | null => {
|
||||||
const referencePrice = toPositiveNumber(entry.reference_price);
|
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);
|
const mode = normalizeTriggerMode(entry.drop_trigger_mode);
|
||||||
if (!referencePrice || !threshold || !mode) return null;
|
if (!referencePrice || threshold === null || !mode) return null;
|
||||||
|
|
||||||
if (mode === 'dollar') {
|
if (mode === 'dollar') {
|
||||||
const triggerPrice = referencePrice - threshold;
|
const triggerPrice = referencePrice - threshold;
|
||||||
@ -238,6 +243,7 @@ async function main() {
|
|||||||
if (shouldArmSimpleBuy(entry)) {
|
if (shouldArmSimpleBuy(entry)) {
|
||||||
const triggerPrice = computeSimpleBuyTriggerPrice(entry);
|
const triggerPrice = computeSimpleBuyTriggerPrice(entry);
|
||||||
const desiredQty = toPositiveNumber(entry.quantity);
|
const desiredQty = toPositiveNumber(entry.quantity);
|
||||||
|
const threshold = toNonNegativeNumber(entry.drop_threshold_for_buy);
|
||||||
if (!triggerPrice || !desiredQty) continue;
|
if (!triggerPrice || !desiredQty) continue;
|
||||||
if (currentPrice > triggerPrice) continue;
|
if (currentPrice > triggerPrice) continue;
|
||||||
|
|
||||||
@ -245,8 +251,8 @@ async function main() {
|
|||||||
symbol,
|
symbol,
|
||||||
'buy',
|
'buy',
|
||||||
desiredQty,
|
desiredQty,
|
||||||
'limit',
|
threshold === 0 ? 'market' : 'limit',
|
||||||
triggerPrice,
|
threshold === 0 ? undefined : triggerPrice,
|
||||||
currentPrice,
|
currentPrice,
|
||||||
entry.user_id
|
entry.user_id
|
||||||
);
|
);
|
||||||
|
|||||||
@ -62,6 +62,13 @@ function parsePositiveNumber(value: string): number | null {
|
|||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 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 {
|
function roundPrice(value: number): number {
|
||||||
return Number(value.toFixed(4));
|
return Number(value.toFixed(4));
|
||||||
}
|
}
|
||||||
@ -80,8 +87,8 @@ function normalizeMode(value: unknown, fallback: TriggerMode = 'percent'): Trigg
|
|||||||
|
|
||||||
function computeBuyTriggerPrice(draft: SimpleSetupDraft): number | null {
|
function computeBuyTriggerPrice(draft: SimpleSetupDraft): number | null {
|
||||||
const currentMarketPrice = parsePositiveNumber(draft.currentMarketPrice);
|
const currentMarketPrice = parsePositiveNumber(draft.currentMarketPrice);
|
||||||
const dropValue = parsePositiveNumber(draft.dropValue);
|
const dropValue = parseNonNegativeNumber(draft.dropValue);
|
||||||
if (!currentMarketPrice || !dropValue) return null;
|
if (!currentMarketPrice || dropValue === null) return null;
|
||||||
|
|
||||||
if (draft.dropMode === 'dollar') {
|
if (draft.dropMode === 'dollar') {
|
||||||
const trigger = currentMarketPrice - dropValue;
|
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.');
|
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');
|
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,
|
entry_price: side === 'sell' ? holding!.entryPrice : null,
|
||||||
reference_price: currentMarketPrice,
|
reference_price: currentMarketPrice,
|
||||||
gain_threshold_for_sell: profitValue,
|
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',
|
workflow_type: 'simple',
|
||||||
simple_side: side,
|
simple_side: side,
|
||||||
drop_trigger_mode: side === 'buy' ? input.draft.dropMode : null,
|
drop_trigger_mode: side === 'buy' ? input.draft.dropMode : null,
|
||||||
@ -177,7 +184,11 @@ function buildPreviewText(draft: SimpleSetupDraft, holding: SimpleHolding | null
|
|||||||
const profitTargetPrice = computeProfitTargetPrice(triggerPrice, draft.profitMode, draft.profitValue);
|
const profitTargetPrice = computeProfitTargetPrice(triggerPrice, draft.profitMode, draft.profitValue);
|
||||||
if (!triggerPrice) return `Buy ${symbol} after the configured drop trigger is hit.`;
|
if (!triggerPrice) return `Buy ${symbol} after the configured drop trigger is hit.`;
|
||||||
|
|
||||||
const dropText = draft.dropMode === 'dollar'
|
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`
|
? `$${Number(draft.dropValue || 0).toFixed(2)} below current price`
|
||||||
: `${draft.dropValue || '0'}% below current price`;
|
: `${draft.dropValue || '0'}% below current price`;
|
||||||
const profitText = draft.profitMode === 'dollar'
|
const profitText = draft.profitMode === 'dollar'
|
||||||
@ -212,9 +223,9 @@ function buildDraftFromEntry(entry: ManualEntryPayload): SimpleSetupDraft {
|
|||||||
quantity: entry.quantity ? String(entry.quantity) : '',
|
quantity: entry.quantity ? String(entry.quantity) : '',
|
||||||
currentMarketPrice: entry.reference_price ? Number(entry.reference_price).toFixed(4) : '',
|
currentMarketPrice: entry.reference_price ? Number(entry.reference_price).toFixed(4) : '',
|
||||||
dropMode: normalizeMode(entry.drop_trigger_mode, 'percent'),
|
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'),
|
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 || ''),
|
notes: String(entry.notes || ''),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -261,7 +272,9 @@ function describeSavedSetup(entry: ManualEntryPayload): string {
|
|||||||
|
|
||||||
if (side === 'buy') {
|
if (side === 'buy') {
|
||||||
const triggerPrice = computeBuyTriggerPrice(buildDraftFromEntry(entry));
|
const triggerPrice = computeBuyTriggerPrice(buildDraftFromEntry(entry));
|
||||||
const dropText = dropMode === 'dollar'
|
const dropText = dropValue === 0
|
||||||
|
? 'at current reference'
|
||||||
|
: dropMode === 'dollar'
|
||||||
? `$${dropValue.toFixed(2)} below`
|
? `$${dropValue.toFixed(2)} below`
|
||||||
: `${dropValue}% below`;
|
: `${dropValue}% below`;
|
||||||
const profitText = profitMode === 'dollar'
|
const profitText = profitMode === 'dollar'
|
||||||
@ -590,7 +603,7 @@ export function SimpleView() {
|
|||||||
<Input
|
<Input
|
||||||
value={draft.dropValue}
|
value={draft.dropValue}
|
||||||
onChange={(e) => updateDraft('dropValue', e.target.value)}
|
onChange={(e) => updateDraft('dropValue', e.target.value)}
|
||||||
placeholder={draft.dropMode === 'dollar' ? '5.00' : '8'}
|
placeholder={draft.dropMode === 'dollar' ? '0.00' : '0'}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user