diff --git a/src/StrategyApiView.test.tsx b/src/StrategyApiView.test.tsx index 0ce14de..3b53ede 100644 --- a/src/StrategyApiView.test.tsx +++ b/src/StrategyApiView.test.tsx @@ -149,7 +149,6 @@ describe('Strategy API view', () => { findings: [{ severity: 'BLOCKING_ERROR', code: 'CONDITION_REQUIRED', message: '조건이 필요합니다.' }], completedAt: '2026-08-01T12:01:00Z', }), - getReleaseInputs: vi.fn(), releaseStrategy: vi.fn(), }; const catalog: BasicStrategyCatalog = { @@ -235,7 +234,7 @@ describe('Strategy API view', () => { heartbeatLease: vi.fn(), releaseLease: vi.fn().mockResolvedValue(undefined), saveDocument: vi.fn().mockResolvedValue({ ...document, editSequence: 1 }), validateStrategy: vi.fn().mockRejectedValue(new (await import('./api/strategies')).StrategyApiError(503, 'validation')), - getReleaseInputs: vi.fn(), releaseStrategy: vi.fn(), + releaseStrategy: vi.fn(), }; const catalogClient: StrategyCatalogClient = { getBasic: vi.fn().mockResolvedValue({ @@ -353,7 +352,7 @@ describe('Strategy API view', () => { .mockRejectedValueOnce(new (await import('./api/strategies')).StrategyApiError(409, 'lease')) .mockResolvedValue({ leaseToken: 'lease-token', expiresAt: '2026-08-08T00:02:00Z' }), heartbeatLease: vi.fn(), releaseLease: vi.fn().mockResolvedValue(undefined), saveDocument: vi.fn(), - validateStrategy: vi.fn(), getReleaseInputs: vi.fn(), releaseStrategy: vi.fn(), + validateStrategy: vi.fn(), releaseStrategy: vi.fn(), } as StrategyAuthoringClient; render( {}} strategyId="reloaded" authoringClient={authoringClient} catalogClient={null} />); @@ -379,7 +378,7 @@ describe('Strategy API view', () => { createBasic: vi.fn(), copyStrategy: vi.fn(), getDocument: vi.fn().mockResolvedValue(document), acquireLease: vi.fn().mockResolvedValue({ leaseToken: 'pagehide-token', expiresAt: '2026-08-08T00:02:00Z' }), heartbeatLease: vi.fn(), releaseLease: vi.fn().mockResolvedValue(undefined), saveDocument: vi.fn(), - validateStrategy: vi.fn(), getReleaseInputs: vi.fn(), releaseStrategy: vi.fn(), + validateStrategy: vi.fn(), releaseStrategy: vi.fn(), } as StrategyAuthoringClient; render( {}} strategyId="pagehide" authoringClient={authoringClient} catalogClient={null} />); @@ -430,25 +429,6 @@ describe('Strategy API view', () => { requestedEditSequence: 0, semanticHash: 'old-hash', }; - const releaseInputs = { - executionPolicies: [{ - version: 'policy-v1', brokerRulesVersion: 'market-v1', accountingRulesVersion: 'accounting-v1', - precisionRulesVersion: 'precision-v1', feePolicyId: 'fee-id', feeRateBps: 20, - buyingPowerBufferPolicyId: 'buffer-id', buyingPowerBufferBps: 1, - }, { - version: 'older-policy', brokerRulesVersion: 'older-market', accountingRulesVersion: 'older-accounting', - precisionRulesVersion: 'older-precision', feePolicyId: 'older-fee', feeRateBps: 30, - buyingPowerBufferPolicyId: 'older-buffer', buyingPowerBufferBps: 2, - }], - datasets: [{ - id: 'dataset-id', feedCode: 'alpaca-sip', dataLayer: 'ADJUSTED', resolution: '1m', - periodStart: '2025-01-01', periodEnd: '2026-01-01', schemaVersion: 'market-bars-v2', - }, { - id: 'older-dataset', feedCode: 'alpaca-sip', dataLayer: 'ADJUSTED', resolution: '1m', - periodStart: '2024-01-01', periodEnd: '2025-01-01', schemaVersion: 'market-bars-v2', - }], - observedAt: '2026-08-07T12:01:00Z', - }; const authoringClient: StrategyAuthoringClient = { createBasic: vi.fn(), copyStrategy: vi.fn(), getDocument: vi.fn().mockResolvedValue(document), acquireLease: vi.fn().mockResolvedValue({ leaseToken: 'lease-token', expiresAt: '2026-08-07T12:02:00Z' }), @@ -465,7 +445,6 @@ describe('Strategy API view', () => { })), getCurrentValidations: vi.fn().mockResolvedValue([loadedValidation]), validateStrategy: vi.fn().mockResolvedValue(validation), - getReleaseInputs: vi.fn().mockResolvedValue(releaseInputs), releaseStrategy: vi.fn().mockResolvedValue({ botId: 'bot-id', backtestLane: 'BASIC' }), }; const element = (elementCode: string) => ({ @@ -510,18 +489,17 @@ describe('Strategy API view', () => { }); await user.click(screen.getByRole('button', { name: '개인 봇 출시' })); const launchDialog = await screen.findByRole('dialog', { name: '개인 운용 봇 출시' }); - await waitFor(() => expect(authoringClient.getReleaseInputs).toHaveBeenCalledTimes(1)); expect(within(launchDialog).queryByText('실행 정책')).not.toBeInTheDocument(); expect(within(launchDialog).queryByText('공식 백테스트 데이터')).not.toBeInTheDocument(); expect(within(launchDialog).queryByRole('combobox')).not.toBeInTheDocument(); await user.click(within(launchDialog).getByRole('button', { name: '봇 출시하기' })); - await waitFor(() => expect(authoringClient.releaseStrategy).toHaveBeenCalledWith(strategyId, expect.objectContaining({ + await waitFor(() => expect(authoringClient.releaseStrategy).toHaveBeenCalledWith(strategyId, { validationRunId: validation.validationRunId, initialCashAmount: '100000.00000000', - datasetManifestId: 'dataset-id', executionPolicyVersion: 'policy-v1', - feePolicyId: 'fee-id', buyingPowerBufferPolicyId: 'buffer-id', - }))); + budgetCapBps: 10000, + candidateConflictPolicy: { policy: 'FIRST_WINS' }, + })); expect(onLaunchBot).toHaveBeenCalledWith({ name: '', description: '', botId: 'bot-id' }); }); @@ -530,7 +508,7 @@ describe('Strategy API view', () => { createBasic: vi.fn(), copyStrategy: vi.fn(), getDocument: vi.fn().mockRejectedValue(new (await import('./api/strategies')).StrategyApiError(404, 'document')), - acquireLease: vi.fn(), heartbeatLease: vi.fn(), releaseLease: vi.fn(), saveDocument: vi.fn(), validateStrategy: vi.fn(), getReleaseInputs: vi.fn(), releaseStrategy: vi.fn(), + acquireLease: vi.fn(), heartbeatLease: vi.fn(), releaseLease: vi.fn(), saveDocument: vi.fn(), validateStrategy: vi.fn(), releaseStrategy: vi.fn(), } as StrategyAuthoringClient; render( {}} strategyId="missing" authoringClient={authoringClient} catalogClient={null} />); @@ -562,7 +540,6 @@ describe('Strategy API view', () => { releaseLease: vi.fn().mockResolvedValue(undefined), saveDocument: vi.fn(), validateStrategy: vi.fn(), - getReleaseInputs: vi.fn(), releaseStrategy: vi.fn(), } as StrategyAuthoringClient; @@ -595,7 +572,6 @@ describe('Strategy API view', () => { releaseLease: vi.fn().mockResolvedValue(undefined), saveDocument: vi.fn(), validateStrategy: vi.fn(), - getReleaseInputs: vi.fn(), releaseStrategy: vi.fn(), } as StrategyAuthoringClient; @@ -626,7 +602,6 @@ describe('Strategy API view', () => { releaseLease: vi.fn().mockResolvedValue(undefined), saveDocument: vi.fn(), validateStrategy: vi.fn(), - getReleaseInputs: vi.fn(), releaseStrategy: vi.fn(), } as StrategyAuthoringClient; diff --git a/src/api/strategies.test.ts b/src/api/strategies.test.ts index 8e18320..2a4947b 100644 --- a/src/api/strategies.test.ts +++ b/src/api/strategies.test.ts @@ -207,37 +207,21 @@ describe('strategy authoring API client', () => { })); }); - it('loads server-owned release inputs and creates an immutable release', async () => { - const inputs = { - executionPolicies: [{ - version: 'policy-v1', brokerRulesVersion: 'market-v1', accountingRulesVersion: 'accounting-v1', - precisionRulesVersion: 'precision-v1', feePolicyId: 'fee-id', feeRateBps: 20, - buyingPowerBufferPolicyId: 'buffer-id', buyingPowerBufferBps: 1, - }], - datasets: [{ - id: 'dataset-id', feedCode: 'alpaca-sip', dataLayer: 'ADJUSTED', resolution: '1m', - periodStart: '2025-01-01', periodEnd: '2026-01-01', schemaVersion: 'market-bars-v2', - }], - observedAt: '2026-08-07T12:00:00Z', - }; + it('creates an immutable release without client-selected policy or datasets', async () => { const release = { validationRunId: 'validation-id', initialCashAmount: '100000', budgetCapBps: 10000, - brokerRulesVersion: 'market-v1', accountingRulesVersion: 'accounting-v1', - precisionRulesVersion: 'precision-v1', feePolicyId: 'fee-id', - buyingPowerBufferPolicyId: 'buffer-id', datasetManifestId: 'dataset-id', - executionPolicyVersion: 'policy-v1', candidateConflictPolicy: { policy: 'FIRST_WINS' }, + candidateConflictPolicy: { policy: 'FIRST_WINS' }, }; - const fetchImpl = vi.fn() - .mockResolvedValueOnce(new Response(JSON.stringify(inputs), { status: 200 })) - .mockResolvedValueOnce(new Response(JSON.stringify({ botId: 'bot-id', backtestLane: 'BASIC' }), { status: 201 })); + const fetchImpl = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ botId: 'bot-id', backtestLane: 'BASIC' }), { status: 201 }), + ); const client = createStrategyAuthoringClient({ fetchImpl }); - await expect(client.getReleaseInputs()).resolves.toEqual(inputs); await expect(client.releaseStrategy(document.strategyId, release)).resolves.toEqual({ botId: 'bot-id', backtestLane: 'BASIC' }); expect(fetchImpl.mock.calls.map(([url, init]) => [url, (init as RequestInit).method ?? 'GET'])).toEqual([ - ['/api/v1/strategy-release-inputs', 'GET'], [`/api/v1/strategies/${document.strategyId}/releases`, 'POST'], ]); + expect(JSON.parse((fetchImpl.mock.calls[0][1] as RequestInit).body as string)).toEqual(release); }); it('exposes conflict status without leaking a lease token', async () => { diff --git a/src/api/strategies.ts b/src/api/strategies.ts index 4a31a75..9dd6859 100644 --- a/src/api/strategies.ts +++ b/src/api/strategies.ts @@ -115,13 +115,6 @@ export interface ReleaseStrategyInput { validationRunId: string; initialCashAmount: string; budgetCapBps: number; - brokerRulesVersion: string; - accountingRulesVersion: string; - precisionRulesVersion: string; - feePolicyId: string; - buyingPowerBufferPolicyId: string; - datasetManifestId: string; - executionPolicyVersion: string; candidateConflictPolicy: Record; } @@ -137,7 +130,6 @@ export interface StrategyAuthoringClient { previewValidation?(strategyId: string, input: PreviewStrategyValidationInput, signal?: AbortSignal): Promise; getCurrentValidations?(signal?: AbortSignal): Promise; validateStrategy(strategyId: string, catalogId: string, signal?: AbortSignal): Promise; - getReleaseInputs(signal?: AbortSignal): Promise; releaseStrategy(strategyId: string, input: ReleaseStrategyInput, signal?: AbortSignal): Promise<{ botId: string; backtestLane: string }>; } @@ -328,10 +320,6 @@ export function createStrategyAuthoringClient({ ); return readValidation(await response.json()); }, - async getReleaseInputs(signal) { - const response = await request('/api/v1/strategy-release-inputs', 'Strategy release inputs', { signal }); - return readStrategyReleaseInputs(await response.json()); - }, async releaseStrategy(strategyId, input, signal) { const response = await request( `/api/v1/strategies/${encodeURIComponent(strategyId)}/releases`, diff --git a/src/views/StrategyViews.tsx b/src/views/StrategyViews.tsx index af953d4..1c00ca3 100644 --- a/src/views/StrategyViews.tsx +++ b/src/views/StrategyViews.tsx @@ -33,7 +33,7 @@ import { } from '../lib/strategyCanvasLayout'; import type { CanvasPoint, CanvasSize, CardMoveGesture } from '../lib/strategyCanvasLayout'; import { defaultStrategyAuthoringClient, defaultStrategyCatalogClient, defaultStrategyLibraryClient, STRATEGY_LIBRARY_PAGE_SIZE, StrategyApiError } from '../api/strategies'; -import type { BasicCatalogInstrument, BasicStrategyCatalog, StrategyAuthoringClient, StrategyCatalogClient, StrategyLibraryClient, StrategyLibraryItem, StrategyReleaseInputs, StrategyValidationResult } from '../api/strategies'; +import type { BasicCatalogInstrument, BasicStrategyCatalog, StrategyAuthoringClient, StrategyCatalogClient, StrategyLibraryClient, StrategyLibraryItem, StrategyValidationResult } from '../api/strategies'; type EditorMode = 'basic' | 'pro'; type EditorLoadFailure = 'sign-in' | 'missing' | 'conflict' | 'transport' | 'unreadable'; @@ -1675,8 +1675,6 @@ export function BasicEditor({ goBack, openEditor, onLaunchBot, blank = false, st const [launchDialogOpen, setLaunchDialogOpen] = useState(false); const [botName, setBotName] = useState(''); const [botDescription, setBotDescription] = useState(''); - const [releaseInputs, setReleaseInputs] = useState(null); - const [releaseInputsPending, setReleaseInputsPending] = useState(false); const [releasePending, setReleasePending] = useState(false); const [releaseError, setReleaseError] = useState(null); const [initialCashAmount, setInitialCashAmount] = useState('100000'); @@ -2352,7 +2350,7 @@ export function BasicEditor({ goBack, openEditor, onLaunchBot, blank = false, st setReleaseError(null); }; - const preparePersonalBotLaunch = async () => { + const preparePersonalBotLaunch = () => { if (!isLaunchable) { const firstIssue = validationIssues[0]?.message ?? (serverValidation?.status === 'INVALID' @@ -2374,32 +2372,15 @@ export function BasicEditor({ goBack, openEditor, onLaunchBot, blank = false, st setSaveFeedback(null); setLaunchDialogOpen(true); setReleaseError(null); - if (!strategyId || !authoringClient) { - setAnnouncement('개인 운용 봇 정보를 입력해 주세요.'); - return; - } - setReleaseInputsPending(true); - setReleaseInputs(null); - try { - const inputs = await authoringClient.getReleaseInputs(); - setReleaseInputs(inputs); - if (inputs.executionPolicies.length === 0 || inputs.datasets.length === 0) { - setReleaseError('현재 봇을 출시할 수 없습니다. 잠시 후 다시 시도해 주세요.'); - } - setAnnouncement('봇 출시 준비를 마쳤습니다.'); - } catch { - setReleaseError('봇 출시를 준비하지 못했습니다. 잠시 후 다시 시도해 주세요.'); - } finally { - setReleaseInputsPending(false); - } + setAnnouncement(strategyId && authoringClient + ? '봇 출시 준비를 마쳤습니다.' + : '개인 운용 봇 정보를 입력해 주세요.'); }; const launchPersonalBot = async (event: FormEvent) => { event.preventDefault(); if (strategyId && authoringClient) { - const policy = releaseInputs?.executionPolicies[0]; - const dataset = releaseInputs?.datasets[0]; - if (!savedValidation || savedValidation.status !== 'VALID' || !policy || !dataset) return; + if (!savedValidation || savedValidation.status !== 'VALID') return; const cash = fixedScaleUsdAmount(initialCashAmount); const budget = Number(budgetPercent); if (!cash || !Number.isFinite(budget) || budget <= 0 || budget > 100) { @@ -2413,13 +2394,6 @@ export function BasicEditor({ goBack, openEditor, onLaunchBot, blank = false, st validationRunId: savedValidation.validationRunId, initialCashAmount: cash, budgetCapBps: Math.round(budget * 100), - brokerRulesVersion: policy.brokerRulesVersion, - accountingRulesVersion: policy.accountingRulesVersion, - precisionRulesVersion: policy.precisionRulesVersion, - feePolicyId: policy.feePolicyId, - buyingPowerBufferPolicyId: policy.buyingPowerBufferPolicyId, - datasetManifestId: dataset.id, - executionPolicyVersion: policy.version, candidateConflictPolicy: { policy: 'FIRST_WINS' }, }); setLaunchDialogOpen(false); @@ -4133,9 +4107,8 @@ export function BasicEditor({ goBack, openEditor, onLaunchBot, blank = false, st
{ void launchPersonalBot(event); }}> {strategyId && authoringClient ? <> - {releaseInputsPending && } {releaseError && } - {!releaseInputsPending && releaseInputs && <> + <> - } + : <>