14 KiB
투자분석결과 산정방식
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)
- 주문 금액 결정
tradeSizeType = CAPITAL_PCT:평가금액 × (tradeSizeValue / 100)tradeSizeType = FIXED_AMOUNT:min(tradeSizeValue, cash)
- 수수료:
commissionType = LINEAR일 때commissionRate적용 (ZERO이면 0) - 슬리피지: 매수 시
effEntry = price × (1 + slippageRate)(applySlippage) - 체결
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 / |
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.javabackend/src/main/java/com/goldenchart/service/PortfolioLedger.javabackend/src/main/java/com/goldenchart/dto/BacktestAnalysisDto.javabackend/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
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. 제한 사항 및 유의점
- 단일 종목·단일 포지션:
maxOpenTrades = 1기준 단일 심볼 LONG 회계입니다. - 과거 결과 불변: 이미 DB에 저장된
analysis_json은 재계산되지 않습니다. - 승률 vs 수익률: 승률은 거래 건별 손익 부호, 수익률은 포트폴리오 전체 평가금액 변화입니다. 반드시 일치하지 않습니다.
- SHORT/BOTH: LONG과 다른 비율 모델을 사용합니다. 정밀한 공매도 회계가 필요하면 향후 확장 대상입니다.
- 수수료·슬리피지: 백테스트 설정(
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 구현 반영