quanthedge / frontend /src /pages /StrategyBuilder.tsx
jashdoshi77's picture
added dark mode
e85ce30
import { useState, useCallback } from 'react';
import { ReactFlow, addEdge, applyNodeChanges, applyEdgeChanges, Background, Controls, MiniMap, Panel, type Node, type Edge, type Connection } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { strategyAPI } from '../api/client';
const NODE_TYPES = [
{ type: 'data', label: 'Data Source', color: '#005241', config: { tickers: ['AAPL', 'MSFT', 'GOOGL'] } },
{ type: 'indicator', label: 'Indicator', color: '#0a8f5c', config: { signal_type: 'momentum' } },
{ type: 'factor', label: 'Factor Model', color: '#b8860b', config: { factors: ['momentum', 'value'] } },
{ type: 'condition', label: 'Condition', color: '#c23030', config: { rule_type: 'threshold', threshold: 0.2, operator: 'gt' } },
{ type: 'allocation', label: 'Allocation', color: '#1a5276', config: { method: 'equal_weight', max_position_pct: 0.1 } },
{ type: 'risk', label: 'Risk Mgmt', color: '#6b21a8', config: { stop_loss_pct: 0.05, take_profit_pct: 0.15 } },
];
export default function StrategyBuilder() {
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
const [strategyConfig, setStrategyConfig] = useState<any>(null);
const [strategies, setStrategies] = useState<any[]>([]);
const [saving, setSaving] = useState(false);
const [strategyName, setStrategyName] = useState('');
const onNodesChange = useCallback((changes: any) => setNodes(nds => applyNodeChanges(changes, nds)), []);
const onEdgesChange = useCallback((changes: any) => setEdges(eds => applyEdgeChanges(changes, eds)), []);
const onConnect = useCallback((connection: Connection) => setEdges(eds => addEdge({...connection, id: `e-${Date.now()}`}, eds)), []);
const addNode = (nodeType: typeof NODE_TYPES[0]) => {
const id = `node-${Date.now()}`;
const newNode: Node = {
id,
position: { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300 },
data: { label: nodeType.label },
style: {
background: nodeType.color + '22',
border: `2px solid ${nodeType.color}`,
borderRadius: '12px',
padding: '12px 20px',
color: 'var(--text-primary)',
fontSize: '13px',
fontWeight: '600',
minWidth: '160px',
textAlign: 'center' as const,
},
type: 'default',
};
(newNode as any)._type = nodeType.type;
(newNode as any)._config = { ...nodeType.config };
setNodes(nds => [...nds, newNode]);
};
const convertToConfig = async () => {
const graph = {
nodes: nodes.map(n => ({
id: n.id,
type: (n as any)._type || 'data',
label: String(n.data?.label || ''),
position: n.position,
config: (n as any)._config || {},
})),
edges: edges.map(e => ({
id: e.id,
source: e.source,
target: e.target,
})),
};
try {
const { data } = await strategyAPI.convertGraph(graph);
setStrategyConfig(data.config);
} catch (e) { console.error(e); }
};
const saveStrategy = async () => {
if (!strategyConfig || !strategyName) return;
setSaving(true);
try {
await strategyAPI.create({
name: strategyName,
description: 'Created via Visual Strategy Builder',
strategy_type: 'custom',
config: strategyConfig,
});
setStrategyName('');
const { data } = await strategyAPI.list();
setStrategies(data.strategies || []);
} catch (e) { console.error(e); } finally { setSaving(false); }
};
useState(() => {
strategyAPI.list().then(({ data }) => setStrategies(data.strategies || [])).catch(console.error);
});
return (
<div className="page animate-fade-in">
<div className="page-header">
<h1>Strategy <span className="text-gradient">Builder</span></h1>
<p>Drag-and-drop visual strategy construction with node graph</p>
</div>
<div className="grid-2" style={{gridTemplateColumns:'1fr 320px'}}>
{/* Canvas */}
<div className="card" style={{padding:0,overflow:'hidden'}}>
<div style={{height:'500px'}}>
<ReactFlow
nodes={nodes} edges={edges}
onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect}
fitView
style={{background:'var(--bg-secondary)'}}
>
<Background color="var(--border-color)" gap={20} />
<Controls />
<MiniMap nodeColor="var(--accent)" style={{background:'var(--bg-tertiary)'}} />
<Panel position="top-left">
<div style={{display:'flex',gap:'0.375rem',flexWrap:'wrap',maxWidth:'400px'}}>
{NODE_TYPES.map(nt => (
<button key={nt.type} className="btn btn-secondary btn-sm" onClick={() => addNode(nt)}
style={{fontSize:'0.7rem',borderColor:nt.color+'66'}}>
{nt.label}
</button>
))}
</div>
</Panel>
</ReactFlow>
</div>
</div>
{/* Sidebar */}
<div style={{display:'flex',flexDirection:'column',gap:'1rem'}}>
<div className="card">
<h3 style={{marginBottom:'0.75rem',fontSize:'0.95rem'}}>Build Strategy</h3>
<p style={{fontSize:'0.8rem',color:'var(--text-secondary)',marginBottom:'1rem'}}>
Add nodes from the palette, connect them, then generate the config.
</p>
<p style={{fontSize:'0.72rem',color:'var(--text-muted)',marginBottom:'1rem',display:'flex',alignItems:'center',gap:'0.35rem'}}>
<kbd style={{padding:'0.1rem 0.35rem',borderRadius:4,border:'1px solid var(--border-color)',background:'var(--bg-tertiary)',fontSize:'0.65rem',fontFamily:'var(--font-mono)'}}></kbd> Press Backspace to delete a selected node
</p>
<button className="btn btn-primary" style={{width:'100%',marginBottom:'0.75rem'}} onClick={convertToConfig} disabled={nodes.length === 0}>
Generate Config
</button>
{strategyConfig && (
<div>
<div className="form-group">
<label>Strategy Name</label>
<input className="input" value={strategyName} onChange={e => setStrategyName(e.target.value)} placeholder="My Strategy" />
</div>
<button className="btn btn-primary" style={{width:'100%'}} onClick={saveStrategy} disabled={saving || !strategyName}>
{saving ? <div className="spinner" /> : 'Save Strategy'}
</button>
<details style={{marginTop:'1rem'}}>
<summary style={{cursor:'pointer',fontSize:'0.8rem',color:'var(--text-muted)'}}>View Config JSON</summary>
<pre style={{marginTop:'0.5rem',padding:'0.75rem',background:'var(--bg-tertiary)',borderRadius:'var(--radius-sm)',fontSize:'0.7rem',overflowX:'auto',maxHeight:200,color:'var(--accent)',fontFamily:'var(--font-mono)'}}>
{JSON.stringify(strategyConfig, null, 2)}
</pre>
</details>
</div>
)}
</div>
<div className="card">
<h3 style={{marginBottom:'0.75rem',fontSize:'0.95rem'}}>Saved Strategies ({strategies.length})</h3>
{strategies.length > 0 ? strategies.slice(0, 5).map(s => (
<div key={s.id} style={{padding:'0.5rem 0',borderBottom:'1px solid var(--border-subtle)',fontSize:'0.8rem'}}>
<div style={{fontWeight:600}}>{s.name}</div>
<div style={{color:'var(--text-muted)',fontSize:'0.7rem'}}>
{s.strategy_type} · v{s.version} · {s.status}
</div>
</div>
)) : <p style={{fontSize:'0.8rem',color:'var(--text-muted)'}}>No strategies yet</p>}
</div>
</div>
</div>
</div>
);
}