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)}
+ />
+
+ )}
)}