From 1cd23f35f22c2e72811c0e28cb58c5106ac36615 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Mon, 4 May 2026 17:17:44 -0700 Subject: [PATCH] fix(B8): wire visual builder backtest --- web/src/views/ResearchView.dom.test.tsx | 102 ++++++++++++++++++++++++ web/src/views/ResearchView.tsx | 50 ++++++++---- 2 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 web/src/views/ResearchView.dom.test.tsx diff --git a/web/src/views/ResearchView.dom.test.tsx b/web/src/views/ResearchView.dom.test.tsx new file mode 100644 index 0000000..5f9e8db --- /dev/null +++ b/web/src/views/ResearchView.dom.test.tsx @@ -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: () =>
Strategies tab
, +})); + +vi.mock('../tabs/SignalsTab', () => ({ + SignalsTab: () =>
Signals tab
, +})); + +vi.mock('../tabs/BacktestTab', () => ({ + BacktestTab: () =>
Backtesting tab
, +})); + +vi.mock('../components/strategy/CodeStrategyEditor', () => ({ + CodeStrategyEditor: () =>
Code editor
, +})); + +vi.mock('../components/strategy/VisualRuleBuilder', () => ({ + VisualRuleBuilder: ({ onBacktest }: any) => ( + + ), +})); + +vi.mock('../backtest/components/BacktestRunnerPanel', () => ({ + BacktestRunnerPanel: ({ title, strategyConfig, symbols, initialCapitalUsd }: any) => ( +
+

{title}

+
{symbols.join(',')}
+
{initialCapitalUsd}
+
{JSON.stringify(strategyConfig)}
+
+ ), +})); + +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( + + + , + ); + + 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(); + }); +}); diff --git a/web/src/views/ResearchView.tsx b/web/src/views/ResearchView.tsx index ac6976b..52ed22e 100644 --- a/web/src/views/ResearchView.tsx +++ b/web/src/views/ResearchView.tsx @@ -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('Strategies'); + const [visualBacktestRules, setVisualBacktestRules] = useState(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 (

Research

@@ -112,9 +120,21 @@ export function ResearchView() {
setVisualBacktestRules(rules.map(rule => ({ ...rule }))) : undefined} /> + {showBacktestTab && visualBacktestRules && ( +
+ setVisualBacktestRules(null)} + /> +
+ )} )}