fix(B8): wire visual builder backtest
This commit is contained in:
parent
eb6c1d8f7a
commit
1cd23f35f2
102
web/src/views/ResearchView.dom.test.tsx
Normal file
102
web/src/views/ResearchView.dom.test.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
// @vitest-environment jsdom
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { AppContext, type AppContextValue } from '../context/AppContext';
|
||||
import { ResearchView } from './ResearchView';
|
||||
|
||||
vi.mock('../tabs/MyStrategiesTab', () => ({
|
||||
MyStrategiesTab: () => <div>Strategies tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../tabs/SignalsTab', () => ({
|
||||
SignalsTab: () => <div>Signals tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../tabs/BacktestTab', () => ({
|
||||
BacktestTab: () => <div>Backtesting tab</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../components/strategy/CodeStrategyEditor', () => ({
|
||||
CodeStrategyEditor: () => <div>Code editor</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../components/strategy/VisualRuleBuilder', () => ({
|
||||
VisualRuleBuilder: ({ onBacktest }: any) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onBacktest?.([{
|
||||
id: 'rule-1',
|
||||
indicator: 'RSI',
|
||||
condition: 'below',
|
||||
value: 30,
|
||||
action: 'BUY',
|
||||
quantity: 10,
|
||||
quantityType: 'shares',
|
||||
}])}
|
||||
>
|
||||
Run Backtest
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../backtest/components/BacktestRunnerPanel', () => ({
|
||||
BacktestRunnerPanel: ({ title, strategyConfig, symbols, initialCapitalUsd }: any) => (
|
||||
<section aria-label="visual backtest runner">
|
||||
<h3>{title}</h3>
|
||||
<div>{symbols.join(',')}</div>
|
||||
<div>{initialCapitalUsd}</div>
|
||||
<pre>{JSON.stringify(strategyConfig)}</pre>
|
||||
</section>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../lib/profileApi', () => ({
|
||||
createTradeProfile: vi.fn(),
|
||||
}));
|
||||
|
||||
const appContext: AppContextValue = {
|
||||
botState: {
|
||||
settings: { totalCapital: 5000, riskPerTrade: 2, isAlgoEnabled: true },
|
||||
symbols: {
|
||||
AAPL: { price: 212.42, changeToday: 1.5 },
|
||||
},
|
||||
health: { tradingLoopHealthy: true, reconciliationLoopHealthy: true, capitalInvariantViolations: 0 },
|
||||
alerts: [],
|
||||
positions: [],
|
||||
orders: [],
|
||||
history: [],
|
||||
uptime: 0,
|
||||
} as any,
|
||||
socket: null,
|
||||
connected: true,
|
||||
activeSymbol: 'AAPL',
|
||||
setActiveSymbol: vi.fn(),
|
||||
isAdmin: false,
|
||||
user: { id: 'u1' },
|
||||
profile: {},
|
||||
showBacktestTab: true,
|
||||
showMarketplaceTab: false,
|
||||
handleSignOut: vi.fn(),
|
||||
};
|
||||
|
||||
describe('ResearchView visual builder backtest wiring', () => {
|
||||
it('opens a backtest runner with the current visual rules', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AppContext.Provider value={appContext}>
|
||||
<ResearchView />
|
||||
</AppContext.Provider>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Visual Builder' }));
|
||||
await user.click(screen.getByRole('button', { name: 'Run Backtest' }));
|
||||
|
||||
expect(screen.getByRole('region', { name: 'visual backtest runner' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Backtest: Visual Strategy (AAPL)')).toBeInTheDocument();
|
||||
expect(screen.getByText('AAPL')).toBeInTheDocument();
|
||||
expect(screen.getByText('5000')).toBeInTheDocument();
|
||||
expect(screen.getByText(/"type":"visual"/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/"indicator":"RSI"/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -6,9 +6,25 @@ import { MyStrategiesTab } from '../tabs/MyStrategiesTab';
|
||||
import { VisualRuleBuilder, type VisualRule } from '../components/strategy/VisualRuleBuilder';
|
||||
import { CodeStrategyEditor } from '../components/strategy/CodeStrategyEditor';
|
||||
import { createTradeProfile } from '../lib/profileApi';
|
||||
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
||||
|
||||
type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting';
|
||||
|
||||
function buildVisualStrategyConfig(rules: VisualRule[]) {
|
||||
return {
|
||||
type: 'visual',
|
||||
version: 1,
|
||||
rules: rules.map(r => ({
|
||||
indicator: r.indicator,
|
||||
condition: r.condition,
|
||||
value: r.value,
|
||||
action: r.action,
|
||||
quantity: r.quantity,
|
||||
quantityType: r.quantityType,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Sub-tab pill styles
|
||||
function SubTab({
|
||||
label, active, onClick,
|
||||
@ -38,6 +54,7 @@ function SubTab({
|
||||
export function ResearchView() {
|
||||
const { botState, connected, showBacktestTab, isAdmin, activeSymbol } = useAppContext();
|
||||
const [tab, setTab] = useState<ResearchTab>('Strategies');
|
||||
const [visualBacktestRules, setVisualBacktestRules] = useState<VisualRule[] | null>(null);
|
||||
|
||||
const tabs: ResearchTab[] = [
|
||||
'Strategies',
|
||||
@ -54,8 +71,7 @@ export function ResearchView() {
|
||||
// Visual rules go inside strategy_config.rules so the strategy engine can
|
||||
// route to the visual interpreter (alongside the existing rule-based engine).
|
||||
const handleSaveVisualStrategy = async (name: string, rules: VisualRule[]) => {
|
||||
const fallbackSymbol = Object.keys(botState.symbols)[0] ?? 'SPY';
|
||||
const symbol = activeSymbol || fallbackSymbol;
|
||||
const symbol = activeSymbol || Object.keys(botState.symbols)[0] || 'SPY';
|
||||
const totalCapital = botState.settings?.totalCapital ?? 1000;
|
||||
const riskPct = botState.settings?.riskPerTrade ?? 1;
|
||||
|
||||
@ -65,21 +81,13 @@ export function ResearchView() {
|
||||
allocated_capital: totalCapital,
|
||||
risk_per_trade_percent: riskPct,
|
||||
is_active: false, // user activates explicitly from MyStrategiesTab
|
||||
strategy_config: {
|
||||
type: 'visual',
|
||||
version: 1,
|
||||
rules: rules.map(r => ({
|
||||
indicator: r.indicator,
|
||||
condition: r.condition,
|
||||
value: r.value,
|
||||
action: r.action,
|
||||
quantity: r.quantity,
|
||||
quantityType: r.quantityType,
|
||||
})),
|
||||
},
|
||||
strategy_config: buildVisualStrategyConfig(rules),
|
||||
});
|
||||
};
|
||||
|
||||
const visualBuilderSymbol = activeSymbol || Object.keys(botState.symbols)[0] || 'SPY';
|
||||
const initialCapitalUsd = botState.settings?.totalCapital ?? 1000;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Research</h2>
|
||||
@ -112,9 +120,21 @@ export function ResearchView() {
|
||||
</div>
|
||||
</div>
|
||||
<VisualRuleBuilder
|
||||
symbol={activeSymbol || Object.keys(botState.symbols)[0] || 'SPY'}
|
||||
symbol={visualBuilderSymbol}
|
||||
onSave={handleSaveVisualStrategy}
|
||||
onBacktest={showBacktestTab ? (rules) => setVisualBacktestRules(rules.map(rule => ({ ...rule }))) : undefined}
|
||||
/>
|
||||
{showBacktestTab && visualBacktestRules && (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<BacktestRunnerPanel
|
||||
strategyConfig={buildVisualStrategyConfig(visualBacktestRules)}
|
||||
symbols={[visualBuilderSymbol]}
|
||||
initialCapitalUsd={initialCapitalUsd}
|
||||
title={`Backtest: Visual Strategy (${visualBuilderSymbol})`}
|
||||
onClose={() => setVisualBacktestRules(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user