diff --git a/src/RuntimeHonesty.test.tsx b/src/RuntimeHonesty.test.tsx index cc0dcf3..3c21881 100644 --- a/src/RuntimeHonesty.test.tsx +++ b/src/RuntimeHonesty.test.tsx @@ -296,6 +296,10 @@ describe('production runtime honesty', () => { />); expect(await screen.findByText('시장 데이터')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '4시간' })); + await waitFor(() => expect(marketDataClient.getRecentBars).toHaveBeenCalledWith( + 'instrument-aapl', '4h', 400, expect.any(AbortSignal), + )); fireEvent.click(screen.getByRole('button', { name: 'MSFT 차트 보기' })); expect(await screen.findByText('시세 데이터 대기')).toBeInTheDocument(); expect(screen.queryByText('시장 데이터')).not.toBeInTheDocument(); diff --git a/src/StrategyApiView.test.tsx b/src/StrategyApiView.test.tsx index f1d4fd0..0ce14de 100644 --- a/src/StrategyApiView.test.tsx +++ b/src/StrategyApiView.test.tsx @@ -188,7 +188,7 @@ describe('Strategy API view', () => { await user.click(screen.getByRole('button', { name: 'PARTITION 01 전략 미리보기' })); expect(await screen.findByTestId('strategy-preview-canvas')).toBeInTheDocument(); expect(marketDataClient.getRecentBars).toHaveBeenCalledWith( - 'spy-id', '1h', 300, expect.any(AbortSignal), + 'spy-id', '1h', 400, expect.any(AbortSignal), ); const save = screen.getByRole('button', { name: '저장' }); await waitFor(() => expect(save).toBeEnabled()); diff --git a/src/StrategyPreview.test.tsx b/src/StrategyPreview.test.tsx index 16adc3c..e1f4b4f 100644 --- a/src/StrategyPreview.test.tsx +++ b/src/StrategyPreview.test.tsx @@ -5,6 +5,7 @@ import { BasicEditor } from './views/StrategyViews'; import { StrategyPreviewChart } from './components/StrategyPreviewChart'; import { LanguageProvider } from './lib/i18n'; import { + PREVIEW_MAX_CANDLES, PREVIEW_WINDOW, bollinger, evaluateStrategyPreview, @@ -117,6 +118,20 @@ describe('strategy preview engine', () => { expect(preview.summary.winRate).not.toBeNull(); }); + test('bounds the fast local preview to the latest 400 server bars', () => { + const supplied = candlesFrom(Array.from({ length: 450 }, (_, index) => 100 + index)); + + const preview = evaluateStrategyPreview({ + symbol: 'AAPL', + candles: supplied, + flows: flowsOf(BUY_BLOCKS, SELL_BLOCKS), + }); + + expect(PREVIEW_MAX_CANDLES).toBe(400); + expect(preview.candles).toHaveLength(400); + expect(preview.candles[0]).toBe(supplied[50]); + }); + test('is deterministic for the same symbol and timeframe', () => { const first = evaluateStrategyPreview({ symbol: 'MSFT', timeframeSeconds: 3600, flows: flowsOf(BUY_BLOCKS, SELL_BLOCKS) }); const second = evaluateStrategyPreview({ symbol: 'MSFT', timeframeSeconds: 3600, flows: flowsOf(BUY_BLOCKS, SELL_BLOCKS) }); @@ -213,6 +228,21 @@ describe('strategy preview engine', () => { }); describe('Basic editor partition preview state', () => { + test('renders fast supported buy and sell markers with an estimate disclaimer', () => { + render( {}} + />); + + expect(screen.getAllByTestId('preview-marker-buy').length).toBeGreaterThan(0); + expect(screen.getAllByTestId('preview-marker-sell').length).toBeGreaterThan(0); + expect(screen.getByText('빠르게 계산할 수 있는 조건만 반영한 예상 결과이며 실제 실행 결과와 다를 수 있습니다.')) + .toBeInTheDocument(); + }); + test('always explains the buy and sell conditions and warns when bars are insufficient', () => { render( { baseUrl: 'https://api.example.com/', fetchImpl, getAccessToken: () => 'session-token', }); - const result = await client.getRecentBars('instrument-1', '4h', 300); + const result = await client.getRecentBars('instrument-1', '4h'); expect(result.bars[0].close).toBe(210.5); expect(fetchImpl).toHaveBeenCalledWith( - 'https://api.example.com/api/v1/market-data/instruments/instrument-1/bars?timeframe=4h&limit=300', + 'https://api.example.com/api/v1/market-data/instruments/instrument-1/bars?timeframe=4h&limit=400', expect.objectContaining({ credentials: 'include', headers: expect.objectContaining({ Authorization: 'Bearer session-token' }), diff --git a/src/api/marketData.ts b/src/api/marketData.ts index f3e0e81..393ca83 100644 --- a/src/api/marketData.ts +++ b/src/api/marketData.ts @@ -77,7 +77,7 @@ export function createMarketDataClient({ `/api/v1/market-data/instruments/${encodeURIComponent(instrumentId)}/bars`; return { - async getRecentBars(instrumentId, timeframe = '30m', limit = 300, signal) { + async getRecentBars(instrumentId, timeframe = '30m', limit = 400, signal) { const response = await fetchImpl( `${root}${path(instrumentId)}?timeframe=${encodeURIComponent(timeframe)}&limit=${encodeURIComponent(String(limit))}`, { credentials: 'include', headers: headers('application/json'), signal }, diff --git a/src/components/StrategyPreviewChart.tsx b/src/components/StrategyPreviewChart.tsx index 39e6035..709ac03 100644 --- a/src/components/StrategyPreviewChart.tsx +++ b/src/components/StrategyPreviewChart.tsx @@ -292,5 +292,8 @@ export function StrategyPreviewChart({ {preview.unsupported.length > 0 &&

{t(`${preview.unsupported.join(', ')} 블록은 계산할 수 없어 해당 플로우의 신호를 표시하지 않아요`)}

} + + {t('빠르게 계산할 수 있는 조건만 반영한 예상 결과이며 실제 실행 결과와 다를 수 있습니다.')} + ; } diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx index 9512e7f..f292e97 100644 --- a/src/lib/i18n.tsx +++ b/src/lib/i18n.tsx @@ -1292,6 +1292,7 @@ const english: Record = { '신호를 계산하기에 최근 데이터가 부족합니다.': 'There is not enough recent data to calculate signals.', '신호만 강조': 'signals only', '최근 1개월 종가와 신호': 'closing prices and signals over the past month', + '빠르게 계산할 수 있는 조건만 반영한 예상 결과이며 실제 실행 결과와 다를 수 있습니다.': 'This estimate uses only conditions that can be calculated quickly and may differ from actual execution results.', /* '최근 1개월'은 백테스트 기간 칩에 이미 있다. */ '계산할 수 있는 지표 블록이 없어요': 'No indicator block here can be evaluated', '블록은 계산에서 제외했어요': 'blocks are excluded from the calculation', diff --git a/src/lib/strategyPreview.ts b/src/lib/strategyPreview.ts index d203cbc..3441100 100644 --- a/src/lib/strategyPreview.ts +++ b/src/lib/strategyPreview.ts @@ -231,6 +231,9 @@ export const PREVIEW_WINDOW = { count: 150, } as const; +/* 브라우저에서 즉시 다시 계산할 수 있도록 실제 시세 입력도 최근 400봉으로 제한한다. */ +export const PREVIEW_MAX_CANDLES = 400; + /* ---------- 지표 계산 ---------------------------------------------------- */ type Series = Array; @@ -1036,7 +1039,10 @@ export const evaluateStrategyPreview = ({ timeframeSeconds = PREVIEW_WINDOW.seconds, candleCount = PREVIEW_WINDOW.count, }: PreviewInput): StrategyPreview => { - const candles = suppliedCandles ?? generatePreviewCandles(symbol, timeframeSeconds, candleCount); + const sourceCandles = suppliedCandles ?? generatePreviewCandles(symbol, timeframeSeconds, candleCount); + const candles = sourceCandles.length > PREVIEW_MAX_CANDLES + ? sourceCandles.slice(-PREVIEW_MAX_CANDLES) + : sourceCandles; const unsupported = new Set(); /* 한 컨테이너 안의 조건은 런타임과 똑같이 AND다. 하나라도 해석할 수 없는 블록이 있으면 그 플로우는 fail-closed로 신호를 만들지 않는다. */ diff --git a/src/styles/balanced.css b/src/styles/balanced.css index 8ffb144..b10c0b9 100644 --- a/src/styles/balanced.css +++ b/src/styles/balanced.css @@ -9012,6 +9012,14 @@ small.dashboard-bot-scope { font-size: var(--fs-micro); line-height: 1.4; } +.strategy-preview-disclaimer { + display: block; + margin: 0; + color: var(--text-faint); + font-size: var(--fs-micro); + line-height: 1.4; + word-break: keep-all; +} @media (max-width: 800px) { .variant-balanced[data-design="signal-studio"] .strategy-preview-card { display: none; } } diff --git a/src/views/BotsView.tsx b/src/views/BotsView.tsx index 0c59c23..80e9a21 100644 --- a/src/views/BotsView.tsx +++ b/src/views/BotsView.tsx @@ -1631,7 +1631,7 @@ export function BotsView({ const refreshSnapshot = async () => { try { const snapshot = await marketDataClient.getRecentBars( - selectedMarketInstrument.instrumentId, chartTimeframe, 300, controller.signal, + selectedMarketInstrument.instrumentId, chartTimeframe, 400, controller.signal, ); publish(snapshot.bars); setMarketDataError(null); diff --git a/src/views/StrategyViews.tsx b/src/views/StrategyViews.tsx index 9f995c3..af953d4 100644 --- a/src/views/StrategyViews.tsx +++ b/src/views/StrategyViews.tsx @@ -3062,7 +3062,7 @@ export function BasicEditor({ goBack, openEditor, onLaunchBot, blank = false, st const controller = new AbortController(); setPreviewPending(true); setPreviewError(null); - void marketDataClient.getRecentBars(instrumentId, resolutionCode(previewSection.timeframe), 300, controller.signal) + void marketDataClient.getRecentBars(instrumentId, resolutionCode(previewSection.timeframe), 400, controller.signal) .then((snapshot) => { setPreviewCandles(snapshot.bars.map((bar) => ({ time: Math.floor(new Date(bar.occurredAt).getTime() / 1000),