/** * GoldenChart Backend API 클라이언트. * Spring Boot 백엔드와 통신한다. * * 환경변수 VITE_API_BASE_URL 로 백엔드 URL 설정 (기본: http://localhost:8080/api) */ /** 백엔드 REST base (Docker 빌드: `/api`, 로컬 Vite: `.env` 또는 localhost:8080) */ export const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080/api'; /** STOMP WebSocket 연결에 사용할 HTTP base URL (api 경로 제거) */ export function getApiBase(): string { return API_BASE.replace(/\/api$/, ''); } /** * STOMP SockJS 엔드포인트. * Spring Boot context-path가 /api 이므로 반드시 /api/ws/trading 경로를 사용한다. */ export function getStompSockJsUrl(): string { const base = API_BASE.replace(/\/$/, ''); const path = base.endsWith('/api') ? `${base}/ws/trading` : `${base}/api/ws/trading`; // SockJS: 배포 환경에서 상대 경로만 쓰면 프록시/오리진 불일치 시 연결 실패가 난다. if (typeof window !== 'undefined' && window.location?.origin) { return `${window.location.origin}${path.startsWith('/') ? path : `/${path}`}`; } return path; } // UUID 생성 — HTTPS/localhost: crypto.randomUUID() 사용, HTTP: 폴리필 function generateUUID(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } // HTTP 환경(비보안 컨텍스트) 폴리필 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } // 기기 ID (localStorage 영속) function getDeviceId(): string { let id = localStorage.getItem('gc_device_id'); if (!id) { id = generateUUID(); localStorage.setItem('gc_device_id', id); } return id; } function authHeaders(): Record { const headers: Record = { 'Content-Type': 'application/json', 'x-device-id': getDeviceId(), }; const userId = localStorage.getItem('gc_user_id'); if (userId) headers['x-user-id'] = userId; return headers; } /** 로그인 API 오류 시 메시지 throw */ async function requestOrThrow(path: string, init?: RequestInit): Promise { const res = await fetch(`${API_BASE}${path}`, { ...init, headers: { ...authHeaders(), ...(init?.headers ?? {}) }, }); const text = await res.text().catch(() => ''); if (!res.ok) { let msg = `요청 실패 (${res.status})`; try { const j = JSON.parse(text) as { message?: string }; if (j.message) msg = j.message; } catch { /* plain text */ } if (!text.startsWith('{') && text) msg = text.slice(0, 200); throw new Error(msg); } if (res.status === 204 || !text) return undefined as T; return JSON.parse(text) as T; } async function request(path: string, init?: RequestInit): Promise { try { const res = await fetch(`${API_BASE}${path}`, { ...init, headers: { ...authHeaders(), ...(init?.headers ?? {}) }, }); if (res.status === 204) return null; if (!res.ok) { console.error(`[backendApi] ${init?.method ?? 'GET'} ${path} → ${res.status}`); return null; } return (await res.json()) as T; } catch (e) { console.error(`[backendApi] network error ${path}`, e); return null; } } // ───────────────────────────────────────────────────────────────────────────── // 타입 정의 (backend DTO 와 동기화) // ───────────────────────────────────────────────────────────────────────────── export interface SlotDto { slotIndex: number; symbol: string | null; timeframe: string | null; chartType: string; theme: string; mode: string; logScale: boolean; drawingsLocked: boolean; drawingsVisible: boolean; indicators: unknown; // IndicatorConfig[] drawings: unknown; // Drawing[] paneLayout: unknown; // PaneLayoutEntry[] mainChartStyle: unknown; // MainChartStyle } export interface WorkspaceDto { id: number; layoutId: string; syncOptions: unknown; slots: SlotDto[]; } export interface WatchlistItem { id: number; symbol: string; koreanName: string | null; englishName: string | null; displayOrder: number; } export interface HoldingsItem extends WatchlistItem { avgPrice: number | null; quantity: number | null; } export interface IndicatorPoint { time: number; value: number | null; color: string | null; } export interface IndicatorCalcResponse { symbol: string; timeframe: string; indicatorType: string; times: number[]; values: Record; } // ───────────────────────────────────────────────────────────────────────────── // 워크스페이스 API // ───────────────────────────────────────────────────────────────────────────── /** 프론트 시작 시 DB에서 전체 설정 로드 */ export async function loadWorkspace(): Promise { return request('/chart/workspace'); } /** 전체 워크스페이스 저장 */ export async function saveWorkspace(data: { layoutId: string; syncOptions: unknown; slots: unknown[]; }): Promise { return request('/chart/workspace', { method: 'POST', body: JSON.stringify(data), }); } /** 레이아웃만 변경 */ export async function updateLayout(layoutId: string, syncOptions?: unknown): Promise { await request('/chart/workspace/layout', { method: 'PUT', body: JSON.stringify({ layoutId, syncOptions }), }); } /** 특정 슬롯 저장 */ export async function saveSlot(slotIndex: number, slotData: Partial): Promise { await request(`/chart/slot/${slotIndex}`, { method: 'PUT', body: JSON.stringify(slotData), }); } // ───────────────────────────────────────────────────────────────────────────── // 관심종목 API // ───────────────────────────────────────────────────────────────────────────── export async function loadWatchlist(): Promise { return (await request('/watchlist')) ?? []; } export async function addWatchlistItem(item: { symbol: string; koreanName?: string; englishName?: string; }): Promise { return request('/watchlist', { method: 'POST', body: JSON.stringify(item), }); } export async function removeWatchlistItem(symbol: string): Promise { await request(`/watchlist/${encodeURIComponent(symbol)}`, { method: 'DELETE' }); } export async function reorderWatchlist( items: { symbol: string; displayOrder: number }[] ): Promise { await request('/watchlist/order', { method: 'PUT', body: JSON.stringify(items) }); } // ───────────────────────────────────────────────────────────────────────────── // 보유종목 API // ───────────────────────────────────────────────────────────────────────────── export async function loadHoldings(): Promise { return (await request('/holdings')) ?? []; } export async function upsertHoldingsItem(item: { symbol: string; koreanName?: string; englishName?: string; avgPrice?: number; quantity?: number; }): Promise { return request('/holdings', { method: 'POST', body: JSON.stringify(item), }); } export async function removeHoldingsItem(symbol: string): Promise { await request(`/holdings/${encodeURIComponent(symbol)}`, { method: 'DELETE' }); } // ───────────────────────────────────────────────────────────────────────────── // 지표 계산 API (백엔드 Ta4j 사용) // ───────────────────────────────────────────────────────────────────────────── export async function calculateIndicator(req: { symbol?: string; timeframe?: string; indicatorType: string; params?: Record; bars?: { time: number; open: number; high: number; low: number; close: number; volume: number }[]; }): Promise { return request('/indicators/calculate', { method: 'POST', body: JSON.stringify(req), }); } // ───────────────────────────────────────────────────────────────────────────── // 전역 지표 파라미터 설정 API // // 역할: 프론트엔드 indicatorRegistry.ts 의 defaultParams 를 완전히 대체. // 사용자가 변경한 파라미터를 DB 에 저장하고, // 백엔드 Ta4j 계산 시에도 동일한 값을 사용하도록 한다. // // 저장 형식: // { // "RSI": { "length": 9, "src": "close" }, // "MACD": { "fastLength": 12, "slowLength": 26, "signalLength": 9 }, // "BollingerBands":{ "length": 20, "mult": 2.0, "src": "close" }, // ... // } // ───────────────────────────────────────────────────────────────────────────── /** 모든 지표 파라미터 로드 (앱 시작 시 호출) */ export async function loadIndicatorSettings(): Promise< Record> > { return ( (await request>>('/indicator-settings')) ?? {} ); } /** 특정 지표 파라미터 로드 */ export async function loadIndicatorSettingsForType( indicatorType: string ): Promise> { return ( (await request>(`/indicator-settings/${encodeURIComponent(indicatorType)}`)) ?? {} ); } /** * 전체 지표 파라미터 저장 (덮어쓰기). * 앱 초기화 또는 설정 내보내기/가져오기 시 사용. */ export async function saveAllIndicatorSettings( allParams: Record> ): Promise { await request('/indicator-settings', { method: 'PUT', body: JSON.stringify(allParams), }); } /** * 특정 지표 파라미터 저장 (병합). * 사용자가 특정 지표의 설정값을 변경할 때마다 호출. * * @param indicatorType 지표 타입 (e.g. "RSI", "MACD") * @param params 변경된 파라미터 (e.g. {length: 9, src: "close"}) */ export async function saveIndicatorSettings( indicatorType: string, params: Record ): Promise { await request(`/indicator-settings/${encodeURIComponent(indicatorType)}`, { method: 'PATCH', body: JSON.stringify(params), }); } /** * 모든 지표의 시각 설정(색상·선굵기·수평선) 로드. * 앱 시작 시 호출 — 전역 시각 기본값을 DB 에서 가져온다. */ export async function loadIndicatorVisualSettings(): Promise< Record > { return ( (await request>( '/indicator-settings/visual' )) ?? {} ); } /** * 특정 지표의 시각 설정(색상·선굵기·수평선)을 저장. * 사용자가 IndicatorSettingsModal 에서 색상·선굵기를 변경할 때 호출. * * @param indicatorType 지표 타입 (e.g. "RSI") * @param visual { plots: PlotDef[], hlines: HLineDef[] } */ export async function saveIndicatorVisualSettings( indicatorType: string, visual: { plots?: unknown[]; hlines?: unknown[] } ): Promise { await request(`/indicator-settings/${encodeURIComponent(indicatorType)}/visual`, { method: 'PATCH', body: JSON.stringify(visual), }); } // ───────────────────────────────────────────────────────────────────────────── // 앱 전역 차트 기본 설정 API // // 프론트엔드 코드에 하드코딩된 기본값들을 DB 로 대체. // // 관리 항목: // defaultSymbol — 기본 종목 (e.g. "KRW-BTC") // defaultTimeframe — 기본 타임프레임 (e.g. "1D") // defaultChartType — 기본 차트 타입 (e.g. "candlestick") // defaultTheme — 기본 테마 (e.g. "dark") // defaultLogScale — 기본 로그 스케일 (false) // defaultLayoutId — 기본 레이아웃 ID (e.g. "1") // mainChartStyle — 캔들 색상 (DEFAULT_MAIN_CHART_STYLE 대체) // syncOptions — 멀티차트 동기화 옵션 (DEFAULT_SYNC 대체) // ───────────────────────────────────────────────────────────────────────────── export interface AppSettingsDto { defaultSymbol?: string; defaultTimeframe?: string; defaultChartType?: string; defaultTheme?: string; defaultLogScale?: boolean; defaultLayoutId?: string; mainChartStyle?: Record | null; syncOptions?: Record | null; /** 백테스팅 완료 시 결과 팝업 자동 표시 여부 (기본 true) */ btAutoPopup?: boolean; /** 백테스팅 매수/매도 마커에 금액 표시 여부 (기본 true) */ btShowPrice?: boolean; /** 보조지표 우측 가격축 라벨·설명·금액 하이라이트 (기본 true) */ chartSeriesPriceLabels?: boolean; /** 차트 하단 거래량 바 표시 (기본 true) */ chartVolumeVisible?: boolean; /** 차트 상단 범례(tv-legend) 항목별 표시 옵션 */ chartLegendOptions?: Record | null; /** 매매 시그널 발생 시 알림 팝업 표시 여부 (기본 true) */ tradeAlertPopup?: boolean; /** 매매 시그널 알림 사운드 ON/OFF */ tradeAlertSoundEnabled?: boolean; /** 매매 시그널 알림음 ID */ tradeAlertSound?: string; /** 알림 팝업 표시 위치: right | left | bottom */ tradeAlertPopupPosition?: string; /** 알림 팝업 배치: stack | grid | strip | single */ tradeAlertPopupLayout?: string; /** 그리드 배치 열 개수 (2~4) */ tradeAlertPopupGridCols?: number; /** 실시간 전략 체크 마스터 ON — ON이면 DB 관심종목 전체가 체크 대상 */ liveStrategyCheck?: boolean; /** 관심종목 공통 전략 ID */ liveStrategyId?: number | null; liveExecutionType?: string; livePositionMode?: string; /** 모의투자 */ paperTradingEnabled?: boolean; paperInitialCapital?: number; paperFeeRatePct?: number; paperSlippagePct?: number; paperMinOrderKrw?: number; paperAutoTradeEnabled?: boolean; paperAutoTradeBudgetPct?: number; /** PAPER | LIVE | BOTH */ tradingMode?: string; liveAutoTradeEnabled?: boolean; upbitAccessKeyMasked?: string | null; hasUpbitKeys?: boolean; upbitAccessKey?: string; upbitSecretKey?: string; /** BACKEND_STOMP | UPBIT_DIRECT */ chartRealtimeSource?: string; liveAutoTradeBudgetPct?: number; fcmPushEnabled?: boolean; /** 차트·UI 표시 시간대 (IANA, 예: Asia/Seoul) */ displayTimezone?: string; } export interface LiveSummaryDto { enabled: boolean; configured: boolean; tradingMode: string; krwBalance: number; positions: { symbol: string; quantity: number; avgPrice: number }[]; } export interface LiveTradeDto { id?: number; symbol: string; side: string; source: string; price: number; quantity: number; grossAmount: number; upbitOrderUuid?: string | null; createdAt?: string | null; } export async function loadLiveSummary(): Promise { return request('/live/summary'); } export async function loadLiveTrades(): Promise { return (await request('/live/trades')) ?? []; } export interface LiveOrderRequest { market: string; side: 'BUY' | 'SELL'; orderKind?: string; krwAmount?: number; quantity?: number; strategyId?: number; } export async function placeLiveOrder(req: LiveOrderRequest): Promise { return request('/live/orders', { method: 'POST', body: JSON.stringify(req), }); } export interface ProcessMonitorDto { id: string; name: string; layer: string; status: 'healthy' | 'degraded' | 'down' | 'disabled'; metrics: Record; } export interface SystemMonitorDto { pipelineEnabled: boolean; sampledAtMs: number; config: Record; jvm: { heapUsedMb: number; heapMaxMb: number; heapPct: number; nonHeapUsedMb: number; threadCount: number; daemonThreadCount: number; }; traffic: Record; memory: { markets: number; series: number; totalBars: number; maxBarsPerSeries: number }; queue: { pending: number; capacity: number; fillRatio: number; enqueued: number; dequeued: number; dropped: number }; workers: { processed: number; errors: number; active: boolean; workerThreads: number }; orders: { pending: number; submitted: number; executed: number; failed: number; dropped: number; workerAlive: boolean; ratePerSecond: number; }; processes: ProcessMonitorDto[]; threads: { name: string; id: number; state: string; daemon: boolean }[]; staleMarkets: { market: string; lastTickAgeSec: number }[]; lastTicks: Record; } export interface DashboardSummaryDto { app: Record; watchlistCount: number; liveCheckMarkets: number; monitoredMarkets: number; paper: PaperSummaryDto; live: LiveSummaryDto; recentSignals: TradeSignalDto[]; systemMonitor: SystemMonitorDto; fcm: { available: boolean; pushEnabled: boolean }; } export async function loadDashboardSummary(): Promise { return request('/dashboard/summary'); } export async function registerFcmToken(token: string): Promise { await request('/fcm/token', { method: 'POST', body: JSON.stringify({ token }), }); } export async function loadFcmStatus(): Promise<{ available: boolean } | null> { return request<{ available: boolean }>('/fcm/status'); } export async function sendFcmTest(): Promise<{ ok: boolean; available: boolean } | null> { return request<{ ok: boolean; available: boolean }>('/fcm/test', { method: 'POST' }); } // ── 모의투자 ─────────────────────────────────────────────────────────────────── export interface PaperPositionDto { id?: number; symbol: string; koreanName?: string | null; quantity: number; avgPrice: number; markPrice?: number | null; evalAmount?: number; profitLoss?: number; profitLossPct?: number; } export interface PaperSummaryDto { enabled: boolean; initialCapital: number; cashBalance: number; stockEvalAmount: number; totalAsset: number; unrealizedPnl: number; realizedPnl: number; totalReturnPct: number; feeRatePct: number; slippagePct: number; minOrderKrw: number; autoTradeEnabled: boolean; autoTradeBudgetPct: number; positions: PaperPositionDto[]; } export interface PaperTradeDto { id: number; symbol: string; side: 'BUY' | 'SELL'; orderKind: string; source: string; strategyId?: number | null; price: number; quantity: number; grossAmount: number; feeAmount: number; netAmount: number; cashAfter: number; createdAt?: string | null; } export interface PaperOrderRequest { market: string; side: 'BUY' | 'SELL'; orderKind?: 'limit' | 'market'; price: number; quantity: number; /** 수량 0일 때 매수 예산 비율 (가용현금 %) */ budgetPct?: number; /** 수량 0일 때 매수 고정 금액 (KRW) */ krwAmount?: number; source?: string; strategyId?: number | null; } export async function loadPaperSummary(markPrices?: Record): Promise { if (markPrices && Object.keys(markPrices).length > 0) { return request('/paper/summary', { method: 'POST', body: JSON.stringify(markPrices), }); } return request('/paper/summary'); } export async function loadPaperTrades(): Promise { return (await request('/paper/trades')) ?? []; } export async function placePaperOrder(body: PaperOrderRequest): Promise { return request('/paper/orders', { method: 'POST', body: JSON.stringify(body), }); } export async function resetPaperAccount(): Promise { return request('/paper/reset', { method: 'POST' }); } /** 매매 시그널 이력 DTO */ export interface TradeSignalDto { id: number; market: string; strategyId: number | null; strategyName: string | null; signalType: 'BUY' | 'SELL'; price: number; candleTime: number; candleType: string; executionType: string; createdAt: string; } /** 매매 시그널 이력 조회 */ export async function loadTradeSignals(market?: string): Promise { const q = market ? `?market=${encodeURIComponent(market)}` : ''; return (await request(`/trade-signals${q}`)) ?? []; } /** * 앱 시작 시 기본 설정 로드. * DB 에 없으면 백엔드 엔티티 기본값(KRW-BTC / 1D / dark 등)을 반환. */ export async function loadAppSettings(): Promise { return (await request('/app-settings')) ?? {}; } /** * 기본 설정 저장. * 변경된 필드만 포함한 객체를 전달 — 없는 키는 기존 값 유지. * * 호출 시점: * - 테마 변경 → { defaultTheme } * - 레이아웃 변경 → { defaultLayoutId } * - 캔들 색상 변경 → { mainChartStyle } * - 동기화 옵션 변경 → { syncOptions } * - 기본 심볼/타임프레임 변경 → { defaultSymbol, defaultTimeframe } */ export async function saveAppSettings(data: AppSettingsDto): Promise { return (await request('/app-settings', { method: 'PUT', body: JSON.stringify(data), })) ?? data; } // ───────────────────────────────────────────────────────────────────────────── // 인증 API // ───────────────────────────────────────────────────────────────────────────── export interface LoginResponse { userId: number; username: string; displayName: string; role?: string; } export interface MenuPermissionsResponse { role: string; permissions: Record; } export interface UserDto { id: number; username: string; displayName: string; role: string; enabled: boolean; createdAt?: string | null; } export async function fetchMenuPermissions(): Promise { const res = await request('/auth/menu-permissions'); return res ?? { role: 'GUEST', permissions: { chart: true, settings: true } }; } export async function verifyAdminPassword(password: string): Promise { await requestOrThrow<{ ok: boolean }>('/admin/verify-password', { method: 'POST', body: JSON.stringify({ password }), }); } export async function listAdminUsers(): Promise { return (await request('/admin/users')) ?? []; } export async function createAdminUser(data: { username: string; password: string; displayName?: string; role: string; enabled?: boolean; }): Promise { return requestOrThrow('/admin/users', { method: 'POST', body: JSON.stringify(data), }); } export async function updateAdminUser( id: number, data: { password?: string; displayName?: string; role?: string; enabled?: boolean }, ): Promise { return requestOrThrow(`/admin/users/${id}`, { method: 'PUT', body: JSON.stringify(data), }); } export async function deleteAdminUser(id: number): Promise { await requestOrThrow(`/admin/users/${id}`, { method: 'DELETE' }); } export async function fetchRolePermissions(): Promise>> { return (await request>>('/admin/role-permissions')) ?? {}; } export async function saveRolePermissions( role: string, permissions: Record, ): Promise { await requestOrThrow('/admin/role-permissions', { method: 'PUT', body: JSON.stringify({ role, permissions }), }); } export async function loginUser(username: string, password: string): Promise { return requestOrThrow('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }), }); } export async function fetchAuthMe(): Promise { return request('/auth/me'); } // ───────────────────────────────────────────────────────────────────────────── // 투자전략 API // ───────────────────────────────────────────────────────────────────────────── export interface StrategyDto { id?: number; name: string; description?: string; buyCondition?: unknown; // LogicNode DSL sellCondition?: unknown; // LogicNode DSL enabled?: boolean; createdAt?: string; updatedAt?: string; } /** 전략 목록 로드 */ export async function loadStrategies(): Promise { return (await request('/strategies')) ?? []; } /** 단일 전략 로드 */ export async function loadStrategy(id: number): Promise { return request(`/strategies/${id}`); } /** 전략 저장 (id 없으면 신규, 있으면 수정) — 실패 시 Error throw */ export async function saveStrategy(dto: StrategyDto): Promise { const res = await fetch(`${API_BASE}/strategies`, { method: 'POST', headers: authHeaders(), body: JSON.stringify(dto), }); if (!res.ok) { const body = await res.text().catch(() => ''); const hint = res.status === 404 ? '서버에 투자전략 API(/api/strategies)가 없습니다. 백엔드를 최신으로 배포했는지 확인하세요.' : res.status === 502 || res.status === 503 ? '백엔드에 연결할 수 없습니다. Docker·nginx 상태를 확인하세요.' : `저장 실패 (HTTP ${res.status})`; console.error(`[backendApi] POST /strategies → ${res.status}`, body); throw new Error(body ? `${hint} — ${body.slice(0, 160)}` : hint); } return (await res.json()) as StrategyDto; } /** 전략 삭제 */ export async function deleteStrategy(id: number): Promise { await request(`/strategies/${id}`, { method: 'DELETE' }); } // ───────────────────────────────────────────────────────────────────────────── // 백테스팅 API // ───────────────────────────────────────────────────────────────────────────── export interface BacktestSignal { time: number; type: 'BUY' | 'SELL' | 'SHORT_ENTRY' | 'SHORT_EXIT' | 'PARTIAL_SELL'; price: number; barIndex: number; } export interface BacktestStats { totalSignals: number; buySignals: number; sellSignals: number; totalTrades: number; winTrades: number; winRate: number; totalReturn: number; maxDrawdown: number; avgReturn: number; finalEquity: number; } /** Ta4j AnalysisCriterion 전체 계산 결과 */ export interface BacktestAnalysis { initialCapital: number; finalEquity: number; totalReturnPct: number; totalProfitLoss: number; grossProfit: number; grossLoss: number; avgReturnPct: number; profitLossRatio: number; numberOfPositions: number; numberOfWinning: number; numberOfLosing: number; numberOfBreakEven: number; winRate: number; maxDrawdownPct: number; maxRunupPct: number; sharpeRatio: number; sortinoRatio: number; calmarRatio: number; valueAtRisk95: number; expectedShortfall: number; buyAndHoldReturnPct: number; vsBuyAndHold: number; } export interface BacktestResponse { signals: BacktestSignal[]; stats: BacktestStats; analysis?: BacktestAnalysis; resultId?: number; } /** DB 저장 이력 레코드 */ export interface BacktestResultRecord { id: number; deviceId?: string; strategyId?: number; strategyName: string; symbol: string; timeframe: string; barCount: number; fromTime: number; toTime: number; settingsJson: string; signalsJson: string; analysisJson: string; totalReturn: number; winRate: number; totalTrades: number; maxDrawdown: number; sharpeRatio: number; finalEquity: number; createdAt: string; } // ───────────────────────────────────────────────────────────────────────────── // 백테스팅 설정 DTO // ───────────────────────────────────────────────────────────────────────────── export interface BacktestSettingsDto { id?: number; initialCapital: number; commissionType: 'LINEAR' | 'ZERO'; commissionRate: number; slippageRate: number; entryPriceType: 'CLOSE' | 'NEXT_OPEN'; exitPriceType: 'CLOSE' | 'NEXT_OPEN'; positionDirection: 'LONG' | 'SHORT' | 'BOTH'; tradeSizeType: 'CAPITAL_PCT' | 'FIXED_AMOUNT'; tradeSizeValue: number; stopLossEnabled: boolean; stopLossPct: number; takeProfitEnabled: boolean; takeProfitPct: number; trailingStopEnabled: boolean; trailingStopPct: number; reentryWaitBars: number; maxOpenTrades: number; partialExitEnabled: boolean; partialExitPct: number; /** "LONG_ONLY" | "SIGNAL_ONLY" — 매도 시그널 포지션 종속성 모드 */ positionMode?: 'LONG_ONLY' | 'SIGNAL_ONLY'; } export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = { initialCapital: 10_000_000, commissionType: 'LINEAR', commissionRate: 0.0015, slippageRate: 0.0005, entryPriceType: 'CLOSE', exitPriceType: 'CLOSE', positionDirection: 'LONG', tradeSizeType: 'CAPITAL_PCT', tradeSizeValue: 100, stopLossEnabled: false, stopLossPct: 2, takeProfitEnabled: false, takeProfitPct: 5, trailingStopEnabled: false, trailingStopPct: 2, reentryWaitBars: 0, maxOpenTrades: 1, partialExitEnabled: false, partialExitPct: 50, positionMode: 'LONG_ONLY', }; /** 백테스팅 설정 로드 */ export async function loadBacktestSettings(): Promise { return (await request('/backtest-settings')) ?? DEFAULT_BACKTEST_SETTINGS; } /** 백테스팅 설정 저장 */ export async function saveBacktestSettings(dto: BacktestSettingsDto): Promise { return request('/backtest-settings', { method: 'POST', body: JSON.stringify(dto), }); } export interface BacktestRequest { strategyId?: number; buyCondition?: unknown; sellCondition?: unknown; bars: { time: number; open: number; high: number; low: number; close: number; volume: number }[]; timeframe: string; indicatorParams?: Record>; settings?: BacktestSettingsDto; symbol?: string; strategyName?: string; deviceId?: string; } /** 백테스팅 실행 */ export async function runBacktest(req: BacktestRequest): Promise { return request('/backtesting/run', { method: 'POST', body: JSON.stringify({ ...req, deviceId: getDeviceId() }), }); } // ───────────────────────────────────────────────────────────────────────────── // 백테스팅 결과 이력 API // ───────────────────────────────────────────────────────────────────────────── /** 결과 이력 목록 로드 */ export async function loadBacktestResults(): Promise { return (await request( `/backtest-results?deviceId=${encodeURIComponent(getDeviceId())}` )) ?? []; } /** 결과 단건 조회 */ export async function loadBacktestResult(id: number): Promise { return request(`/backtest-results/${id}`); } /** 결과 삭제 */ export async function deleteBacktestResult(id: number): Promise { await request(`/backtest-results/${id}`, { method: 'DELETE' }); } // ───────────────────────────────────────────────────────────────────────────── // 실시간 전략 체크 설정 API // ───────────────────────────────────────────────────────────────────────────── export interface LiveStrategySettingsDto { market: string; strategyId: number | null; isLiveCheck: boolean; /** "CANDLE_CLOSE" | "REALTIME_TICK" */ executionType: string; /** "LONG_ONLY" | "SIGNAL_ONLY" — 매도 시그널 포지션 종속성 모드 */ positionMode?: string; /** 전략 평가 분봉: 1m, 3m, 5m, 15m, 30m, 1h, 4h, 1d */ candleType?: string; } /** 실시간 전략 체크 설정 로드 */ export async function loadLiveStrategySettings( market: string, ): Promise { return (await request( `/strategy/settings?market=${encodeURIComponent(market)}`, )) ?? { market, strategyId: null, isLiveCheck: false, executionType: 'CANDLE_CLOSE', positionMode: 'LONG_ONLY', candleType: '1m' }; } /** 실시간 전략 체크 설정 저장 */ export async function saveLiveStrategySettings( dto: LiveStrategySettingsDto, ): Promise { return requestOrThrow('/strategy/settings', { method: 'PUT', body: JSON.stringify(dto), }); } /** 현재 디바이스에서 실시간 체크 ON + 전략 지정된 종목 목록 */ export async function loadActiveLiveStrategySettings(): Promise { return (await request('/strategy/settings/active')) ?? []; } /** 관심종목 등 여러 마켓에 동일 실시간 전략 설정 일괄 적용 */ export async function saveLiveStrategySettingsBulk( markets: string[], template: Omit, ): Promise { const list = (await request('/strategy/settings/bulk', { method: 'PUT', body: JSON.stringify({ markets, template: { ...template, market: markets[0] ?? 'KRW-BTC' } }), })) ?? []; return list; }