# 투자분석결과 산정방식 GoldenChart의 백테스팅·상세분석 레포트·알림 목록 레포트 등에서 표시되는 **수익률·손익·승률**이 어떻게 계산되는지 정리한 문서입니다. --- ## 1. 개요 투자분석 결과는 백엔드 `BacktestingService`가 캔들 구간을 순회하며 전략 시그널을 재현하고, 포트폴리오 손익을 시뮬레이션한 뒤 `BacktestAnalysisDto`를 생성합니다. 프론트엔드는 이 DTO를 `BacktestAnalysis` 타입으로 받아 `BacktestDashboard`(상세분석 레포트 팝업) 등에 표시합니다. ``` [설정] gc_backtest_settings.analysis_method ↓ [API] POST /backtesting/run (settings 포함) ↓ [백엔드] BacktestingService.runBacktest() ├─ 시그널 재현 (Ta4j Rule + TradingRecord) ├─ PortfolioLedger (LONG 표준 회계) 또는 레거시 비율 모델 (SHORT/BOTH) └─ calcAnalysis() → BacktestAnalysisDto ↓ [프론트] BacktestAnalysisReportModal / BacktestDashboard ``` **핵심 원칙** - 수익률·손익 금액은 **사용자가 선택한 투자분석 방식**에 따라 달라집니다. - 승률·거래 횟수는 **청산 완료된 포지션(라운드트립)** 기준이며, 분석 방식과 무관하게 동일한 규칙을 사용합니다. - 리스크 지표(MDD, Sharpe 등)는 Ta4j `AnalysisCriterion` 결과를 사용하며, 총 수익률은 포트폴리오 시뮬레이션 결과와 연동됩니다. --- ## 2. 투자분석 방식 설정 ### 2.1 설정 항목 | DB 컬럼 | DTO 필드 | 기본값 | 설명 | |---------|----------|--------|------| | `gc_backtest_settings.analysis_method` | `analysisMethod` | `MARK_TO_MARKET` | 수익률·손익 산정 방식 | ### 2.2 선택 가능한 방식 | 코드 | UI 표시명 | 설명 | |------|-----------|------| | `MARK_TO_MARKET` | 평가손익 포함 (표준) | HTS·MTS **평가금액** 방식. 예수금 + 보유주식 시가평가 | | `REALIZED_ONLY` | 실현손익만 | **청산 완료**된 거래의 실현손익만 수익률에 반영. 기말 미청산 포지션은 평가 제외 | ### 2.3 설정 저장·로드 - **저장소**: `gc_backtest_settings` (디바이스 ID 기준 upsert) - **API**: `GET/POST /backtest-settings` - **UI** - 설정 화면 → 백테스팅 → **투자분석 방식** (`BacktestSettingsPanel.tsx`) - 차트 툴바 백테스트 설정 모달 (`BacktestSettingsModal.tsx`) ### 2.4 설정이 적용되는 화면 `runBacktest()` 호출 시 `settings`를 함께 전달하는 모든 경로에 동일하게 적용됩니다. | 화면 | 설정 로드 방식 | |------|----------------| | 차트 백테스팅 | `App.tsx` → `loadBacktestSettings()` → `useBacktest()` | | 백테스팅 메뉴 | 실행 시 당시 저장된 설정 (재실행 시 최신 설정) | | 분석레포트 | 좌측 목록 선택 후 레포트 생성 시 | | **알림 목록** 상세분석 레포트 | `loadBacktestSettings()` 후 `runBacktest()` | | 모의투자 분석 차트 | `PaperAnalysisChart` → `loadBacktestSettings()` | > **참고**: DB에 이미 저장된 과거 백테스트 결과(`gc_backtest_result.analysis_json`)는 당시 방식 그대로입니다. 새 방식을 보려면 백테스트를 **다시 실행**해야 합니다. --- ## 3. 분석 대상 기간 백테스트는 요청에 포함된 **캔들 배열 전체**를 대상으로 합니다. | 실행 경로 | 기간 결정 | |-----------|-----------| | 차트 백테스팅 | 현재 차트에 로드된 캔들 구간 | | 알림 목록 레포트 | 알림 `candleTime`을 끝점으로 과거 **300봉** (`loadAnalysisCandles`) | | 백테스팅 메뉴 | 저장된 백테스트 결과의 `fromTime` ~ `toTime` | 기간의 **첫 봉 ~ 마지막 봉**을 순회하며 매수·매도 시그널을 재현합니다. --- ## 4. 포트폴리오 회계 (`PortfolioLedger`) LONG 포지션(기본 설정 `positionDirection = LONG`)에서는 **표준 주식 프로그램 방식**의 예수금·보유수량 회계를 사용합니다. 구현 파일: `backend/src/main/java/com/goldenchart/service/PortfolioLedger.java` ### 4.1 상태 변수 | 변수 | 의미 | |------|------| | `cash` | 예수금 (초기값 = `initialCapital`) | | `shares` | 보유 수량 | | `costBasis` | 현재 보유분 취득원가 (매수 수수료 포함) | | `realizedPnl` | 누적 실현손익 | | `grossProfit` | 실현 이익 거래 합계 (양수만 누적) | | `grossLoss` | 실현 손실 거래 합계 (음수만 누적) | ### 4.2 평가금액 (기간 중 임의 시점) ``` 평가금액 = cash + shares × markPrice ``` `markPrice`는 해당 봉의 종가(또는 진입/청산 가격 설정에 따른 가격)입니다. ### 4.3 매수 체결 (`executeBuy`) 1. **주문 금액** 결정 - `tradeSizeType = CAPITAL_PCT`: `평가금액 × (tradeSizeValue / 100)` - `tradeSizeType = FIXED_AMOUNT`: `min(tradeSizeValue, cash)` 2. **수수료**: `commissionType = LINEAR`일 때 `commissionRate` 적용 (`ZERO`이면 0) 3. **슬리피지**: 매수 시 `effEntry = price × (1 + slippageRate)` (`applySlippage`) 4. **체결** ``` sharesToBuy = orderAmount / effEntry totalCost = sharesToBuy × effEntry × (1 + commissionRate) cash -= totalCost shares += sharesToBuy costBasis += totalCost ``` 예수금이 부족하면 가용 예수금 범위 내에서만 매수합니다. ### 4.4 매도 체결 (`executeSell`) `sellFraction`: 청산 비율 (0~1). 전량 매도 = 1, 분할 청산 = `partialExitPct / 100`. ``` sharesToSell = shares × sellFraction proceeds = sharesToSell × effExit × (1 - commissionRate) costPortion = costBasis × (sharesToSell / shares) pnl = proceeds - costPortion cash += proceeds realizedPnl += pnl grossProfit += pnl (pnl ≥ 0) grossLoss += pnl (pnl < 0) shares -= sharesToSell costBasis -= costPortion ``` `effExit`는 매도 슬리피지 적용: `price × (1 - slippageRate)`. ### 4.5 기말 최종 자산 (`resolveFinalEquity`) 마지막 봉 종가를 `lastMarkPrice`로 사용합니다. #### MARK_TO_MARKET (평가손익 포함) ``` 최종 자산(finalEquity) = cash + shares × lastMarkPrice 평가 손익(unrealizedPnl) = shares × lastMarkPrice - costBasis (보유 시) ``` 기말에 **미청산 포지션**이 있어도 보유주식을 종가로 평가하여 수익률에 반영합니다. #### REALIZED_ONLY (실현손익만) ``` 최종 자산(finalEquity) = initialCapital + realizedPnl ``` 기말 미청산 포지션의 시가평가는 **수익률에 포함하지 않습니다**. --- ## 5. 수익률·손익 지표 산식 모든 비율 값은 **소수**로 저장됩니다 (예: `0.0427` = +4.27%). 프론트엔드 `pct()` 함수가 ×100 하여 `%`로 표시합니다. ### 5.1 총 수익률 ``` totalReturnPct = (finalEquity - initialCapital) / initialCapital ``` - `initialCapital`: 백테스트 설정의 초기 자본 (기본 10,000,000원) - `finalEquity`: §4.5 기말 최종 자산 ### 5.2 총 손익 (금액) ``` totalProfitLoss = finalEquity - initialCapital ``` ### 5.3 총 이익 / 총 손실 - `grossProfit`: 청산 시 **실현 이익**의 합 (PortfolioLedger 누적) - `grossLoss`: 청산 시 **실현 손실**의 합 (음수) 두 값의 합은 `REALIZED_ONLY` 기준 실현손익과 일치합니다. `MARK_TO_MARKET`에서는 기말 평가손익이 추가로 반영되어 `grossProfit + grossLoss`와 `totalProfitLoss`가 다를 수 있습니다. ### 5.4 평균 수익률/거래 ``` avgReturnPct = totalReturnPct / numberOfPositions (거래 1건 이상) ``` ### 5.5 손익비 (Profit Factor) ``` profitLossRatio = grossProfit / |grossLoss| (grossLoss ≠ 0) ``` --- ## 6. 승률·거래 통계 승률은 **투자분석 방식과 무관**하며, Ta4j `TradingRecord`의 **청산 완료 포지션**을 기준으로 합니다. | 지표 | 산식 / 출처 | |------|-------------| | `numberOfPositions` | `NumberOfPositionsCriterion` (완료 거래 수) | | `numberOfWinning` | `NumberOfWinningPositionsCriterion` 또는 `position.getProfit() > 0` | | `numberOfLosing` | `NumberOfLosingPositionsCriterion` 또는 `position.getProfit() < 0` | | `numberOfBreakEven` | `positions - winning - losing` | | `winRate` | `winning / positions` (0~1) | **판정 기준** - 각 포지션 = 매수 진입 후 매도 청산까지의 **1회 라운드트립** - 포지션별 손익 = Ta4j `position.getProfit()` (진입가·청산가·수량 기준) - **기말 미청산** 매수는 승률 집계에 **포함되지 않음** **예시**: 13거래 중 4승 9패 → `winRate = 4/13 ≈ 0.308` → 화면 **30.8%** --- ## 7. 리스크·벤치마크 지표 | 지표 | 계산 방식 | |------|-----------| | `maxDrawdownPct` | Ta4j `MaximumDrawdownCriterion` | | `maxRunupPct` | Ta4j `MaximumRunupCriterion` | | `sharpeRatio` | Ta4j `SharpeRatioCriterion` | | `sortinoRatio` | Ta4j `SortinoRatioCriterion` | | `calmarRatio` | `totalReturnPct / |maxDrawdownPct|` | | `valueAtRisk95` | Ta4j `ValueAtRiskCriterion` | | `expectedShortfall` | Ta4j `ExpectedShortfallCriterion` | | `buyAndHoldReturnPct` | Ta4j `EnterAndHoldReturnCriterion` 또는 첫봉~마지막봉 종가 변화율 | | `vsBuyAndHold` | `(1 + totalReturnPct) / (1 + buyAndHoldReturnPct)` | > 총 수익률(`totalReturnPct`)이 포트폴리오 시뮬레이션 기준으로 산출되므로, 칼마 비율·벤치마크 비교·초과 수익도 선택한 분석 방식의 영향을 받습니다. --- ## 8. SHORT / BOTH 포지션 (레거시 비율 모델) `positionDirection`이 `LONG`이 아닌 경우(`SHORT`, `BOTH`)에는 `PortfolioLedger` 대신 **비율 기반 equity 갱신**을 사용합니다. ``` equity += equity × tradeSizePct × sellFraction × (rawReturn - commission×2) ``` 기말 처리 (`resolveFinalEquity`): | 분석 방식 | 기말 미청산 시 | |-----------|----------------| | `MARK_TO_MARKET` | 마지막 봉 종가 기준 미실현 수익률을 equity에 반영 | | `REALIZED_ONLY` | `initialCapital + 누적 실현손익` (비율 모델 gross 합계) | --- ## 9. 백테스트 실행 흐름 (`BacktestingService`) ``` 1. BarSeries 생성 (요청 캔들) 2. entryRule / exitRule 합성 (DSL + 손절/익절/트레일링) 3. 봉별 루프 ├─ positionMode = SIGNAL_ONLY → 지표만으로 BUY/SELL 시그널 └─ positionMode = LONG_ONLY → Ta4j TradingRecord 포지션 제약 4. 진입 시: enterPosition() → PortfolioLedger.executeBuy() (LONG) 5. 청산 시: applyExit() → PortfolioLedger.executeSell() 6. 기말: resolveFinalEquity(lastMarkPrice) 7. calcAnalysis() → BacktestAnalysisDto 8. (선택) gc_backtest_result 저장 ``` 관련 파일: - `backend/src/main/java/com/goldenchart/service/BacktestingService.java` - `backend/src/main/java/com/goldenchart/service/PortfolioLedger.java` - `backend/src/main/java/com/goldenchart/dto/BacktestAnalysisDto.java` - `backend/src/main/java/com/goldenchart/dto/BacktestSettingsDto.java` --- ## 10. API 응답 구조 (`BacktestAnalysis`) 프론트엔드 `BacktestAnalysis` 인터페이스 (`frontend/src/utils/backendApi.ts`)와 1:1 대응합니다. ### 10.1 포트폴리오·방식 (추가 필드) | 필드 | 타입 | 설명 | |------|------|------| | `analysisMethod` | `'MARK_TO_MARKET' \| 'REALIZED_ONLY'` | 적용된 분석 방식 | | `cashBalance` | number | 기말 예수금 | | `holdingsValue` | number | 기말 보유주식 평가액 (`shares × lastMarkPrice`) | | `realizedPnl` | number | 실현 손익 | | `unrealizedPnl` | number | 평가 손익 (미청산 보유분) | ### 10.2 수익성·거래·리스크 §5~§7 절의 `totalReturnPct`, `totalProfitLoss`, `grossProfit`, `grossLoss`, `winRate`, `maxDrawdownPct`, `sharpeRatio` 등. --- ## 11. 프론트엔드 표시 ### 11.1 상세분석 레포트 (`BacktestDashboard`) 파일: `frontend/src/components/BacktestResultModal.tsx` - 헤더 배지: `analysisMethodLabel()` — 적용된 분석 방식 표시 - 수익성 지표 카드: - 총 수익률, 총 손익 - **실현 손익**, **평가 손익** (있을 때) - **기말 예수금**, **보유 평가액** (있을 때) - 총 이익, 총 손실 - 거래 통계: 승률 원형 차트, 수익/손실 거래 수 표시 유틸: `frontend/src/components/shared/analysisDashboardUi.tsx` ```typescript pct(v) = (v × 100).toFixed(2) + '%' // 부호 포함 pctAbs(v) = (v × 100).toFixed(1) + '%' // 승률 등 wonFmt(v) = 원화 단위 (만/억 축약) ``` ### 11.2 분석 방식 라벨 파일: `frontend/src/utils/analysisMethodLabels.ts` --- ## 12. 계산 예시 **조건**: 초기 자본 1,000만원, 13거래 전량 청산, 최종 평가금액 957만원, 분석 방식 `MARK_TO_MARKET` ``` totalReturnPct = (9,570,000 - 10,000,000) / 10,000,000 = -0.043 → -4.30% totalProfitLoss = -430,000원 winRate = 4 / 13 ≈ 30.8% (승률은 거래 단위, 수익률과 별개) ``` 동일 데이터에서 `REALIZED_ONLY`이고 기말 미청산이 없다면 `finalEquity`가 동일하면 수익률도 동일합니다. 기말 **미청산 보유**가 있으면 두 방식의 `finalEquity`·수익률이 달라집니다. --- ## 13. 제한 사항 및 유의점 1. **단일 종목·단일 포지션**: `maxOpenTrades = 1` 기준 단일 심볼 LONG 회계입니다. 2. **과거 결과 불변**: 이미 DB에 저장된 `analysis_json`은 재계산되지 않습니다. 3. **승률 vs 수익률**: 승률은 거래 건별 손익 부호, 수익률은 포트폴리오 전체 평가금액 변화입니다. 반드시 일치하지 않습니다. 4. **SHORT/BOTH**: LONG과 다른 비율 모델을 사용합니다. 정밀한 공매도 회계가 필요하면 향후 확장 대상입니다. 5. **수수료·슬리피지**: 백테스트 설정(`commissionRate`, `slippageRate`)에 따르며, 실거래 환경과 차이가 있을 수 있습니다. --- ## 14. 관련 파일 목록 | 구분 | 경로 | |------|------| | 포트폴리오 회계 | `backend/.../service/PortfolioLedger.java` | | 백테스트·분석 엔진 | `backend/.../service/BacktestingService.java` | | 분석 결과 DTO | `backend/.../dto/BacktestAnalysisDto.java` | | 설정 DTO | `backend/.../dto/BacktestSettingsDto.java` | | DB 마이그레이션 | `backend/.../db/migration/V63__backtest_analysis_method.sql` | | 프론트 API 타입 | `frontend/src/utils/backendApi.ts` | | 분석 방식 라벨 | `frontend/src/utils/analysisMethodLabels.ts` | | 설정 UI | `frontend/src/components/BacktestSettingsPanel.tsx` | | 레포트 UI | `frontend/src/components/BacktestResultModal.tsx` | | 알림 레포트 | `frontend/src/components/TradeNotificationListPage.tsx` | --- *문서 작성 기준: GoldenChart 백엔드 `PortfolioLedger` / `BacktestingService` 구현 반영*