From 59c548cb4191a3438da43952f48bbad0f43288d4 Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 7 Jun 2026 15:25:45 +0900 Subject: [PATCH] =?UTF-8?q?=ED=88=AC=EC=9E=90=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 투자분석결과 산정방식.md | 381 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 투자분석결과 산정방식.md diff --git a/투자분석결과 산정방식.md b/투자분석결과 산정방식.md new file mode 100644 index 0000000..8f57d3c --- /dev/null +++ b/투자분석결과 산정방식.md @@ -0,0 +1,381 @@ +# 투자분석결과 산정방식 + +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` 구현 반영*