fix(B8): wire visual builder backtest

This commit is contained in:
Saravana Achu Mac 2026-05-04 17:17:44 -07:00
parent eb6c1d8f7a
commit 1cd23f35f2
2 changed files with 137 additions and 15 deletions

View 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();
});
});

View File

@ -6,9 +6,25 @@ import { MyStrategiesTab } from '../tabs/MyStrategiesTab';
import { VisualRuleBuilder, type VisualRule } from '../components/strategy/VisualRuleBuilder'; import { VisualRuleBuilder, type VisualRule } from '../components/strategy/VisualRuleBuilder';
import { CodeStrategyEditor } from '../components/strategy/CodeStrategyEditor'; import { CodeStrategyEditor } from '../components/strategy/CodeStrategyEditor';
import { createTradeProfile } from '../lib/profileApi'; import { createTradeProfile } from '../lib/profileApi';
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting'; 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 // Sub-tab pill styles
function SubTab({ function SubTab({
label, active, onClick, label, active, onClick,
@ -38,6 +54,7 @@ function SubTab({
export function ResearchView() { export function ResearchView() {
const { botState, connected, showBacktestTab, isAdmin, activeSymbol } = useAppContext(); const { botState, connected, showBacktestTab, isAdmin, activeSymbol } = useAppContext();
const [tab, setTab] = useState<ResearchTab>('Strategies'); const [tab, setTab] = useState<ResearchTab>('Strategies');
const [visualBacktestRules, setVisualBacktestRules] = useState<VisualRule[] | null>(null);
const tabs: ResearchTab[] = [ const tabs: ResearchTab[] = [
'Strategies', 'Strategies',
@ -54,8 +71,7 @@ export function ResearchView() {
// Visual rules go inside strategy_config.rules so the strategy engine can // Visual rules go inside strategy_config.rules so the strategy engine can
// route to the visual interpreter (alongside the existing rule-based engine). // route to the visual interpreter (alongside the existing rule-based engine).
const handleSaveVisualStrategy = async (name: string, rules: VisualRule[]) => { const handleSaveVisualStrategy = async (name: string, rules: VisualRule[]) => {
const fallbackSymbol = Object.keys(botState.symbols)[0] ?? 'SPY'; const symbol = activeSymbol || Object.keys(botState.symbols)[0] || 'SPY';
const symbol = activeSymbol || fallbackSymbol;
const totalCapital = botState.settings?.totalCapital ?? 1000; const totalCapital = botState.settings?.totalCapital ?? 1000;
const riskPct = botState.settings?.riskPerTrade ?? 1; const riskPct = botState.settings?.riskPerTrade ?? 1;
@ -65,21 +81,13 @@ export function ResearchView() {
allocated_capital: totalCapital, allocated_capital: totalCapital,
risk_per_trade_percent: riskPct, risk_per_trade_percent: riskPct,
is_active: false, // user activates explicitly from MyStrategiesTab is_active: false, // user activates explicitly from MyStrategiesTab
strategy_config: { strategy_config: buildVisualStrategyConfig(rules),
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,
})),
},
}); });
}; };
const visualBuilderSymbol = activeSymbol || Object.keys(botState.symbols)[0] || 'SPY';
const initialCapitalUsd = botState.settings?.totalCapital ?? 1000;
return ( return (
<div> <div>
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Research</h2> <h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Research</h2>
@ -112,9 +120,21 @@ export function ResearchView() {
</div> </div>
</div> </div>
<VisualRuleBuilder <VisualRuleBuilder
symbol={activeSymbol || Object.keys(botState.symbols)[0] || 'SPY'} symbol={visualBuilderSymbol}
onSave={handleSaveVisualStrategy} 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> </div>
)} )}