fix(B6): persist code strategy saves

Route CodeStrategyEditor saves through the canonical profile API instead of browser localStorage, carrying the active symbol plus ResearchView capital and risk defaults into an inactive saved code strategy profile.

Refs: docs/AUDIT_REDESIGN.md item B6.

Co-Authored-By: GPT-5 Codex <noreply@openai.com>
This commit is contained in:
Saravana Achu Mac 2026-05-04 16:34:45 -07:00
parent d80734288e
commit da79682ca3
3 changed files with 123 additions and 12 deletions

View File

@ -0,0 +1,74 @@
// @vitest-environment jsdom
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CodeStrategyEditor } from './CodeStrategyEditor';
const { createTradeProfileMock } = vi.hoisted(() => ({
createTradeProfileMock: vi.fn(),
}));
vi.mock('@monaco-editor/react', () => ({
default: ({ value, onChange }: any) => (
<textarea
aria-label="strategy code"
value={value}
onChange={(event) => onChange(event.currentTarget.value)}
/>
),
}));
vi.mock('../../lib/profileApi', () => ({
createTradeProfile: (...args: any[]) => createTradeProfileMock(...args),
}));
describe('CodeStrategyEditor save behavior', () => {
let setItemSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
createTradeProfileMock.mockReset();
createTradeProfileMock.mockResolvedValue({ id: 'profile-code-1' });
setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
});
afterEach(() => {
setItemSpy.mockRestore();
});
it('persists code strategies through createTradeProfile instead of localStorage', async () => {
const user = userEvent.setup();
render(<CodeStrategyEditor symbol="AAPL" allocatedCapital={2500} riskPerTradePercent={2} />);
fireEvent.change(screen.getByLabelText('strategy code'), {
target: { value: 'function strategy() { return { signal: "HOLD" }; }' },
});
await user.click(screen.getByRole('button', { name: /^save$/i }));
await waitFor(() => expect(createTradeProfileMock).toHaveBeenCalledTimes(1));
expect(createTradeProfileMock).toHaveBeenCalledWith({
name: 'Code Strategy (AAPL)',
symbols: 'AAPL',
allocated_capital: 2500,
risk_per_trade_percent: 2,
is_active: false,
strategy_config: {
type: 'code',
version: 1,
language: 'javascript',
code: 'function strategy() { return { signal: "HOLD" }; }',
},
});
expect(setItemSpy).not.toHaveBeenCalled();
expect(await screen.findByRole('button', { name: /saved!/i })).toBeInTheDocument();
});
it('surfaces save failures from the profile API', async () => {
const user = userEvent.setup();
createTradeProfileMock.mockRejectedValueOnce(new Error('Profile save failed'));
render(<CodeStrategyEditor symbol="MSFT" />);
await user.click(screen.getByRole('button', { name: /^save$/i }));
expect(await screen.findByText('Profile save failed')).toBeInTheDocument();
});
});

View File

@ -6,6 +6,7 @@ import { useState, useRef } from 'react';
import Editor from '@monaco-editor/react';
import { Play, Save, Copy, RotateCcw } from 'lucide-react';
import { getPlatformAccessToken } from '../../lib/authSession';
import { createTradeProfile } from '../../lib/profileApi';
import { tradingRuntime } from '../../lib/runtime';
import { createRequestId } from '../../../../shared/request-id.js';
@ -50,11 +51,18 @@ interface BacktestResult {
interface Props {
symbol: string;
allocatedCapital?: number;
riskPerTradePercent?: number;
}
export function CodeStrategyEditor({ symbol }: Props) {
export function CodeStrategyEditor({
symbol,
allocatedCapital = 1000,
riskPerTradePercent = 1,
}: Props) {
const [code, setCode] = useState(DEFAULT_TEMPLATE);
const [running, setRunning] = useState(false);
const [saving, setSaving] = useState(false);
const [result, setResult] = useState<BacktestResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [saved, setSaved] = useState(false);
@ -98,12 +106,30 @@ export function CodeStrategyEditor({ symbol }: Props) {
}
};
const handleSave = () => {
// Store in localStorage for now — real save would POST to /api/profiles
const key = `strategy_code_${symbol}_${Date.now()}`;
localStorage.setItem(key, code);
setSaved(true);
setTimeout(() => setSaved(false), 2500);
const handleSave = async () => {
setSaving(true);
setError(null);
try {
await createTradeProfile({
name: `Code Strategy (${symbol})`,
symbols: symbol,
allocated_capital: allocatedCapital,
risk_per_trade_percent: riskPerTradePercent,
is_active: false,
strategy_config: {
type: 'code',
version: 1,
language: 'javascript',
code,
},
});
setSaved(true);
} catch (err: any) {
setSaved(false);
setError(err?.message ?? 'Failed to save strategy');
} finally {
setSaving(false);
}
};
const handleReset = () => {
@ -135,9 +161,13 @@ export function CodeStrategyEditor({ symbol }: Props) {
style={toolBtn('#F9FAFB','#374151','#E5E7EB')}>
<RotateCcw size={13} /> Reset
</button>
<button onClick={handleSave}
style={toolBtn('#F0FDF4', saved ? '#16A34A' : '#374151', '#86EFAC')}>
<Save size={13} /> {saved ? 'Saved!' : 'Save'}
<button onClick={handleSave} disabled={saving}
style={{
...toolBtn('#F0FDF4', saved ? '#16A34A' : '#374151', '#86EFAC'),
opacity: saving ? 0.7 : 1,
cursor: saving ? 'wait' : 'pointer',
}}>
<Save size={13} /> {saving ? 'Saving...' : saved ? 'Saved!' : 'Save'}
</button>
<button onClick={handleRunBacktest} disabled={running}
style={{
@ -155,7 +185,10 @@ export function CodeStrategyEditor({ symbol }: Props) {
height="380px"
defaultLanguage="javascript"
value={code}
onChange={v => setCode(v ?? '')}
onChange={v => {
setCode(v ?? '');
setSaved(false);
}}
onMount={(editor) => { editorRef.current = editor; }}
theme="light"
options={{

View File

@ -128,7 +128,11 @@ 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'} />
<CodeStrategyEditor
symbol={activeSymbol || Object.keys(botState.symbols)[0] || 'SPY'}
allocatedCapital={botState.settings?.totalCapital ?? 1000}
riskPerTradePercent={botState.settings?.riskPerTrade ?? 1}
/>
</div>
)}