Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/RuntimeHonesty.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/StrategyApiView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
30 changes: 30 additions & 0 deletions src/StrategyPreview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) });
Expand Down Expand Up @@ -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(<StrategyPreviewChart
partitionLabel="PARTITION 01"
symbols={['AAPL']}
flows={flowsOf(BUY_BLOCKS, SELL_BLOCKS)}
candles={generatePreviewCandles('AAPL', 1800, 400)}
onClose={() => {}}
/>);

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(<StrategyPreviewChart
partitionLabel="PARTITION 01"
Expand Down
4 changes: 2 additions & 2 deletions src/api/marketData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ describe('market data client', () => {
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' }),
Expand Down
2 changes: 1 addition & 1 deletion src/api/marketData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
3 changes: 3 additions & 0 deletions src/components/StrategyPreviewChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,5 +292,8 @@ export function StrategyPreviewChart({
{preview.unsupported.length > 0 && <p className="strategy-preview-warning">
{t(`${preview.unsupported.join(', ')} 블록은 계산할 수 없어 해당 플로우의 신호를 표시하지 않아요`)}
</p>}
<small className="strategy-preview-disclaimer">
{t('빠르게 계산할 수 있는 조건만 반영한 예상 결과이며 실제 실행 결과와 다를 수 있습니다.')}
</small>
</aside>;
}
1 change: 1 addition & 0 deletions src/lib/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,7 @@ const english: Record<string, string> = {
'신호를 계산하기에 최근 데이터가 부족합니다.': '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',
Expand Down
8 changes: 7 additions & 1 deletion src/lib/strategyPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ export const PREVIEW_WINDOW = {
count: 150,
} as const;

/* 브라우저에서 즉시 다시 계산할 수 있도록 실제 시세 입력도 최근 400봉으로 제한한다. */
export const PREVIEW_MAX_CANDLES = 400;

/* ---------- 지표 계산 ---------------------------------------------------- */

type Series = Array<number | null>;
Expand Down Expand Up @@ -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<string>();
/* 한 컨테이너 안의 조건은 런타임과 똑같이 AND다. 하나라도 해석할 수 없는
블록이 있으면 그 플로우는 fail-closed로 신호를 만들지 않는다. */
Expand Down
8 changes: 8 additions & 0 deletions src/styles/balanced.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Expand Down
2 changes: 1 addition & 1 deletion src/views/BotsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/views/StrategyViews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading