fix(E1): lazy-load code strategy editor

This commit is contained in:
Saravana Achu Mac 2026-05-04 18:02:46 -07:00
parent e5bda5ade9
commit 8a8c313ee8
3 changed files with 87 additions and 35 deletions

View File

@ -49,7 +49,7 @@ describe('CodeStrategyEditor save behavior', () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<CodeStrategyEditor symbol="AAPL" allocatedCapital={2500} riskPerTradePercent={2} />); render(<CodeStrategyEditor symbol="AAPL" allocatedCapital={2500} riskPerTradePercent={2} />);
fireEvent.change(screen.getByLabelText('strategy code'), { fireEvent.change(await screen.findByLabelText('strategy code'), {
target: { value: 'function strategy() { return { signal: "HOLD" }; }' }, target: { value: 'function strategy() { return { signal: "HOLD" }; }' },
}); });
await user.click(screen.getByRole('button', { name: /^save$/i })); await user.click(screen.getByRole('button', { name: /^save$/i }));
@ -99,7 +99,7 @@ describe('CodeStrategyEditor save behavior', () => {
it('saves with Cmd/Ctrl-S while focused in the editor', async () => { it('saves with Cmd/Ctrl-S while focused in the editor', async () => {
render(<CodeStrategyEditor symbol="TSLA" />); render(<CodeStrategyEditor symbol="TSLA" />);
fireEvent.keyDown(screen.getByLabelText('strategy code'), { key: 's', metaKey: true }); fireEvent.keyDown(await screen.findByLabelText('strategy code'), { key: 's', metaKey: true });
await waitFor(() => expect(createTradeProfileMock).toHaveBeenCalledTimes(1)); await waitFor(() => expect(createTradeProfileMock).toHaveBeenCalledTimes(1));
}); });
@ -112,7 +112,7 @@ describe('CodeStrategyEditor save behavior', () => {
vi.stubGlobal('fetch', fetchMock); vi.stubGlobal('fetch', fetchMock);
render(<CodeStrategyEditor symbol="TSLA" />); render(<CodeStrategyEditor symbol="TSLA" />);
fireEvent.keyDown(screen.getByLabelText('strategy code'), { key: 'Enter', metaKey: true }); fireEvent.keyDown(await screen.findByLabelText('strategy code'), { key: 'Enter', metaKey: true });
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
expect(String(fetchMock.mock.calls[0][0])).toBe('https://trading.test/api/backtest/run'); expect(String(fetchMock.mock.calls[0][0])).toBe('https://trading.test/api/backtest/run');

View File

@ -2,8 +2,7 @@
* Monaco-based code strategy editor. * Monaco-based code strategy editor.
* Users write a JS strategy function; "Run Backtest" posts it to /api/backtest. * Users write a JS strategy function; "Run Backtest" posts it to /api/backtest.
*/ */
import { useCallback, useEffect, useRef, useState } from 'react'; import { Suspense, lazy, useCallback, useEffect, useRef, useState } from 'react';
import Editor from '@monaco-editor/react';
import { Play, Save, Copy, RotateCcw } from 'lucide-react'; import { Play, Save, Copy, RotateCcw } from 'lucide-react';
import { getPlatformAccessToken } from '../../lib/authSession'; import { getPlatformAccessToken } from '../../lib/authSession';
import { createTradeProfile } from '../../lib/profileApi'; import { createTradeProfile } from '../../lib/profileApi';
@ -40,6 +39,8 @@ function strategy({ symbol, price, rsi, ema50, ema200, macd, volume }) {
} }
`; `;
const MonacoEditor = lazy(() => import('@monaco-editor/react'));
interface BacktestResult { interface BacktestResult {
trades?: number; trades?: number;
winRate?: number; winRate?: number;
@ -231,29 +232,31 @@ export function CodeStrategyEditor({
{/* Monaco editor */} {/* Monaco editor */}
<div style={{ border: '1px solid #E5E7EB', borderRadius: 10, overflow: 'hidden' }}> <div style={{ border: '1px solid #E5E7EB', borderRadius: 10, overflow: 'hidden' }}>
<Editor <Suspense fallback={<CodeEditorFallback />}>
height="380px" <MonacoEditor
defaultLanguage="javascript" height="380px"
value={code} defaultLanguage="javascript"
onChange={v => { value={code}
setCode(v ?? ''); onChange={v => {
setSaved(false); setCode(v ?? '');
}} setSaved(false);
theme="light" }}
options={{ theme="light"
fontSize: 13, options={{
minimap: { enabled: false }, fontSize: 13,
scrollBeyondLastLine: false, minimap: { enabled: false },
wordWrap: 'on', scrollBeyondLastLine: false,
lineNumbers: 'on', wordWrap: 'on',
renderLineHighlight: 'gutter', lineNumbers: 'on',
padding: { top: 12, bottom: 12 }, renderLineHighlight: 'gutter',
fontFamily: '"Fira Code", "Cascadia Code", "Consolas", monospace', padding: { top: 12, bottom: 12 },
fontLigatures: true, fontFamily: '"Fira Code", "Cascadia Code", "Consolas", monospace',
tabSize: 2, fontLigatures: true,
automaticLayout: true, tabSize: 2,
}} automaticLayout: true,
/> }}
/>
</Suspense>
</div> </div>
{/* Error */} {/* Error */}
@ -332,6 +335,27 @@ export function CodeStrategyEditor({
); );
} }
function CodeEditorFallback() {
return (
<div
role="status"
aria-label="Loading code editor"
style={{
height: 380,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #F8FAFC, #EEF2FF)',
color: '#4B5563',
fontSize: 13,
fontWeight: 700,
}}
>
Loading code editor
</div>
);
}
function toolBtn(bg: string, color: string, border: string): React.CSSProperties { function toolBtn(bg: string, color: string, border: string): React.CSSProperties {
return { return {
display: 'flex', alignItems: 'center', gap: 5, display: 'flex', alignItems: 'center', gap: 5,

View File

@ -1,15 +1,18 @@
import { useState } from 'react'; import { Suspense, lazy, useState } from 'react';
import { useAppContext } from '../context/AppContext'; import { useAppContext } from '../context/AppContext';
import { SignalsTab } from '../tabs/SignalsTab'; import { SignalsTab } from '../tabs/SignalsTab';
import { BacktestTab } from '../tabs/BacktestTab'; import { BacktestTab } from '../tabs/BacktestTab';
import { MyStrategiesTab } from '../tabs/MyStrategiesTab'; 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 { createTradeProfile } from '../lib/profileApi'; import { createTradeProfile } from '../lib/profileApi';
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel'; import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting'; type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting';
const CodeStrategyEditor = lazy(() =>
import('../components/strategy/CodeStrategyEditor').then(module => ({ default: module.CodeStrategyEditor })),
);
function buildVisualStrategyConfig(rules: VisualRule[]) { function buildVisualStrategyConfig(rules: VisualRule[]) {
return { return {
type: 'visual', type: 'visual',
@ -148,11 +151,13 @@ export function ResearchView() {
Write a custom strategy function in JavaScript. Click "Run Backtest" to test it against historical data. Write a custom strategy function in JavaScript. Click "Run Backtest" to test it against historical data.
</div> </div>
</div> </div>
<CodeStrategyEditor <Suspense fallback={<CodeStrategyEditorFallback />}>
symbol={activeSymbol || Object.keys(botState.symbols)[0] || 'SPY'} <CodeStrategyEditor
allocatedCapital={botState.settings?.totalCapital ?? 1000} symbol={activeSymbol || Object.keys(botState.symbols)[0] || 'SPY'}
riskPerTradePercent={botState.settings?.riskPerTrade ?? 1} allocatedCapital={botState.settings?.totalCapital ?? 1000}
/> riskPerTradePercent={botState.settings?.riskPerTrade ?? 1}
/>
</Suspense>
</div> </div>
)} )}
@ -166,3 +171,26 @@ export function ResearchView() {
</div> </div>
); );
} }
function CodeStrategyEditorFallback() {
return (
<div
role="status"
aria-label="Loading code strategy editor"
style={{
minHeight: 420,
border: '1px solid #E5E7EB',
borderRadius: 12,
background: 'linear-gradient(135deg, #F8FAFC, #EEF2FF)',
color: '#4B5563',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
fontWeight: 700,
}}
>
Loading code strategy editor
</div>
);
}