Files
goldenChart/투자분석결과 산정방식.md
T
2026-06-07 15:25:45 +09:00

382 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 투자분석결과 산정방식
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` 구현 반영*