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();
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" }; }' },
});
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 () => {
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));
});
@ -112,7 +112,7 @@ describe('CodeStrategyEditor save behavior', () => {
vi.stubGlobal('fetch', fetchMock);
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));
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.
* Users write a JS strategy function; "Run Backtest" posts it to /api/backtest.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import Editor from '@monaco-editor/react';
import { Suspense, lazy, useCallback, useEffect, useRef, useState } from 'react';
import { Play, Save, Copy, RotateCcw } from 'lucide-react';
import { getPlatformAccessToken } from '../../lib/authSession';
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 {
trades?: number;
winRate?: number;
@ -231,29 +232,31 @@ export function CodeStrategyEditor({
{/* Monaco editor */}
<div style={{ border: '1px solid #E5E7EB', borderRadius: 10, overflow: 'hidden' }}>
<Editor
height="380px"
defaultLanguage="javascript"
value={code}
onChange={v => {
setCode(v ?? '');
setSaved(false);
}}
theme="light"
options={{
fontSize: 13,
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: 'on',
lineNumbers: 'on',
renderLineHighlight: 'gutter',
padding: { top: 12, bottom: 12 },
fontFamily: '"Fira Code", "Cascadia Code", "Consolas", monospace',
fontLigatures: true,
tabSize: 2,
automaticLayout: true,
}}
/>
<Suspense fallback={<CodeEditorFallback />}>
<MonacoEditor
height="380px"
defaultLanguage="javascript"
value={code}
onChange={v => {
setCode(v ?? '');
setSaved(false);
}}
theme="light"
options={{
fontSize: 13,
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: 'on',
lineNumbers: 'on',
renderLineHighlight: 'gutter',
padding: { top: 12, bottom: 12 },
fontFamily: '"Fira Code", "Cascadia Code", "Consolas", monospace',
fontLigatures: true,
tabSize: 2,
automaticLayout: true,
}}
/>
</Suspense>
</div>
{/* 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 {
return {
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 { SignalsTab } from '../tabs/SignalsTab';
import { BacktestTab } from '../tabs/BacktestTab';
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';
const CodeStrategyEditor = lazy(() =>
import('../components/strategy/CodeStrategyEditor').then(module => ({ default: module.CodeStrategyEditor })),
);
function buildVisualStrategyConfig(rules: VisualRule[]) {
return {
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.
</div>
</div>
<CodeStrategyEditor
symbol={activeSymbol || Object.keys(botState.symbols)[0] || 'SPY'}
allocatedCapital={botState.settings?.totalCapital ?? 1000}
riskPerTradePercent={botState.settings?.riskPerTrade ?? 1}
/>
<Suspense fallback={<CodeStrategyEditorFallback />}>
<CodeStrategyEditor
symbol={activeSymbol || Object.keys(botState.symbols)[0] || 'SPY'}
allocatedCapital={botState.settings?.totalCapital ?? 1000}
riskPerTradePercent={botState.settings?.riskPerTrade ?? 1}
/>
</Suspense>
</div>
)}
@ -166,3 +171,26 @@ export function ResearchView() {
</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>
);
}