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:
parent
d80734288e
commit
da79682ca3
74
web/src/components/strategy/CodeStrategyEditor.dom.test.tsx
Normal file
74
web/src/components/strategy/CodeStrategyEditor.dom.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -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={{
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user