refactor(ui): migrate visual strategy controls
This commit is contained in:
parent
a622326be6
commit
bb4efc2b0d
@ -19,8 +19,8 @@ import {
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { GripVertical, Plus, Trash2, Save, Play } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Card, CardContent } from '../ui/card';
|
||||
import { Button, IconButton, Input, Select } from '../ui/Primitives';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
export type Indicator = 'RSI' | 'MACD' | 'EMA_50' | 'EMA_200' | 'Price' | 'Volume';
|
||||
@ -130,29 +130,35 @@ function RuleCard({
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}>IF</span>
|
||||
|
||||
{/* Indicator */}
|
||||
<select value={rule.indicator} style={sel}
|
||||
<Select
|
||||
value={rule.indicator}
|
||||
style={sel}
|
||||
controlSize="sm"
|
||||
variant="surface"
|
||||
options={(Object.keys(INDICATOR_LABELS) as Indicator[]).map(k => ({ value: k, label: INDICATOR_LABELS[k] }))}
|
||||
onChange={e => onChange(rule.id, {
|
||||
indicator: e.target.value as Indicator,
|
||||
value: INDICATOR_DEFAULTS[e.target.value as Indicator],
|
||||
})}>
|
||||
{(Object.keys(INDICATOR_LABELS) as Indicator[]).map(k => (
|
||||
<option key={k} value={k}>{INDICATOR_LABELS[k]}</option>
|
||||
))}
|
||||
</select>
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Condition */}
|
||||
<select value={rule.condition} style={sel}
|
||||
onChange={e => onChange(rule.id, { condition: e.target.value as Condition })}>
|
||||
{(Object.keys(CONDITION_LABELS) as Condition[]).map(k => (
|
||||
<option key={k} value={k}>{CONDITION_LABELS[k]}</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
value={rule.condition}
|
||||
style={sel}
|
||||
controlSize="sm"
|
||||
variant="surface"
|
||||
options={(Object.keys(CONDITION_LABELS) as Condition[]).map(k => ({ value: k, label: CONDITION_LABELS[k] }))}
|
||||
onChange={e => onChange(rule.id, { condition: e.target.value as Condition })}
|
||||
/>
|
||||
|
||||
{/* Value */}
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
value={rule.value}
|
||||
style={numInp}
|
||||
controlSize="sm"
|
||||
variant="surface"
|
||||
onChange={e => onChange(rule.id, { value: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
|
||||
@ -160,36 +166,51 @@ function RuleCard({
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}>→</span>
|
||||
|
||||
{/* Action */}
|
||||
<select value={rule.action} style={{
|
||||
<Select value={rule.action} style={{
|
||||
...sel,
|
||||
color: rule.action === 'BUY' ? '#16A34A' : '#DC2626',
|
||||
fontWeight: 700,
|
||||
background: rule.action === 'BUY' ? '#F0FDF4' : '#FEF2F2',
|
||||
border: `1px solid ${rule.action === 'BUY' ? '#86EFAC' : '#FCA5A5'}`,
|
||||
}}
|
||||
onChange={e => onChange(rule.id, { action: e.target.value as TradeAction })}>
|
||||
<option value="BUY">BUY</option>
|
||||
<option value="SELL">SELL</option>
|
||||
</select>
|
||||
controlSize="sm"
|
||||
variant="surface"
|
||||
options={[
|
||||
{ value: 'BUY', label: 'BUY' },
|
||||
{ value: 'SELL', label: 'SELL' },
|
||||
]}
|
||||
onChange={e => onChange(rule.id, { action: e.target.value as TradeAction })}
|
||||
/>
|
||||
|
||||
{/* Quantity */}
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
value={rule.quantity}
|
||||
style={numInp}
|
||||
min={1}
|
||||
controlSize="sm"
|
||||
variant="surface"
|
||||
onChange={e => onChange(rule.id, { quantity: parseFloat(e.target.value) || 1 })}
|
||||
/>
|
||||
|
||||
{/* Qty type */}
|
||||
<select value={rule.quantityType} style={sel}
|
||||
onChange={e => onChange(rule.id, { quantityType: e.target.value as QtyType })}>
|
||||
<option value="shares">shares</option>
|
||||
<option value="percent">% of capital</option>
|
||||
</select>
|
||||
<Select
|
||||
value={rule.quantityType}
|
||||
style={sel}
|
||||
controlSize="sm"
|
||||
variant="surface"
|
||||
options={[
|
||||
{ value: 'shares', label: 'shares' },
|
||||
{ value: 'percent', label: '% of capital' },
|
||||
]}
|
||||
onChange={e => onChange(rule.id, { quantityType: e.target.value as QtyType })}
|
||||
/>
|
||||
|
||||
{/* Delete */}
|
||||
<button
|
||||
<IconButton
|
||||
type="button"
|
||||
label="Remove rule"
|
||||
icon={<Trash2 size={14} />}
|
||||
onClick={() => onDelete(rule.id)}
|
||||
style={{
|
||||
marginLeft: 'auto', background: 'none', border: 'none',
|
||||
@ -197,10 +218,7 @@ function RuleCard({
|
||||
display: 'flex', alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
title="Remove rule"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -295,9 +313,11 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 500, marginBottom: 3 }}>Strategy name</div>
|
||||
<input
|
||||
<Input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
controlSize="sm"
|
||||
variant="surface"
|
||||
style={{
|
||||
border: '1px solid var(--border)', borderRadius: 12,
|
||||
padding: '7px 12px', fontSize: 14, fontWeight: 600,
|
||||
@ -362,8 +382,11 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
|
||||
</DndContext>
|
||||
|
||||
{/* Add rule */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setRules(prev => [...prev, makeRule()])}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 7,
|
||||
width: '100%', padding: '10px 0', border: '1px dashed var(--border-strong)',
|
||||
@ -373,7 +396,7 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
|
||||
}}
|
||||
>
|
||||
<Plus size={15} /> Add Rule
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Rule summary */}
|
||||
{rules.length > 0 && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user