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 { 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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user