refactor(plans): centralize local view state
This commit is contained in:
parent
ac17525124
commit
5d5c1ed2bc
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { Pencil, RefreshCw, Trash2 } from 'lucide-react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
@ -17,23 +17,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../co
|
||||
import { Input } from '../components/ui/input';
|
||||
import { PageHeader } from '../components/ui/page-header';
|
||||
import { Select } from '../components/ui/select';
|
||||
|
||||
type SimpleSide = 'buy' | 'sell';
|
||||
type TriggerMode = 'dollar' | 'percent';
|
||||
|
||||
type SimpleSetupDraft = {
|
||||
symbol: string;
|
||||
side: SimpleSide;
|
||||
sizingMode: 'quantity' | 'amount';
|
||||
quantity: string;
|
||||
amountUsd: string;
|
||||
currentMarketPrice: string;
|
||||
dropMode: TriggerMode;
|
||||
dropValue: string;
|
||||
profitMode: TriggerMode;
|
||||
profitValue: string;
|
||||
notes: string;
|
||||
};
|
||||
import {
|
||||
DEFAULT_TRADE_PLANS_UI_STATE,
|
||||
reduceTradePlansUiState,
|
||||
type MarketPriceSource,
|
||||
type SimpleSetupDraft,
|
||||
type SimpleSide,
|
||||
type TriggerMode,
|
||||
} from './tradePlansState';
|
||||
|
||||
type SimpleHolding = {
|
||||
symbol: string;
|
||||
@ -53,8 +44,6 @@ type SimpleRuntimeSnapshot = {
|
||||
|
||||
type SimpleOperationalEvent = NonNullable<ReturnType<typeof useAppContext>['botState']['operationalEvents']>[number];
|
||||
|
||||
type MarketPriceSource = 'live' | 'latest_close' | 'reference_price' | null;
|
||||
|
||||
const SIMPLE_AUTO_PROFILE_NAME = 'Simple Auto Profile';
|
||||
const SIMPLE_AUTO_PROFILE_KEY = SIMPLE_AUTO_PROFILE_NAME.toLowerCase();
|
||||
const SIMPLE_SYMBOL_DATALIST_ID = 'simple-supported-symbols';
|
||||
@ -72,20 +61,6 @@ const COMMON_SIMPLE_SYMBOLS = [
|
||||
'SOL/USD',
|
||||
];
|
||||
|
||||
const DEFAULT_DRAFT: SimpleSetupDraft = {
|
||||
symbol: '',
|
||||
side: 'buy',
|
||||
sizingMode: 'quantity',
|
||||
quantity: '',
|
||||
amountUsd: '',
|
||||
currentMarketPrice: '',
|
||||
dropMode: 'percent',
|
||||
dropValue: '',
|
||||
profitMode: 'percent',
|
||||
profitValue: '',
|
||||
notes: '',
|
||||
};
|
||||
|
||||
function parsePositiveNumber(value: string): number | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
@ -620,22 +595,26 @@ export function SimpleView() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [profiles, setProfiles] = useState<TradeProfilePayload[]>([]);
|
||||
const [savedSetups, setSavedSetups] = useState<ManualEntryPayload[]>([]);
|
||||
const [editingSetupId, setEditingSetupId] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<SimpleSetupDraft>(DEFAULT_DRAFT);
|
||||
const [uiState, dispatch] = useReducer(reduceTradePlansUiState, DEFAULT_TRADE_PLANS_UI_STATE);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [loadingPrice, setLoadingPrice] = useState(false);
|
||||
const [marketPriceSource, setMarketPriceSource] = useState<MarketPriceSource>(null);
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedHoldingTradeId, setSelectedHoldingTradeId] = useState<string | null>(null);
|
||||
const [focusedSetupId, setFocusedSetupId] = useState<string | null>(null);
|
||||
const marketPriceRequestSymbolRef = useRef<string>('');
|
||||
const consumedPrefillKeyRef = useRef<string>('');
|
||||
const consumedSetupFocusKeyRef = useRef<string>('');
|
||||
const setupCardRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
const focusedSetupTimerRef = useRef<number | null>(null);
|
||||
|
||||
const {
|
||||
editingSetupId,
|
||||
draft,
|
||||
marketPriceSource,
|
||||
copiedKey,
|
||||
message,
|
||||
error,
|
||||
selectedHoldingTradeId,
|
||||
focusedSetupId,
|
||||
} = uiState;
|
||||
|
||||
const normalizedSymbol = draft.symbol.trim().toUpperCase();
|
||||
const symbolState = botState?.symbols && typeof botState.symbols === 'object' ? botState.symbols : {};
|
||||
const livePrice = normalizedSymbol ? Number(symbolState?.[normalizedSymbol]?.price || 0) : 0;
|
||||
@ -728,7 +707,7 @@ export function SimpleView() {
|
||||
setSavedSetups(normalizeSimpleEntries(entryRows));
|
||||
} catch (err: any) {
|
||||
if (cancelled) return;
|
||||
setError(err?.message ?? 'Failed to load Simple setups');
|
||||
dispatch({ type: 'set-error', value: err?.message ?? 'Failed to load Simple setups' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -740,12 +719,10 @@ export function SimpleView() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!livePrice) return;
|
||||
setMarketPriceSource('live');
|
||||
setDraft((prev) => (
|
||||
prev.currentMarketPrice === livePrice.toFixed(4)
|
||||
? prev
|
||||
: { ...prev, currentMarketPrice: livePrice.toFixed(4) }
|
||||
));
|
||||
dispatch({ type: 'set-market-price-source', value: 'live' });
|
||||
if (draft.currentMarketPrice !== livePrice.toFixed(4)) {
|
||||
dispatch({ type: 'set-draft-field', key: 'currentMarketPrice', value: livePrice.toFixed(4) });
|
||||
}
|
||||
}, [livePrice]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -777,24 +754,21 @@ export function SimpleView() {
|
||||
|
||||
function updateDraft<K extends keyof SimpleSetupDraft>(key: K, value: SimpleSetupDraft[K]) {
|
||||
if (key === 'side' && value === 'buy') {
|
||||
setSelectedHoldingTradeId(null);
|
||||
dispatch({ type: 'set-selected-holding-trade-id', value: null });
|
||||
}
|
||||
if (key === 'symbol' && draft.side === 'sell') {
|
||||
setSelectedHoldingTradeId(null);
|
||||
dispatch({ type: 'set-selected-holding-trade-id', value: null });
|
||||
}
|
||||
setDraft((prev) => ({ ...prev, [key]: value }));
|
||||
dispatch({ type: 'set-draft-field', key, value });
|
||||
}
|
||||
|
||||
function applyHoldingToDraft(holding: SimpleHolding) {
|
||||
setMarketPriceSource(null);
|
||||
setSelectedHoldingTradeId(holding.tradeId || null);
|
||||
setDraft((prev) => ({
|
||||
...prev,
|
||||
side: 'sell',
|
||||
dispatch({
|
||||
type: 'apply-holding',
|
||||
tradeId: holding.tradeId || null,
|
||||
symbol: holding.symbol,
|
||||
quantity: String(holding.size),
|
||||
currentMarketPrice: '',
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -823,8 +797,8 @@ export function SimpleView() {
|
||||
|
||||
if (selected) {
|
||||
applyHoldingToDraft(selected);
|
||||
setMessage(`Loaded ${selected.symbol} from Portfolio. Configure the profit target and save the plan.`);
|
||||
setError(null);
|
||||
dispatch({ type: 'set-message', value: `Loaded ${selected.symbol} from Portfolio. Configure the profit target and save the plan.` });
|
||||
dispatch({ type: 'set-error', value: null });
|
||||
}
|
||||
consumedPrefillKeyRef.current = prefillKey;
|
||||
setSearchParams({}, { replace: true });
|
||||
@ -842,14 +816,14 @@ export function SimpleView() {
|
||||
|
||||
if (!targetEntry) return;
|
||||
|
||||
setFocusedSetupId(requestedSetupId);
|
||||
setMessage(`Focused saved plan for ${targetEntry.symbol}.`);
|
||||
setError(null);
|
||||
dispatch({ type: 'set-focused-setup-id', value: requestedSetupId });
|
||||
dispatch({ type: 'set-message', value: `Focused saved plan for ${targetEntry.symbol}.` });
|
||||
dispatch({ type: 'set-error', value: null });
|
||||
if (focusedSetupTimerRef.current !== null) {
|
||||
window.clearTimeout(focusedSetupTimerRef.current);
|
||||
}
|
||||
focusedSetupTimerRef.current = window.setTimeout(() => {
|
||||
setFocusedSetupId((prev) => (prev === requestedSetupId ? null : prev));
|
||||
dispatch({ type: 'set-focused-setup-id', value: null });
|
||||
focusedSetupTimerRef.current = null;
|
||||
}, 2200);
|
||||
window.requestAnimationFrame(() => {
|
||||
@ -870,12 +844,12 @@ export function SimpleView() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
const key = `${kind}:${value}`;
|
||||
setCopiedKey(key);
|
||||
dispatch({ type: 'set-copied-key', value: key });
|
||||
window.setTimeout(() => {
|
||||
setCopiedKey((prev) => (prev === key ? null : prev));
|
||||
dispatch({ type: 'set-copied-key', value: null });
|
||||
}, 1200);
|
||||
} catch {
|
||||
setError(`Failed to copy ${kind} ID`);
|
||||
dispatch({ type: 'set-error', value: `Failed to copy ${kind} ID` });
|
||||
}
|
||||
}
|
||||
|
||||
@ -898,7 +872,7 @@ export function SimpleView() {
|
||||
}
|
||||
|
||||
function setMarketPriceValue(value: string, source: MarketPriceSource) {
|
||||
setMarketPriceSource(source);
|
||||
dispatch({ type: 'set-market-price-source', value: source });
|
||||
updateDraft('currentMarketPrice', value);
|
||||
}
|
||||
|
||||
@ -908,8 +882,7 @@ export function SimpleView() {
|
||||
marketPriceRequestSymbolRef.current = requestSymbol;
|
||||
setLoadingPrice(true);
|
||||
if (!options?.silent) {
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
dispatch({ type: 'clear-feedback' });
|
||||
}
|
||||
|
||||
try {
|
||||
@ -937,7 +910,7 @@ export function SimpleView() {
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (!options?.silent && marketPriceRequestSymbolRef.current === requestSymbol) {
|
||||
setError(err?.message ?? 'Failed to load market data');
|
||||
dispatch({ type: 'set-error', value: err?.message ?? 'Failed to load market data' });
|
||||
}
|
||||
} finally {
|
||||
if (marketPriceRequestSymbolRef.current === requestSymbol) {
|
||||
@ -949,8 +922,7 @@ export function SimpleView() {
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
dispatch({ type: 'clear-feedback' });
|
||||
|
||||
try {
|
||||
const existingEntry = editingSetupId
|
||||
@ -965,37 +937,34 @@ export function SimpleView() {
|
||||
|
||||
if (editingSetupId) {
|
||||
await updateManualEntry(editingSetupId, payload);
|
||||
setMessage(`Updated ${normalizedSymbol} Simple setup.`);
|
||||
dispatch({ type: 'set-message', value: `Updated ${normalizedSymbol} Simple setup.` });
|
||||
} else {
|
||||
await createManualEntry({
|
||||
...payload,
|
||||
stock_instance_id: crypto.randomUUID(),
|
||||
});
|
||||
setMessage(`Saved ${normalizedSymbol} Simple setup.`);
|
||||
dispatch({ type: 'set-message', value: `Saved ${normalizedSymbol} Simple setup.` });
|
||||
}
|
||||
|
||||
await refreshSetupList();
|
||||
setEditingSetupId(null);
|
||||
setSelectedHoldingTradeId(null);
|
||||
setMarketPriceSource(draft.currentMarketPrice ? marketPriceSource : null);
|
||||
setDraft({
|
||||
...DEFAULT_DRAFT,
|
||||
dispatch({
|
||||
type: 'reset-form',
|
||||
currentMarketPrice: draft.currentMarketPrice,
|
||||
marketPriceSource: draft.currentMarketPrice ? marketPriceSource : null,
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err?.message ?? 'Failed to save Simple setup');
|
||||
dispatch({ type: 'set-error', value: err?.message ?? 'Failed to save Simple setup' });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit(entry: ManualEntryPayload) {
|
||||
setEditingSetupId(String(entry.stock_instance_id || ''));
|
||||
setSelectedHoldingTradeId(String(entry.linked_trade_id || '').trim() || null);
|
||||
setMarketPriceSource(inferMarketPriceSourceFromEntry(entry));
|
||||
setDraft(buildDraftFromEntry(entry));
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
dispatch({ type: 'set-editing-setup-id', value: String(entry.stock_instance_id || '') });
|
||||
dispatch({ type: 'set-selected-holding-trade-id', value: String(entry.linked_trade_id || '').trim() || null });
|
||||
dispatch({ type: 'set-market-price-source', value: inferMarketPriceSourceFromEntry(entry) });
|
||||
dispatch({ type: 'replace-draft', draft: buildDraftFromEntry(entry) });
|
||||
dispatch({ type: 'clear-feedback' });
|
||||
}
|
||||
|
||||
async function handleDelete(entryId: string) {
|
||||
@ -1003,22 +972,18 @@ export function SimpleView() {
|
||||
try {
|
||||
await deleteManualEntry(entryId);
|
||||
if (editingSetupId === entryId) {
|
||||
setEditingSetupId(null);
|
||||
setSelectedHoldingTradeId(null);
|
||||
setMarketPriceSource(null);
|
||||
setDraft(DEFAULT_DRAFT);
|
||||
dispatch({ type: 'reset-form', currentMarketPrice: '', marketPriceSource: null });
|
||||
}
|
||||
await refreshSetupList();
|
||||
} catch (err: any) {
|
||||
setError(err?.message ?? 'Failed to delete Simple setup');
|
||||
dispatch({ type: 'set-error', value: err?.message ?? 'Failed to delete Simple setup' });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConvertToLongTerm(entry: ManualEntryPayload) {
|
||||
const entryId = String(entry.stock_instance_id || '');
|
||||
if (!entryId) return;
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
dispatch({ type: 'clear-feedback' });
|
||||
try {
|
||||
await updateSavedSetup(entryId, (current) => ({
|
||||
...current,
|
||||
@ -1027,17 +992,16 @@ export function SimpleView() {
|
||||
status: 'simple_bought',
|
||||
active: true,
|
||||
}));
|
||||
setMessage(`${String(entry.symbol || '').trim().toUpperCase()} is now treated as a long-term hold. Automated exit monitoring is paused.`);
|
||||
dispatch({ type: 'set-message', value: `${String(entry.symbol || '').trim().toUpperCase()} is now treated as a long-term hold. Automated exit monitoring is paused.` });
|
||||
} catch (err: any) {
|
||||
setError(err?.message ?? 'Failed to convert setup to long-term mode');
|
||||
dispatch({ type: 'set-error', value: err?.message ?? 'Failed to convert setup to long-term mode' });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResumeExitManagement(entry: ManualEntryPayload) {
|
||||
const entryId = String(entry.stock_instance_id || '');
|
||||
if (!entryId) return;
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
dispatch({ type: 'clear-feedback' });
|
||||
try {
|
||||
await updateSavedSetup(entryId, (current) => ({
|
||||
...current,
|
||||
@ -1046,9 +1010,9 @@ export function SimpleView() {
|
||||
status: 'simple_bought',
|
||||
active: true,
|
||||
}));
|
||||
setMessage(`${String(entry.symbol || '').trim().toUpperCase()} is back under short-term exit management.`);
|
||||
dispatch({ type: 'set-message', value: `${String(entry.symbol || '').trim().toUpperCase()} is back under short-term exit management.` });
|
||||
} catch (err: any) {
|
||||
setError(err?.message ?? 'Failed to resume short-term exit management');
|
||||
dispatch({ type: 'set-error', value: err?.message ?? 'Failed to resume short-term exit management' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -1072,15 +1036,11 @@ export function SimpleView() {
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingSetupId(null);
|
||||
setSelectedHoldingTradeId(null);
|
||||
setMarketPriceSource(draft.currentMarketPrice ? marketPriceSource : null);
|
||||
setDraft({
|
||||
...DEFAULT_DRAFT,
|
||||
dispatch({
|
||||
type: 'reset-form',
|
||||
currentMarketPrice: draft.currentMarketPrice,
|
||||
marketPriceSource: draft.currentMarketPrice ? marketPriceSource : null,
|
||||
});
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@ -1096,10 +1056,9 @@ export function SimpleView() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
setSelectedHoldingTradeId(null);
|
||||
setDraft((prev) => ({ ...prev, side: 'buy' }));
|
||||
dispatch({ type: 'clear-feedback' });
|
||||
dispatch({ type: 'set-selected-holding-trade-id', value: null });
|
||||
updateDraft('side', 'buy');
|
||||
}}
|
||||
className={`rounded-[1.25rem] border px-4 py-4 text-left transition ${
|
||||
draft.side === 'buy'
|
||||
@ -1114,12 +1073,11 @@ export function SimpleView() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
dispatch({ type: 'clear-feedback' });
|
||||
if (availableSellHoldings.length > 0) {
|
||||
applyHoldingToDraft(availableSellHoldings[0]);
|
||||
} else {
|
||||
setDraft((prev) => ({ ...prev, side: 'sell' }));
|
||||
updateDraft('side', 'sell');
|
||||
}
|
||||
}}
|
||||
className={`rounded-[1.25rem] border px-4 py-4 text-left transition ${
|
||||
@ -1167,15 +1125,18 @@ export function SimpleView() {
|
||||
<Input
|
||||
value={draft.symbol}
|
||||
onChange={(e) => {
|
||||
setMarketPriceSource(null);
|
||||
dispatch({ type: 'set-market-price-source', value: null });
|
||||
if (draft.side === 'sell') {
|
||||
setSelectedHoldingTradeId(null);
|
||||
dispatch({ type: 'set-selected-holding-trade-id', value: null });
|
||||
}
|
||||
setDraft((prev) => ({
|
||||
...prev,
|
||||
symbol: e.target.value.toUpperCase(),
|
||||
currentMarketPrice: '',
|
||||
}));
|
||||
dispatch({
|
||||
type: 'replace-draft',
|
||||
draft: {
|
||||
...draft,
|
||||
symbol: e.target.value.toUpperCase(),
|
||||
currentMarketPrice: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
list={SIMPLE_SYMBOL_DATALIST_ID}
|
||||
placeholder="AAPL"
|
||||
@ -1200,15 +1161,18 @@ export function SimpleView() {
|
||||
key={symbol}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMarketPriceSource(null);
|
||||
dispatch({ type: 'set-market-price-source', value: null });
|
||||
if (draft.side === 'sell') {
|
||||
setSelectedHoldingTradeId(null);
|
||||
dispatch({ type: 'set-selected-holding-trade-id', value: null });
|
||||
}
|
||||
setDraft((prev) => ({
|
||||
...prev,
|
||||
symbol,
|
||||
currentMarketPrice: '',
|
||||
}));
|
||||
dispatch({
|
||||
type: 'replace-draft',
|
||||
draft: {
|
||||
...draft,
|
||||
symbol,
|
||||
currentMarketPrice: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
className={`rounded-full border px-3 py-1 text-[11px] font-semibold transition ${
|
||||
symbol === normalizedSymbol
|
||||
|
||||
53
web/src/views/tradePlansState.test.ts
Normal file
53
web/src/views/tradePlansState.test.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
DEFAULT_TRADE_PLANS_UI_STATE,
|
||||
reduceTradePlansUiState,
|
||||
} from './tradePlansState';
|
||||
|
||||
describe('tradePlansState reducer', () => {
|
||||
it('applies a holding and switches the draft into sell management mode', () => {
|
||||
const next = reduceTradePlansUiState(DEFAULT_TRADE_PLANS_UI_STATE, {
|
||||
type: 'apply-holding',
|
||||
tradeId: 'TRD-1',
|
||||
symbol: 'AAPL',
|
||||
quantity: '10',
|
||||
});
|
||||
|
||||
expect(next.selectedHoldingTradeId).toBe('TRD-1');
|
||||
expect(next.marketPriceSource).toBeNull();
|
||||
expect(next.draft.side).toBe('sell');
|
||||
expect(next.draft.symbol).toBe('AAPL');
|
||||
expect(next.draft.quantity).toBe('10');
|
||||
expect(next.draft.currentMarketPrice).toBe('');
|
||||
});
|
||||
|
||||
it('resets the editor while preserving the current market price context', () => {
|
||||
const next = reduceTradePlansUiState({
|
||||
...DEFAULT_TRADE_PLANS_UI_STATE,
|
||||
editingSetupId: 'setup-1',
|
||||
selectedHoldingTradeId: 'TRD-1',
|
||||
message: 'hello',
|
||||
error: 'bad',
|
||||
marketPriceSource: 'live',
|
||||
draft: {
|
||||
...DEFAULT_TRADE_PLANS_UI_STATE.draft,
|
||||
symbol: 'BTC/USD',
|
||||
currentMarketPrice: '81234.0000',
|
||||
notes: 'keep',
|
||||
},
|
||||
}, {
|
||||
type: 'reset-form',
|
||||
currentMarketPrice: '81234.0000',
|
||||
marketPriceSource: 'live',
|
||||
});
|
||||
|
||||
expect(next.editingSetupId).toBeNull();
|
||||
expect(next.selectedHoldingTradeId).toBeNull();
|
||||
expect(next.message).toBeNull();
|
||||
expect(next.error).toBeNull();
|
||||
expect(next.marketPriceSource).toBe('live');
|
||||
expect(next.draft.symbol).toBe('');
|
||||
expect(next.draft.currentMarketPrice).toBe('81234.0000');
|
||||
expect(next.draft.notes).toBe('');
|
||||
});
|
||||
});
|
||||
154
web/src/views/tradePlansState.ts
Normal file
154
web/src/views/tradePlansState.ts
Normal file
@ -0,0 +1,154 @@
|
||||
export type SimpleSide = 'buy' | 'sell';
|
||||
export type TriggerMode = 'dollar' | 'percent';
|
||||
export type MarketPriceSource = 'live' | 'latest_close' | 'reference_price' | null;
|
||||
|
||||
export type SimpleSetupDraft = {
|
||||
symbol: string;
|
||||
side: SimpleSide;
|
||||
sizingMode: 'quantity' | 'amount';
|
||||
quantity: string;
|
||||
amountUsd: string;
|
||||
currentMarketPrice: string;
|
||||
dropMode: TriggerMode;
|
||||
dropValue: string;
|
||||
profitMode: TriggerMode;
|
||||
profitValue: string;
|
||||
notes: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_TRADE_PLAN_DRAFT: SimpleSetupDraft = {
|
||||
symbol: '',
|
||||
side: 'buy',
|
||||
sizingMode: 'quantity',
|
||||
quantity: '',
|
||||
amountUsd: '',
|
||||
currentMarketPrice: '',
|
||||
dropMode: 'percent',
|
||||
dropValue: '',
|
||||
profitMode: 'percent',
|
||||
profitValue: '',
|
||||
notes: '',
|
||||
};
|
||||
|
||||
export type TradePlansUiState = {
|
||||
editingSetupId: string | null;
|
||||
draft: SimpleSetupDraft;
|
||||
marketPriceSource: MarketPriceSource;
|
||||
copiedKey: string | null;
|
||||
message: string | null;
|
||||
error: string | null;
|
||||
selectedHoldingTradeId: string | null;
|
||||
focusedSetupId: string | null;
|
||||
};
|
||||
|
||||
export const DEFAULT_TRADE_PLANS_UI_STATE: TradePlansUiState = {
|
||||
editingSetupId: null,
|
||||
draft: DEFAULT_TRADE_PLAN_DRAFT,
|
||||
marketPriceSource: null,
|
||||
copiedKey: null,
|
||||
message: null,
|
||||
error: null,
|
||||
selectedHoldingTradeId: null,
|
||||
focusedSetupId: null,
|
||||
};
|
||||
|
||||
export type TradePlansUiAction =
|
||||
| { type: 'set-draft-field'; key: keyof SimpleSetupDraft; value: SimpleSetupDraft[keyof SimpleSetupDraft] }
|
||||
| { type: 'replace-draft'; draft: SimpleSetupDraft }
|
||||
| { type: 'set-editing-setup-id'; value: string | null }
|
||||
| { type: 'set-market-price-source'; value: MarketPriceSource }
|
||||
| { type: 'set-copied-key'; value: string | null }
|
||||
| { type: 'set-message'; value: string | null }
|
||||
| { type: 'set-error'; value: string | null }
|
||||
| { type: 'set-selected-holding-trade-id'; value: string | null }
|
||||
| { type: 'set-focused-setup-id'; value: string | null }
|
||||
| { type: 'clear-feedback' }
|
||||
| { type: 'reset-form'; currentMarketPrice: string; marketPriceSource: MarketPriceSource }
|
||||
| { type: 'apply-holding'; tradeId: string | null; symbol: string; quantity: string };
|
||||
|
||||
export function reduceTradePlansUiState(state: TradePlansUiState, action: TradePlansUiAction): TradePlansUiState {
|
||||
switch (action.type) {
|
||||
case 'set-draft-field':
|
||||
return {
|
||||
...state,
|
||||
draft: {
|
||||
...state.draft,
|
||||
[action.key]: action.value,
|
||||
},
|
||||
};
|
||||
case 'replace-draft':
|
||||
return {
|
||||
...state,
|
||||
draft: action.draft,
|
||||
};
|
||||
case 'set-editing-setup-id':
|
||||
return {
|
||||
...state,
|
||||
editingSetupId: action.value,
|
||||
};
|
||||
case 'set-market-price-source':
|
||||
return {
|
||||
...state,
|
||||
marketPriceSource: action.value,
|
||||
};
|
||||
case 'set-copied-key':
|
||||
return {
|
||||
...state,
|
||||
copiedKey: action.value,
|
||||
};
|
||||
case 'set-message':
|
||||
return {
|
||||
...state,
|
||||
message: action.value,
|
||||
};
|
||||
case 'set-error':
|
||||
return {
|
||||
...state,
|
||||
error: action.value,
|
||||
};
|
||||
case 'set-selected-holding-trade-id':
|
||||
return {
|
||||
...state,
|
||||
selectedHoldingTradeId: action.value,
|
||||
};
|
||||
case 'set-focused-setup-id':
|
||||
return {
|
||||
...state,
|
||||
focusedSetupId: action.value,
|
||||
};
|
||||
case 'clear-feedback':
|
||||
return {
|
||||
...state,
|
||||
message: null,
|
||||
error: null,
|
||||
};
|
||||
case 'reset-form':
|
||||
return {
|
||||
...state,
|
||||
editingSetupId: null,
|
||||
selectedHoldingTradeId: null,
|
||||
message: null,
|
||||
error: null,
|
||||
marketPriceSource: action.marketPriceSource,
|
||||
draft: {
|
||||
...DEFAULT_TRADE_PLAN_DRAFT,
|
||||
currentMarketPrice: action.currentMarketPrice,
|
||||
},
|
||||
};
|
||||
case 'apply-holding':
|
||||
return {
|
||||
...state,
|
||||
selectedHoldingTradeId: action.tradeId,
|
||||
marketPriceSource: null,
|
||||
draft: {
|
||||
...state.draft,
|
||||
side: 'sell',
|
||||
symbol: action.symbol,
|
||||
quantity: action.quantity,
|
||||
currentMarketPrice: '',
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user