deltafq 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deltafq/__init__.py +29 -0
- deltafq/backtest/__init__.py +32 -0
- deltafq/backtest/engine.py +145 -0
- deltafq/backtest/metrics.py +74 -0
- deltafq/backtest/performance.py +350 -0
- deltafq/charts/__init__.py +14 -0
- deltafq/charts/performance.py +319 -0
- deltafq/charts/price.py +64 -0
- deltafq/charts/signals.py +181 -0
- deltafq/core/__init__.py +18 -0
- deltafq/core/base.py +21 -0
- deltafq/core/config.py +62 -0
- deltafq/core/exceptions.py +34 -0
- deltafq/core/logger.py +44 -0
- deltafq/data/__init__.py +16 -0
- deltafq/data/cleaner.py +39 -0
- deltafq/data/fetcher.py +58 -0
- deltafq/data/storage.py +264 -0
- deltafq/data/validator.py +51 -0
- deltafq/indicators/__init__.py +14 -0
- deltafq/indicators/fundamental.py +28 -0
- deltafq/indicators/talib_indicators.py +67 -0
- deltafq/indicators/technical.py +251 -0
- deltafq/live/__init__.py +16 -0
- deltafq/live/connection.py +235 -0
- deltafq/live/data_feed.py +158 -0
- deltafq/live/monitoring.py +191 -0
- deltafq/live/risk_control.py +192 -0
- deltafq/strategy/__init__.py +12 -0
- deltafq/strategy/base.py +34 -0
- deltafq/strategy/signals.py +193 -0
- deltafq/trader/__init__.py +16 -0
- deltafq/trader/broker.py +119 -0
- deltafq/trader/engine.py +174 -0
- deltafq/trader/order_manager.py +110 -0
- deltafq/trader/position_manager.py +92 -0
- deltafq-0.4.0.dist-info/METADATA +115 -0
- deltafq-0.4.0.dist-info/RECORD +42 -0
- deltafq-0.4.0.dist-info/WHEEL +5 -0
- deltafq-0.4.0.dist-info/entry_points.txt +2 -0
- deltafq-0.4.0.dist-info/licenses/LICENSE +21 -0
- deltafq-0.4.0.dist-info/top_level.txt +1 -0
deltafq/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeltaFQ - A comprehensive Python quantitative finance library.
|
|
3
|
+
|
|
4
|
+
This library provides tools for strategy development, backtesting,
|
|
5
|
+
paper trading, and live trading.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
|
+
__author__ = "DeltaF"
|
|
10
|
+
|
|
11
|
+
# Import core modules
|
|
12
|
+
from . import core
|
|
13
|
+
from . import data
|
|
14
|
+
from . import strategy
|
|
15
|
+
from . import backtest
|
|
16
|
+
from . import indicators
|
|
17
|
+
from . import trader
|
|
18
|
+
from . import live
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"core",
|
|
22
|
+
"data",
|
|
23
|
+
"strategy",
|
|
24
|
+
"backtest",
|
|
25
|
+
"indicators",
|
|
26
|
+
"trader",
|
|
27
|
+
"live"
|
|
28
|
+
]
|
|
29
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backtesting module for DeltaFQ.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .engine import BacktestEngine
|
|
6
|
+
from .performance import PerformanceReporter
|
|
7
|
+
from .metrics import (
|
|
8
|
+
calculate_annualized_return,
|
|
9
|
+
calculate_calmar_ratio,
|
|
10
|
+
calculate_max_drawdown,
|
|
11
|
+
calculate_returns,
|
|
12
|
+
calculate_sharpe_ratio,
|
|
13
|
+
calculate_total_return,
|
|
14
|
+
calculate_volatility,
|
|
15
|
+
compute_cumulative_returns,
|
|
16
|
+
compute_drawdown_series,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BacktestEngine",
|
|
21
|
+
"PerformanceReporter",
|
|
22
|
+
"calculate_returns",
|
|
23
|
+
"compute_cumulative_returns",
|
|
24
|
+
"compute_drawdown_series",
|
|
25
|
+
"calculate_total_return",
|
|
26
|
+
"calculate_annualized_return",
|
|
27
|
+
"calculate_volatility",
|
|
28
|
+
"calculate_sharpe_ratio",
|
|
29
|
+
"calculate_max_drawdown",
|
|
30
|
+
"calculate_calmar_ratio",
|
|
31
|
+
]
|
|
32
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backtesting engine for DeltaFQ.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from typing import Dict, Any, Optional, Tuple, List
|
|
7
|
+
from ..core.base import BaseComponent
|
|
8
|
+
from ..trader.engine import ExecutionEngine
|
|
9
|
+
from ..data.storage import DataStorage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BacktestEngine(BaseComponent):
|
|
13
|
+
"""Backtesting engine for DeltaFQ."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, initial_capital: float = 1000000, commission: float = 0.001,
|
|
16
|
+
slippage: Optional[float] = None, storage: Optional[DataStorage] = None,
|
|
17
|
+
storage_path: str = None, **kwargs):
|
|
18
|
+
"""Initialize backtest engine."""
|
|
19
|
+
super().__init__(**kwargs)
|
|
20
|
+
self.initial_capital = initial_capital
|
|
21
|
+
self.commission = commission
|
|
22
|
+
self.slippage = slippage
|
|
23
|
+
|
|
24
|
+
# Create execution engine for paper trading (broker=None)
|
|
25
|
+
self.execution = ExecutionEngine(
|
|
26
|
+
broker=None,
|
|
27
|
+
initial_capital=initial_capital,
|
|
28
|
+
commission=commission,
|
|
29
|
+
**kwargs
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Data storage
|
|
33
|
+
self.storage = storage or DataStorage(base_path=storage_path)
|
|
34
|
+
|
|
35
|
+
def initialize(self) -> bool:
|
|
36
|
+
"""Initialize backtest engine."""
|
|
37
|
+
self.logger.info(f"Initializing backtest engine with capital: {self.initial_capital}, "
|
|
38
|
+
f"commission: {self.commission}")
|
|
39
|
+
return self.execution.initialize()
|
|
40
|
+
|
|
41
|
+
def run_backtest(self, symbol: str, signals: pd.Series, price_series: pd.Series,
|
|
42
|
+
save_csv: bool = False, strategy_name: Optional[str] = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
43
|
+
"""Execute a historical replay for a single symbol.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
symbol: Instrument identifier (e.g. ticker).
|
|
47
|
+
signals: Series aligned with `price_series`, containing {-1, 0, 1}.
|
|
48
|
+
price_series: Historical close prices used for fills.
|
|
49
|
+
save_csv: When True, persist trades and equity curve via `DataStorage`.
|
|
50
|
+
strategy_name: Optional strategy label for saved files.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
trades_df: Executed orders recorded by the execution engine.
|
|
54
|
+
values_df: Daily portfolio snapshot with cash, positions, and PnL.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
# Reset execution engine for new backtest
|
|
58
|
+
self.execution = ExecutionEngine(
|
|
59
|
+
broker=None,
|
|
60
|
+
initial_capital=self.initial_capital,
|
|
61
|
+
commission=self.commission
|
|
62
|
+
)
|
|
63
|
+
self.execution.initialize()
|
|
64
|
+
|
|
65
|
+
# Normalize input to DataFrame with required columns
|
|
66
|
+
df_sig = pd.DataFrame({
|
|
67
|
+
'Signal': signals,
|
|
68
|
+
'Close': price_series
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
values_records: List[Dict[str, Any]] = []
|
|
72
|
+
|
|
73
|
+
for i, (date, row) in enumerate(df_sig.iterrows()):
|
|
74
|
+
signal = row['Signal']
|
|
75
|
+
price = row['Close']
|
|
76
|
+
|
|
77
|
+
# Process signals and define order parameters
|
|
78
|
+
if signal == 1: # Buy signal
|
|
79
|
+
# Calculate maximum quantity based on available cash
|
|
80
|
+
# Note: ExecutionEngine will handle cash validation
|
|
81
|
+
max_qty = int(self.execution.cash / (price * (1 + self.commission)))
|
|
82
|
+
if max_qty > 0:
|
|
83
|
+
# Execute order through ExecutionEngine
|
|
84
|
+
self.execution.execute_order(
|
|
85
|
+
symbol=symbol,
|
|
86
|
+
quantity=max_qty,
|
|
87
|
+
order_type="market",
|
|
88
|
+
price=price,
|
|
89
|
+
timestamp=date
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
elif signal == -1: # Sell signal
|
|
93
|
+
# Get current position
|
|
94
|
+
current_qty = self.execution.position_manager.get_position(symbol)
|
|
95
|
+
if current_qty > 0:
|
|
96
|
+
# Execute order through ExecutionEngine
|
|
97
|
+
self.execution.execute_order(
|
|
98
|
+
symbol=symbol,
|
|
99
|
+
quantity=-current_qty, # Negative for sell
|
|
100
|
+
order_type="market",
|
|
101
|
+
price=price,
|
|
102
|
+
timestamp=date
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Calculate daily portfolio metrics from ExecutionEngine
|
|
106
|
+
position_qty = self.execution.position_manager.get_position(symbol)
|
|
107
|
+
position_value = position_qty * price
|
|
108
|
+
total_value = position_value + self.execution.cash
|
|
109
|
+
|
|
110
|
+
daily_pnl = 0.0 if i == 0 else total_value - values_records[-1]['total_value']
|
|
111
|
+
|
|
112
|
+
values_records.append({
|
|
113
|
+
'date': date,
|
|
114
|
+
'signal': signal,
|
|
115
|
+
'price': price,
|
|
116
|
+
'cash': self.execution.cash,
|
|
117
|
+
'position': position_qty,
|
|
118
|
+
'position_value': position_value,
|
|
119
|
+
'total_value': total_value,
|
|
120
|
+
'daily_pnl': daily_pnl,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
# Get trades from ExecutionEngine
|
|
124
|
+
trades_df = pd.DataFrame(self.execution.trades)
|
|
125
|
+
values_df = pd.DataFrame(values_records)
|
|
126
|
+
|
|
127
|
+
if save_csv:
|
|
128
|
+
self._save_backtest_results(symbol, trades_df, values_df, strategy_name)
|
|
129
|
+
|
|
130
|
+
return trades_df, values_df
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
self.logger.error(f"run_backtest error: {e}")
|
|
134
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
135
|
+
|
|
136
|
+
def _save_backtest_results(self, symbol: str, trades_df: pd.DataFrame,
|
|
137
|
+
values_df: pd.DataFrame, strategy_name: Optional[str] = None) -> None:
|
|
138
|
+
"""Save backtest results using DataStorage."""
|
|
139
|
+
paths = self.storage.save_backtest_results(
|
|
140
|
+
trades_df=trades_df,
|
|
141
|
+
values_df=values_df,
|
|
142
|
+
symbol=symbol,
|
|
143
|
+
strategy_name=strategy_name
|
|
144
|
+
)
|
|
145
|
+
self.logger.info(f"Saved backtest results: {paths}")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Pure performance metric helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def calculate_returns(equity: pd.Series) -> pd.Series:
|
|
12
|
+
"""Daily percentage returns for an equity curve."""
|
|
13
|
+
return equity.pct_change().fillna(0.0)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def compute_cumulative_returns(returns: pd.Series) -> pd.Series:
|
|
17
|
+
"""Cumulative returns from daily returns."""
|
|
18
|
+
return (1 + returns).cumprod() - 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compute_drawdown_series(returns: pd.Series) -> pd.Series:
|
|
22
|
+
"""Drawdown series derived from cumulative returns."""
|
|
23
|
+
cumulative = (1 + returns).cumprod()
|
|
24
|
+
peak = cumulative.cummax()
|
|
25
|
+
return (cumulative - peak) / peak
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def calculate_total_return(equity: pd.Series) -> float:
|
|
29
|
+
"""Total return over an equity curve."""
|
|
30
|
+
return float(equity.iloc[-1] / equity.iloc[0] - 1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def calculate_annualized_return(returns: pd.Series, periods: int = 252) -> float:
|
|
34
|
+
"""Annualized return from periodic returns."""
|
|
35
|
+
return float((1 + returns.mean()) ** periods - 1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def calculate_volatility(returns: pd.Series, periods: int = 252) -> float:
|
|
39
|
+
"""Annualized volatility."""
|
|
40
|
+
return float(returns.std() * np.sqrt(periods))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def calculate_sharpe_ratio(returns: pd.Series, risk_free: float = 0.0, periods: int = 252) -> float:
|
|
44
|
+
"""Annualized Sharpe ratio."""
|
|
45
|
+
excess = returns - risk_free / periods
|
|
46
|
+
return float(excess.mean() / excess.std() * np.sqrt(periods)) if excess.std() else 0.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def calculate_max_drawdown(equity: pd.Series) -> float:
|
|
50
|
+
"""Maximum drawdown from an equity curve."""
|
|
51
|
+
peak = equity.cummax()
|
|
52
|
+
drawdown = (equity - peak) / peak
|
|
53
|
+
return float(drawdown.min())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def calculate_calmar_ratio(annualized_return: float, max_drawdown: float) -> float:
|
|
57
|
+
"""Calmar ratio from annualized return and max drawdown."""
|
|
58
|
+
if max_drawdown == 0:
|
|
59
|
+
return float("inf") if annualized_return > 0 else 0.0
|
|
60
|
+
return float(abs(annualized_return / max_drawdown))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"calculate_returns",
|
|
65
|
+
"compute_cumulative_returns",
|
|
66
|
+
"compute_drawdown_series",
|
|
67
|
+
"calculate_total_return",
|
|
68
|
+
"calculate_annualized_return",
|
|
69
|
+
"calculate_volatility",
|
|
70
|
+
"calculate_sharpe_ratio",
|
|
71
|
+
"calculate_max_drawdown",
|
|
72
|
+
"calculate_calmar_ratio",
|
|
73
|
+
]
|
|
74
|
+
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Combined performance computation and reporting utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from ..core.base import BaseComponent
|
|
13
|
+
from .metrics import (
|
|
14
|
+
calculate_annualized_return,
|
|
15
|
+
calculate_calmar_ratio,
|
|
16
|
+
calculate_max_drawdown,
|
|
17
|
+
calculate_returns,
|
|
18
|
+
calculate_sharpe_ratio,
|
|
19
|
+
calculate_total_return,
|
|
20
|
+
calculate_volatility,
|
|
21
|
+
compute_cumulative_returns,
|
|
22
|
+
compute_drawdown_series,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_EMPTY_TRADE_METRICS = {
|
|
26
|
+
"total_trades": 0,
|
|
27
|
+
"total_pnl": 0.0,
|
|
28
|
+
"win_rate": 0.0,
|
|
29
|
+
"winning_trades": 0,
|
|
30
|
+
"losing_trades": 0,
|
|
31
|
+
"avg_win": 0.0,
|
|
32
|
+
"avg_loss": 0.0,
|
|
33
|
+
"profit_loss_ratio": 0.0,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_EMPTY_TRADING_METRICS = {
|
|
37
|
+
"total_commission": 0.0,
|
|
38
|
+
"total_turnover": 0.0,
|
|
39
|
+
"avg_daily_pnl": 0.0,
|
|
40
|
+
"avg_daily_commission": 0.0,
|
|
41
|
+
"avg_daily_turnover": 0.0,
|
|
42
|
+
"avg_daily_trade_count": 0.0,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PerformanceReporter(BaseComponent):
|
|
47
|
+
"""Compute backtest metrics and print summary."""
|
|
48
|
+
|
|
49
|
+
def initialize(self) -> bool:
|
|
50
|
+
self.logger.info("Initializing performance reporter")
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
def print_summary(
|
|
54
|
+
self,
|
|
55
|
+
symbol: str,
|
|
56
|
+
trades_df: pd.DataFrame,
|
|
57
|
+
values_df: pd.DataFrame,
|
|
58
|
+
title: str | None = None,
|
|
59
|
+
language: str = "zh",
|
|
60
|
+
) -> None:
|
|
61
|
+
_, metrics = self.compute(symbol, trades_df, values_df)
|
|
62
|
+
texts = _TEXTS_ZH if language == "zh" else _TEXTS_EN
|
|
63
|
+
_ensure_utf8(language)
|
|
64
|
+
|
|
65
|
+
print("\n" + "=" * 80)
|
|
66
|
+
header = title or texts["title_default"]
|
|
67
|
+
print(f" {header}")
|
|
68
|
+
print("=" * 80 + "\n")
|
|
69
|
+
|
|
70
|
+
print(texts["date_info"])
|
|
71
|
+
print(f" {texts['first_trade_date']}: {metrics.get('first_trade_date')}")
|
|
72
|
+
print(f" {texts['last_trade_date']}: {metrics.get('last_trade_date')}")
|
|
73
|
+
print(f" {texts['total_trading_days']}: {metrics.get('total_trading_days', 0)}")
|
|
74
|
+
print(f" {texts['profitable_days']}: {metrics.get('profitable_days', 0)}")
|
|
75
|
+
print(f" {texts['losing_days']}: {metrics.get('losing_days', 0)}\n")
|
|
76
|
+
|
|
77
|
+
print(texts["capital_info"])
|
|
78
|
+
print(f" {texts['start_capital']}: {metrics.get('start_capital', 0.0):,.2f}")
|
|
79
|
+
end_capital = float(metrics.get("end_capital", 0.0))
|
|
80
|
+
start_capital = float(metrics.get("start_capital", 0.0))
|
|
81
|
+
print(f" {texts['end_capital']}: {end_capital:,.2f}")
|
|
82
|
+
growth = end_capital - start_capital
|
|
83
|
+
total_return = metrics.get("total_return", 0.0)
|
|
84
|
+
print(f" {texts['capital_growth']}: {growth:,.2f} ({total_return:.2%})\n")
|
|
85
|
+
|
|
86
|
+
print(texts["return_metrics"])
|
|
87
|
+
print(f" {texts['total_return']}: {total_return:.2%}")
|
|
88
|
+
print(f" {texts['annualized_return']}: {metrics.get('annualized_return', 0.0):.2%}")
|
|
89
|
+
print(f" {texts['avg_daily_return']}: {metrics.get('avg_daily_return', 0.0):.2%}\n")
|
|
90
|
+
|
|
91
|
+
print(texts["risk_metrics"])
|
|
92
|
+
print(f" {texts['max_drawdown']}: {metrics.get('max_drawdown', 0.0):.2%}")
|
|
93
|
+
print(f" {texts['return_std']}: {metrics.get('return_std', 0.0):.2%}")
|
|
94
|
+
print(f" {texts['volatility']}: {metrics.get('volatility', 0.0):.2%}\n")
|
|
95
|
+
|
|
96
|
+
print(texts["performance_metrics"])
|
|
97
|
+
print(f" {texts['sharpe_ratio']}: {metrics.get('sharpe_ratio', 0.0):.2f}")
|
|
98
|
+
print(f" {texts['return_drawdown_ratio']}: {metrics.get('return_drawdown_ratio', 0.0):.2f}")
|
|
99
|
+
print(f" {texts['win_rate']}: {metrics.get('win_rate', 0.0):.2%}")
|
|
100
|
+
print(f" {texts['profit_loss_ratio']}: {metrics.get('profit_loss_ratio', 0.0):.2f}")
|
|
101
|
+
print(f" {texts['avg_win']}: {metrics.get('avg_win', 0.0):,.2f}")
|
|
102
|
+
print(f" {texts['avg_loss']}: {metrics.get('avg_loss', 0.0):,.2f}\n")
|
|
103
|
+
|
|
104
|
+
print(texts["trading_stats"])
|
|
105
|
+
print(f" {texts['total_pnl']}: {metrics.get('total_pnl', 0.0):,.2f}")
|
|
106
|
+
print(f" {texts['total_commission']}: {metrics.get('total_commission', 0.0):,.2f}")
|
|
107
|
+
print(f" {texts['total_turnover']}: {metrics.get('total_turnover', 0.0):,.2f}")
|
|
108
|
+
print(f" {texts['total_trade_count']}: {metrics.get('total_trade_count', 0)}\n")
|
|
109
|
+
|
|
110
|
+
print(texts["daily_stats"])
|
|
111
|
+
print(f" {texts['avg_daily_pnl']}: {metrics.get('avg_daily_pnl', 0.0):,.2f}")
|
|
112
|
+
print(f" {texts['avg_daily_commission']}: {metrics.get('avg_daily_commission', 0.0):,.2f}")
|
|
113
|
+
print(f" {texts['avg_daily_turnover']}: {metrics.get('avg_daily_turnover', 0.0):,.2f}")
|
|
114
|
+
print(f" {texts['avg_daily_trade_count']}: {metrics.get('avg_daily_trade_count', 0.0):.2f}\n")
|
|
115
|
+
print("=" * 80 + "\n")
|
|
116
|
+
|
|
117
|
+
def compute(
|
|
118
|
+
self,
|
|
119
|
+
symbol: str,
|
|
120
|
+
trades_df: pd.DataFrame,
|
|
121
|
+
values_df: pd.DataFrame,
|
|
122
|
+
) -> tuple[pd.DataFrame, Dict[str, Any]]:
|
|
123
|
+
values = values_df.copy()
|
|
124
|
+
trades = trades_df.copy()
|
|
125
|
+
|
|
126
|
+
if not values.empty and "date" in values.columns:
|
|
127
|
+
values["date"] = pd.to_datetime(values["date"])
|
|
128
|
+
values = values.set_index("date")
|
|
129
|
+
|
|
130
|
+
if "timestamp" in trades.columns:
|
|
131
|
+
trades["timestamp"] = pd.to_datetime(trades["timestamp"])
|
|
132
|
+
|
|
133
|
+
values = values.sort_index()
|
|
134
|
+
index = values.index
|
|
135
|
+
|
|
136
|
+
equity = values.get("total_value", pd.Series(dtype=float, index=index)).astype(float)
|
|
137
|
+
has_equity = len(equity) > 1
|
|
138
|
+
|
|
139
|
+
returns = (
|
|
140
|
+
calculate_returns(equity).reindex(index, fill_value=0.0)
|
|
141
|
+
if has_equity
|
|
142
|
+
else pd.Series(0.0, index=index)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
values["returns"] = returns
|
|
146
|
+
values["cumulative_returns"] = compute_cumulative_returns(returns)
|
|
147
|
+
values["drawdown"] = compute_drawdown_series(returns)
|
|
148
|
+
|
|
149
|
+
start_capital = float(equity.iloc[0]) if not equity.empty else 0.0
|
|
150
|
+
end_capital = float(equity.iloc[-1]) if not equity.empty else start_capital
|
|
151
|
+
|
|
152
|
+
pnl_series = values.get("daily_pnl", pd.Series(0.0, index=index))
|
|
153
|
+
profitable_days = int((pnl_series > 0).sum())
|
|
154
|
+
losing_days = int((pnl_series < 0).sum())
|
|
155
|
+
|
|
156
|
+
total_days = int(len(values))
|
|
157
|
+
|
|
158
|
+
avg_daily_return = float(returns.mean())
|
|
159
|
+
return_std = float(returns.std())
|
|
160
|
+
total_return = calculate_total_return(equity) if has_equity else 0.0
|
|
161
|
+
annualized_return = calculate_annualized_return(returns) if has_equity else 0.0
|
|
162
|
+
volatility = calculate_volatility(returns) if has_equity else 0.0
|
|
163
|
+
sharpe_ratio = calculate_sharpe_ratio(returns) if has_equity else 0.0
|
|
164
|
+
max_drawdown = calculate_max_drawdown(equity) if has_equity else 0.0
|
|
165
|
+
calmar_ratio = calculate_calmar_ratio(annualized_return, max_drawdown) if has_equity else float("inf")
|
|
166
|
+
return_drawdown_ratio = (
|
|
167
|
+
calmar_ratio
|
|
168
|
+
if math.isfinite(calmar_ratio)
|
|
169
|
+
else (abs(annualized_return / max_drawdown) if max_drawdown else float("inf"))
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
trade_metrics = _calculate_trade_metrics(trades)
|
|
173
|
+
trading_metrics = _calculate_trading_metrics(trades, total_days)
|
|
174
|
+
|
|
175
|
+
metrics: Dict[str, Any] = {
|
|
176
|
+
"symbol": symbol,
|
|
177
|
+
"first_trade_date": values.index[0] if not values.empty else None,
|
|
178
|
+
"last_trade_date": values.index[-1] if not values.empty else None,
|
|
179
|
+
"total_trading_days": total_days,
|
|
180
|
+
"profitable_days": profitable_days,
|
|
181
|
+
"losing_days": losing_days,
|
|
182
|
+
"start_capital": start_capital,
|
|
183
|
+
"end_capital": end_capital,
|
|
184
|
+
"total_return": total_return,
|
|
185
|
+
"annualized_return": annualized_return,
|
|
186
|
+
"avg_daily_return": avg_daily_return,
|
|
187
|
+
"max_drawdown": max_drawdown,
|
|
188
|
+
"return_std": return_std,
|
|
189
|
+
"volatility": volatility,
|
|
190
|
+
"sharpe_ratio": sharpe_ratio,
|
|
191
|
+
"return_drawdown_ratio": return_drawdown_ratio,
|
|
192
|
+
"total_pnl": trade_metrics.get("total_pnl", 0.0),
|
|
193
|
+
"avg_win": trade_metrics.get("avg_win", 0.0),
|
|
194
|
+
"avg_loss": trade_metrics.get("avg_loss", 0.0),
|
|
195
|
+
"total_commission": trading_metrics.get("total_commission", 0.0),
|
|
196
|
+
"total_turnover": trading_metrics.get("total_turnover", 0.0),
|
|
197
|
+
"total_trade_count": trade_metrics.get("total_trades", 0),
|
|
198
|
+
"win_rate": trade_metrics.get("win_rate", 0.0),
|
|
199
|
+
"profit_loss_ratio": trade_metrics.get("profit_loss_ratio", 0.0),
|
|
200
|
+
"avg_daily_pnl": trading_metrics.get("avg_daily_pnl", 0.0),
|
|
201
|
+
"avg_daily_commission": trading_metrics.get("avg_daily_commission", 0.0),
|
|
202
|
+
"avg_daily_turnover": trading_metrics.get("avg_daily_turnover", 0.0),
|
|
203
|
+
"avg_daily_trade_count": trading_metrics.get("avg_daily_trade_count", 0.0),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return values, metrics
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _calculate_trade_metrics(trades_df: pd.DataFrame) -> Dict[str, Any]:
|
|
210
|
+
if trades_df.empty:
|
|
211
|
+
return _EMPTY_TRADE_METRICS.copy()
|
|
212
|
+
|
|
213
|
+
pnl_series = trades_df.get("profit_loss")
|
|
214
|
+
if pnl_series is None:
|
|
215
|
+
return _EMPTY_TRADE_METRICS.copy()
|
|
216
|
+
|
|
217
|
+
pnl_series = pnl_series.dropna()
|
|
218
|
+
if pnl_series.empty:
|
|
219
|
+
return _EMPTY_TRADE_METRICS.copy()
|
|
220
|
+
|
|
221
|
+
total_pnl = float(pnl_series.sum())
|
|
222
|
+
winning = pnl_series[pnl_series > 0]
|
|
223
|
+
losing = pnl_series[pnl_series < 0]
|
|
224
|
+
avg_win = float(winning.mean()) if not winning.empty else 0.0
|
|
225
|
+
avg_loss = float(losing.mean()) if not losing.empty else 0.0
|
|
226
|
+
profit_loss_ratio = float(avg_win / abs(avg_loss)) if avg_loss else (float("inf") if avg_win > 0 else 0.0)
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"total_trades": int(len(pnl_series)),
|
|
230
|
+
"total_pnl": total_pnl,
|
|
231
|
+
"win_rate": float((pnl_series > 0).mean()),
|
|
232
|
+
"winning_trades": int((pnl_series > 0).sum()),
|
|
233
|
+
"losing_trades": int((pnl_series < 0).sum()),
|
|
234
|
+
"avg_win": avg_win,
|
|
235
|
+
"avg_loss": avg_loss,
|
|
236
|
+
"profit_loss_ratio": profit_loss_ratio,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _calculate_trading_metrics(trades_df: pd.DataFrame, total_days: int) -> Dict[str, float]:
|
|
241
|
+
if trades_df.empty:
|
|
242
|
+
return _EMPTY_TRADING_METRICS.copy()
|
|
243
|
+
|
|
244
|
+
commission = float(trades_df.get("commission", pd.Series(dtype=float)).sum())
|
|
245
|
+
turnover = float(trades_df.get("gross_revenue", pd.Series(dtype=float)).sum())
|
|
246
|
+
pnl = float(trades_df.get("profit_loss", pd.Series(dtype=float)).sum())
|
|
247
|
+
divisor = total_days or 1
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
"total_commission": commission,
|
|
251
|
+
"total_turnover": turnover,
|
|
252
|
+
"avg_daily_pnl": pnl / divisor,
|
|
253
|
+
"avg_daily_commission": commission / divisor,
|
|
254
|
+
"avg_daily_turnover": turnover / divisor,
|
|
255
|
+
"avg_daily_trade_count": len(trades_df) / divisor,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
_TEXTS_ZH = {
|
|
260
|
+
"title_default": "策略回测报告",
|
|
261
|
+
"date_info": "【日期信息】",
|
|
262
|
+
"first_trade_date": "首个交易日",
|
|
263
|
+
"last_trade_date": "最后交易日",
|
|
264
|
+
"total_trading_days": "总交易日",
|
|
265
|
+
"profitable_days": "盈利交易日",
|
|
266
|
+
"losing_days": "亏损交易日",
|
|
267
|
+
"capital_info": "【资金信息】",
|
|
268
|
+
"start_capital": "起始资金",
|
|
269
|
+
"end_capital": "结束资金",
|
|
270
|
+
"capital_growth": "资金增长",
|
|
271
|
+
"return_metrics": "【收益指标】",
|
|
272
|
+
"total_return": "总收益率",
|
|
273
|
+
"annualized_return": "年化收益",
|
|
274
|
+
"avg_daily_return": "日均收益率",
|
|
275
|
+
"risk_metrics": "【风险指标】",
|
|
276
|
+
"max_drawdown": "最大回撤",
|
|
277
|
+
"return_std": "收益标准差",
|
|
278
|
+
"volatility": "波动率",
|
|
279
|
+
"performance_metrics": "【绩效指标】",
|
|
280
|
+
"sharpe_ratio": "夏普比率",
|
|
281
|
+
"return_drawdown_ratio": "收益回撤比",
|
|
282
|
+
"win_rate": "交易胜率",
|
|
283
|
+
"profit_loss_ratio": "盈亏比",
|
|
284
|
+
"avg_win": "平均盈利",
|
|
285
|
+
"avg_loss": "平均亏损",
|
|
286
|
+
"trading_stats": "【交易统计】",
|
|
287
|
+
"total_pnl": "总盈亏",
|
|
288
|
+
"total_commission": "总手续费",
|
|
289
|
+
"total_turnover": "总成交额",
|
|
290
|
+
"total_trade_count": "总成交笔数",
|
|
291
|
+
"daily_stats": "【日均统计】",
|
|
292
|
+
"avg_daily_pnl": "日均盈亏",
|
|
293
|
+
"avg_daily_commission": "日均手续费",
|
|
294
|
+
"avg_daily_turnover": "日均成交额",
|
|
295
|
+
"avg_daily_trade_count": "日均成交笔数",
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
_TEXTS_EN = {
|
|
300
|
+
"title_default": "Backtest Report",
|
|
301
|
+
"date_info": "[Date Information]",
|
|
302
|
+
"first_trade_date": "First Trading Date",
|
|
303
|
+
"last_trade_date": "Last Trading Date",
|
|
304
|
+
"total_trading_days": "Total Trading Days",
|
|
305
|
+
"profitable_days": "Profitable Days",
|
|
306
|
+
"losing_days": "Losing Days",
|
|
307
|
+
"capital_info": "[Capital Information]",
|
|
308
|
+
"start_capital": "Start Capital",
|
|
309
|
+
"end_capital": "End Capital",
|
|
310
|
+
"capital_growth": "Capital Growth",
|
|
311
|
+
"return_metrics": "[Return Metrics]",
|
|
312
|
+
"total_return": "Total Return",
|
|
313
|
+
"annualized_return": "Annualized Return",
|
|
314
|
+
"avg_daily_return": "Average Daily Return",
|
|
315
|
+
"risk_metrics": "[Risk Metrics]",
|
|
316
|
+
"max_drawdown": "Max Drawdown",
|
|
317
|
+
"return_std": "Return Std Dev",
|
|
318
|
+
"volatility": "Volatility",
|
|
319
|
+
"performance_metrics": "[Performance Metrics]",
|
|
320
|
+
"sharpe_ratio": "Sharpe Ratio",
|
|
321
|
+
"return_drawdown_ratio": "Return/Drawdown Ratio",
|
|
322
|
+
"win_rate": "Win Rate",
|
|
323
|
+
"profit_loss_ratio": "Profit/Loss Ratio",
|
|
324
|
+
"avg_win": "Avg Win",
|
|
325
|
+
"avg_loss": "Avg Loss",
|
|
326
|
+
"trading_stats": "[Trading Statistics]",
|
|
327
|
+
"total_pnl": "Total P&L",
|
|
328
|
+
"total_commission": "Total Commission",
|
|
329
|
+
"total_turnover": "Total Turnover",
|
|
330
|
+
"total_trade_count": "Total Trades",
|
|
331
|
+
"daily_stats": "[Daily Statistics]",
|
|
332
|
+
"avg_daily_pnl": "Avg Daily P&L",
|
|
333
|
+
"avg_daily_commission": "Avg Daily Commission",
|
|
334
|
+
"avg_daily_turnover": "Avg Daily Turnover",
|
|
335
|
+
"avg_daily_trade_count": "Avg Daily Trades",
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
__all__ = ["PerformanceReporter"]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _ensure_utf8(language: str) -> None:
|
|
343
|
+
if language == "zh":
|
|
344
|
+
encoding = getattr(sys.stdout, "encoding", "") or ""
|
|
345
|
+
if encoding.lower() != "utf-8":
|
|
346
|
+
try:
|
|
347
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
348
|
+
except AttributeError:
|
|
349
|
+
pass
|
|
350
|
+
|