상세투자분석 계산로직 수정

This commit is contained in:
Macbook
2026-06-07 15:12:42 +09:00
parent c4365283cf
commit e1cf2ea46d
14 changed files with 366 additions and 58 deletions
@@ -20,6 +20,7 @@ import {
WinRateCircle,
CompareBar,
} from './shared/analysisDashboardUi';
import { analysisMethodLabel } from '../utils/analysisMethodLabels';
const fmtDate = (ts: number) => {
const d = new Date(ts * 1000);
@@ -104,6 +105,11 @@ export function BacktestDashboard({
<span className="brd-dash-badge">{timeframe}</span>
<span className="brd-dash-badge">{periodStr}</span>
{createdAt && <span className="brd-dash-badge brd-dash-badge--time"> {fmtDateStr(createdAt)}</span>}
{a.analysisMethod && (
<span className="brd-dash-badge brd-dash-badge--method">
{analysisMethodLabel(a.analysisMethod)}
</span>
)}
</div>
</div>
<div className={`brd-dash-header-kpi${reportMode ? ' brd-dash-header-kpi--report' : ''}`}>
@@ -141,6 +147,18 @@ export function BacktestDashboard({
<div className="brd-card-body">
<MetricRow label="총 수익률" value={pct(a.totalReturnPct)} cls={colorCls(a.totalReturnPct)}/>
<MetricRow label="총 손익 (금액)" value={wonFmt(a.totalProfitLoss)} cls={colorCls(a.totalProfitLoss)}/>
{a.realizedPnl != null && (
<MetricRow label="실현 손익" value={wonFmt(a.realizedPnl)} cls={colorCls(a.realizedPnl)} />
)}
{a.unrealizedPnl != null && a.unrealizedPnl !== 0 && (
<MetricRow label="평가 손익" value={wonFmt(a.unrealizedPnl)} cls={colorCls(a.unrealizedPnl)} />
)}
{a.cashBalance != null && a.cashBalance > 0 && (
<MetricRow label="기말 예수금" value={wonFmt(a.cashBalance)} />
)}
{a.holdingsValue != null && a.holdingsValue > 0 && (
<MetricRow label="보유 평가액" value={wonFmt(a.holdingsValue)} />
)}
<MetricRow label="총 이익" value={wonFmt(a.grossProfit)} cls="brd-pos"/>
<MetricRow label="총 손실" value={wonFmt(a.grossLoss)} cls="brd-neg"/>
<MetricRow label="평균 수익률/거래" value={pct(a.avgReturnPct)} cls={colorCls(a.avgReturnPct)}/>
@@ -15,6 +15,11 @@ interface FieldMeta {
}
const FIELD_META: Partial<Record<keyof BacktestSettingsDto, FieldMeta>> = {
analysisMethod: {
label: '투자분석 방식',
description:
'수익률·손익 산정 방식을 선택합니다.\n• 평가손익 포함(표준): 예수금 + 보유주식 시가평가. HTS 평가금액과 동일합니다.\n• 실현손익만: 청산 완료 거래의 실현손익만 반영합니다.',
},
initialCapital: {
label: '초기 자본',
description:
@@ -129,6 +134,18 @@ interface SettingSection {
}
const SECTIONS: SettingSection[] = [
{
title: '📊 투자분석 방식',
fields: [
{
key: 'analysisMethod', type: 'select',
opts: [
{ value: 'MARK_TO_MARKET', label: '평가손익 포함 (표준)' },
{ value: 'REALIZED_ONLY', label: '실현손익만' },
],
},
],
},
{
title: '💰 자본 설정',
fields: [
@@ -8,6 +8,7 @@ import {
loadBacktestSettings,
saveBacktestSettings,
} from '../utils/backendApi';
import { ANALYSIS_METHOD_OPTIONS } from '../utils/analysisMethodLabels';
// ── 백테스팅 전용 UI 컴포넌트 ───────────────────────────────────────────────
@@ -106,6 +107,26 @@ export const BacktestSettingsPanel: React.FC<BacktestSettingsPanelProps> = ({
</BtGrid>
</BtSection>
<BtSection title="투자분석 방식">
<BtGrid>
<BtField
label="수익률 · 손익 산정"
desc="백테스팅, 알림 레포트, 분석레포트 등 모든 투자분석 결과에 동일하게 적용됩니다."
full
>
<select
className="stg-select stg-select--wide"
value={cfg.analysisMethod ?? 'MARK_TO_MARKET'}
onChange={e => field('analysisMethod', e.target.value as 'MARK_TO_MARKET' | 'REALIZED_ONLY')}
>
{ANALYSIS_METHOD_OPTIONS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</BtField>
</BtGrid>
</BtSection>
<BtSection title="자본 · 거래 규모">
<BtGrid>
<BtField
@@ -6,7 +6,7 @@ import type { Theme, TradeOrderFillRequest } from '../types';
import type { TickerData } from '../hooks/useMarketTicker';
import { useTradeNotification, type TradeNotificationItem } from '../contexts/TradeNotificationContext';
import {
DEFAULT_BACKTEST_SETTINGS,
loadBacktestSettings,
loadPaperSummary,
loadStrategy,
runBacktest,
@@ -271,6 +271,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
}
const strategyName = strategy.name || item.strategyName || '전략';
const btSettings = await loadBacktestSettings();
const res = await runBacktest({
strategyId: item.strategyId,
bars: bars.map(b => ({
@@ -284,7 +285,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
timeframe,
symbol: market,
strategyName,
settings: DEFAULT_BACKTEST_SETTINGS,
settings: btSettings,
indicatorParams: Object.fromEntries(
REPORT_INDICATOR_TYPES.map(t => [t, getParams(t) as Record<string, unknown>]),
),
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import BacktestAnalysisChart from '../backtest/BacktestAnalysisChart';
import {
DEFAULT_BACKTEST_SETTINGS,
loadBacktestSettings,
loadStrategy,
runBacktest,
type BacktestSignal,
@@ -82,6 +82,7 @@ const PaperAnalysisChart: React.FC<Props> = ({ market, strategyId, theme = 'dark
setRunning(true);
setError(null);
try {
const btSettings = await loadBacktestSettings();
const res = await runBacktest({
strategyId: sid,
bars: bars.map(b => ({
@@ -95,7 +96,7 @@ const PaperAnalysisChart: React.FC<Props> = ({ market, strategyId, theme = 'dark
timeframe: tf,
symbol: sym,
strategyName: strat.name,
settings: DEFAULT_BACKTEST_SETTINGS,
settings: btSettings,
indicatorParams: Object.fromEntries(
['RSI', 'MACD', 'CCI', 'SMA', 'EMA', 'IchimokuCloud', 'Stochastic', 'ADX', 'MFI', 'NewPsychological'].map(t => [
t,
@@ -0,0 +1,23 @@
export type AnalysisMethod = 'MARK_TO_MARKET' | 'REALIZED_ONLY';
export const ANALYSIS_METHOD_OPTIONS: { value: AnalysisMethod; label: string; desc: string }[] = [
{
value: 'MARK_TO_MARKET',
label: '평가손익 포함 (표준)',
desc: '예수금 + 보유주식 시가평가. HTS·MTS 평가금액과 동일한 방식으로 수익률을 계산합니다.',
},
{
value: 'REALIZED_ONLY',
label: '실현손익만',
desc: '청산 완료된 거래의 실현손익만 반영합니다. 기말 미청산 포지션은 수익률에 포함하지 않습니다.',
},
];
export function normalizeAnalysisMethod(raw?: string | null): AnalysisMethod {
return raw === 'REALIZED_ONLY' ? 'REALIZED_ONLY' : 'MARK_TO_MARKET';
}
export function analysisMethodLabel(method?: string | null): string {
const m = normalizeAnalysisMethod(method);
return ANALYSIS_METHOD_OPTIONS.find(o => o.value === m)?.label ?? '평가손익 포함 (표준)';
}
+8
View File
@@ -1151,6 +1151,11 @@ export interface BacktestStats {
export interface BacktestAnalysis {
initialCapital: number;
finalEquity: number;
analysisMethod?: 'MARK_TO_MARKET' | 'REALIZED_ONLY';
cashBalance?: number;
holdingsValue?: number;
realizedPnl?: number;
unrealizedPnl?: number;
totalReturnPct: number;
totalProfitLoss: number;
grossProfit: number;
@@ -1230,6 +1235,8 @@ export interface BacktestSettingsDto {
partialExitPct: number;
/** "LONG_ONLY" | "SIGNAL_ONLY" — 매도 시그널 포지션 종속성 모드 */
positionMode?: 'LONG_ONLY' | 'SIGNAL_ONLY';
/** MARK_TO_MARKET | REALIZED_ONLY — 투자분석 방식 */
analysisMethod?: 'MARK_TO_MARKET' | 'REALIZED_ONLY';
}
export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = {
@@ -1253,6 +1260,7 @@ export const DEFAULT_BACKTEST_SETTINGS: BacktestSettingsDto = {
partialExitEnabled: false,
partialExitPct: 50,
positionMode: 'LONG_ONLY',
analysisMethod: 'MARK_TO_MARKET',
};
/** 백테스팅 설정 로드 */