quantcli 0.1.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.
- quantcli/cli.py +996 -0
- quantcli/core/__init__.py +50 -0
- quantcli/core/backtest.py +534 -0
- quantcli/core/data.py +512 -0
- quantcli/core/factor.py +507 -0
- quantcli/datasources/__init__.py +83 -0
- quantcli/datasources/akshare.py +313 -0
- quantcli/datasources/baostock.py +478 -0
- quantcli/datasources/base.py +220 -0
- quantcli/datasources/cache.py +377 -0
- quantcli/datasources/mixed.py +174 -0
- quantcli/factors/__init__.py +29 -0
- quantcli/factors/base.py +163 -0
- quantcli/factors/compute.py +281 -0
- quantcli/factors/loader.py +293 -0
- quantcli/factors/pipeline.py +463 -0
- quantcli/factors/ranking.py +538 -0
- quantcli/factors/screening.py +138 -0
- quantcli/parser/__init__.py +70 -0
- quantcli/parser/constants.py +24 -0
- quantcli/parser/formula.py +397 -0
- quantcli/utils/__init__.py +163 -0
- quantcli/utils/logger.py +207 -0
- quantcli/utils/path.py +422 -0
- quantcli/utils/time.py +522 -0
- quantcli/utils/validate.py +491 -0
- quantcli-0.1.0.dist-info/METADATA +79 -0
- quantcli-0.1.0.dist-info/RECORD +31 -0
- quantcli-0.1.0.dist-info/WHEEL +5 -0
- quantcli-0.1.0.dist-info/entry_points.txt +2 -0
- quantcli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""核心模块 - 因子计算、回测引擎、数据管理
|
|
2
|
+
|
|
3
|
+
Modules:
|
|
4
|
+
- data: 数据获取、缓存、清洗
|
|
5
|
+
- factor: 因子计算引擎
|
|
6
|
+
- backtest: 回测引擎 (基于 Backtrader)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .data import (
|
|
10
|
+
DataManager,
|
|
11
|
+
DataConfig,
|
|
12
|
+
DataQualityReport,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .factor import (
|
|
16
|
+
FactorEngine,
|
|
17
|
+
Factor,
|
|
18
|
+
FactorEvaluation,
|
|
19
|
+
FactorRegistry,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .backtest import (
|
|
23
|
+
BacktestEngine,
|
|
24
|
+
BacktestConfig,
|
|
25
|
+
BacktestResult,
|
|
26
|
+
Strategy,
|
|
27
|
+
Trade,
|
|
28
|
+
quick_backtest,
|
|
29
|
+
run_from_dm,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Data
|
|
34
|
+
"DataManager",
|
|
35
|
+
"DataConfig",
|
|
36
|
+
"DataQualityReport",
|
|
37
|
+
# Factor
|
|
38
|
+
"FactorEngine",
|
|
39
|
+
"Factor",
|
|
40
|
+
"FactorEvaluation",
|
|
41
|
+
"FactorRegistry",
|
|
42
|
+
# Backtest
|
|
43
|
+
"BacktestEngine",
|
|
44
|
+
"BacktestConfig",
|
|
45
|
+
"BacktestResult",
|
|
46
|
+
"Strategy",
|
|
47
|
+
"Trade",
|
|
48
|
+
"quick_backtest",
|
|
49
|
+
"run_from_dm",
|
|
50
|
+
]
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""回测模块 - 基于 Backtrader
|
|
2
|
+
|
|
3
|
+
功能:
|
|
4
|
+
1. 回测引擎 (BacktestEngine)
|
|
5
|
+
2. 策略基类 (Strategy)
|
|
6
|
+
3. 回测结果 (BacktestResult)
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
>>> from quantcli.core import BacktestEngine, Strategy, BacktestConfig
|
|
10
|
+
>>> from quantcli.core.data import DataManager
|
|
11
|
+
>>> config = BacktestConfig(initial_capital=1000000, fee=0.0003)
|
|
12
|
+
>>> engine = BacktestEngine(dm, config)
|
|
13
|
+
>>> engine.add_data("600519", df)
|
|
14
|
+
>>> result = engine.run(DualMAStrategy)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from typing import Dict, List, Optional, Any, Type, Union
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import pandas as pd
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
import backtrader as bt
|
|
27
|
+
from backtrader import Strategy as BtStrategy
|
|
28
|
+
|
|
29
|
+
from ..utils import get_logger, parse_date, format_date
|
|
30
|
+
from ..datasources import create_datasource
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# 配置类
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class BacktestConfig:
|
|
41
|
+
"""回测配置"""
|
|
42
|
+
initial_capital: float = 1000000.0 # 初始资金
|
|
43
|
+
fee: float = 0.0003 # 手续费率
|
|
44
|
+
slippage: float = 0.0005 # 滑点
|
|
45
|
+
benchmark: str = "000300.SH" # 基准指数
|
|
46
|
+
start_date: Optional[Any] = None # 开始日期
|
|
47
|
+
end_date: Optional[Any] = None # 结束日期
|
|
48
|
+
timezone: str = "Asia/Shanghai" # 时区
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Trade:
|
|
53
|
+
"""交易记录"""
|
|
54
|
+
date: Any
|
|
55
|
+
symbol: str
|
|
56
|
+
side: str # "buy" / "sell"
|
|
57
|
+
price: float
|
|
58
|
+
quantity: int
|
|
59
|
+
fee: float
|
|
60
|
+
pnl: Optional[float] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class BacktestResult:
|
|
65
|
+
"""回测结果
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
total_return: 总收益率
|
|
69
|
+
annual_return: 年化收益率
|
|
70
|
+
max_drawdown: 最大回撤
|
|
71
|
+
sharpe: 夏普比率
|
|
72
|
+
sortino: 索提诺比率
|
|
73
|
+
win_rate: 胜率
|
|
74
|
+
profit_factor: 盈亏比
|
|
75
|
+
total_trades: 总交易次数
|
|
76
|
+
trades: 交易记录 DataFrame
|
|
77
|
+
equity_curve: 资金曲线 DataFrame
|
|
78
|
+
"""
|
|
79
|
+
total_return: float = 0.0
|
|
80
|
+
annual_return: float = 0.0
|
|
81
|
+
max_drawdown: float = 0.0
|
|
82
|
+
sharpe: float = 0.0
|
|
83
|
+
sortino: float = 0.0
|
|
84
|
+
win_rate: float = 0.0
|
|
85
|
+
profit_factor: float = 0.0
|
|
86
|
+
total_trades: int = 0
|
|
87
|
+
trades: pd.DataFrame = None
|
|
88
|
+
equity_curve: pd.DataFrame = None
|
|
89
|
+
benchmark_curve: pd.DataFrame = None
|
|
90
|
+
|
|
91
|
+
def __post_init__(self):
|
|
92
|
+
if self.trades is None:
|
|
93
|
+
self.trades = pd.DataFrame()
|
|
94
|
+
if self.equity_curve is None:
|
|
95
|
+
self.equity_curve = pd.DataFrame()
|
|
96
|
+
|
|
97
|
+
def to_dict(self) -> Dict:
|
|
98
|
+
return {
|
|
99
|
+
"total_return": self.total_return,
|
|
100
|
+
"annual_return": self.annual_return,
|
|
101
|
+
"max_drawdown": self.max_drawdown,
|
|
102
|
+
"sharpe": self.sharpe,
|
|
103
|
+
"sortino": self.sortino,
|
|
104
|
+
"win_rate": self.win_rate,
|
|
105
|
+
"profit_factor": self.profit_factor,
|
|
106
|
+
"total_trades": self.total_trades,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# =============================================================================
|
|
111
|
+
# 数据源适配器
|
|
112
|
+
# =============================================================================
|
|
113
|
+
|
|
114
|
+
class PandasData(bt.feeds.PandasData):
|
|
115
|
+
"""Pandas 数据源适配器 - 将 DataFrame 转换为 Backtrader 格式"""
|
|
116
|
+
|
|
117
|
+
params = (
|
|
118
|
+
("datetime", None),
|
|
119
|
+
("open", "open"),
|
|
120
|
+
("high", "high"),
|
|
121
|
+
("low", "low"),
|
|
122
|
+
("close", "close"),
|
|
123
|
+
("volume", "volume"),
|
|
124
|
+
("openinterest", -1),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MultiData(bt.feeds.PandasData):
|
|
129
|
+
"""多标的数据源"""
|
|
130
|
+
|
|
131
|
+
params = (
|
|
132
|
+
("datetime", None),
|
|
133
|
+
("open", "open"),
|
|
134
|
+
("high", "high"),
|
|
135
|
+
("low", "low"),
|
|
136
|
+
("close", "close"),
|
|
137
|
+
("volume", "volume"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# =============================================================================
|
|
142
|
+
# 策略基类
|
|
143
|
+
# =============================================================================
|
|
144
|
+
|
|
145
|
+
class Strategy(BtStrategy):
|
|
146
|
+
"""策略基类 - 继承 Backtrader Strategy
|
|
147
|
+
|
|
148
|
+
子类示例:
|
|
149
|
+
>>> class DualMA(Strategy):
|
|
150
|
+
>>> name = "双均线策略"
|
|
151
|
+
>>> params = {"fast": 5, "slow": 20}
|
|
152
|
+
>>>
|
|
153
|
+
>>> def __init__(self):
|
|
154
|
+
>>> super().__init__()
|
|
155
|
+
>>> self.ma_fast = bt.indicators.SMA(self.data.close, period=self.params.fast)
|
|
156
|
+
>>> self.ma_slow = bt.indicators.SMA(self.data.close, period=self.params.slow)
|
|
157
|
+
>>>
|
|
158
|
+
>>> def next(self):
|
|
159
|
+
>>> if self.ma_fast[0] > self.ma_slow[0] and self.ma_fast[-1] <= self.ma_slow[-1]:
|
|
160
|
+
>>> self.buy()
|
|
161
|
+
>>> elif self.ma_fast[0] < self.ma_slow[0] and self.ma_fast[-1] >= self.ma_slow[-1]:
|
|
162
|
+
>>> self.sell()
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
name = "BaseStrategy"
|
|
166
|
+
params = {}
|
|
167
|
+
|
|
168
|
+
def __init__(self):
|
|
169
|
+
super().__init__()
|
|
170
|
+
# 订单记录
|
|
171
|
+
self.order = None
|
|
172
|
+
self.trade_log = []
|
|
173
|
+
|
|
174
|
+
# 交易统计
|
|
175
|
+
self.wins = 0
|
|
176
|
+
self.losses = 0
|
|
177
|
+
self.gross_profit = 0.0
|
|
178
|
+
self.gross_loss = 0.0
|
|
179
|
+
|
|
180
|
+
def notify_order(self, order):
|
|
181
|
+
"""订单状态通知"""
|
|
182
|
+
if order.status in [order.Submitted, order.Accepted]:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if order.status in [order.Completed]:
|
|
186
|
+
if order.isbuy():
|
|
187
|
+
self.trade_log.append({
|
|
188
|
+
"date": self.datas[0].datetime.date(0),
|
|
189
|
+
"symbol": self.datas[0]._name,
|
|
190
|
+
"side": "buy",
|
|
191
|
+
"price": order.executed.price,
|
|
192
|
+
"quantity": order.executed.size,
|
|
193
|
+
"fee": order.executed.comm,
|
|
194
|
+
})
|
|
195
|
+
else:
|
|
196
|
+
pnl = (order.executed.price - order.orders[0].created.price) * order.executed.size if len(self) > 1 else 0
|
|
197
|
+
self.trade_log.append({
|
|
198
|
+
"date": self.datas[0].datetime.date(0),
|
|
199
|
+
"symbol": self.datas[0]._name,
|
|
200
|
+
"side": "sell",
|
|
201
|
+
"price": order.executed.price,
|
|
202
|
+
"quantity": order.executed.size,
|
|
203
|
+
"fee": order.executed.comm,
|
|
204
|
+
"pnl": pnl,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
# 更新统计
|
|
208
|
+
if pnl > 0:
|
|
209
|
+
self.wins += 1
|
|
210
|
+
self.gross_profit += pnl
|
|
211
|
+
else:
|
|
212
|
+
self.losses += 1
|
|
213
|
+
self.gross_loss += abs(pnl)
|
|
214
|
+
|
|
215
|
+
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
|
|
216
|
+
logger.warning(f"Order rejected: {order.status}")
|
|
217
|
+
|
|
218
|
+
self.order = None
|
|
219
|
+
|
|
220
|
+
def log(self, txt, dt=None):
|
|
221
|
+
"""日志输出"""
|
|
222
|
+
dt = dt or self.datas[0].datetime.date(0)
|
|
223
|
+
logger.info(f"{dt}: {txt}")
|
|
224
|
+
|
|
225
|
+
def notify_trade(self, trade):
|
|
226
|
+
"""交易完成通知"""
|
|
227
|
+
if trade.isclosed:
|
|
228
|
+
logger.info(f"Trade: {trade.getdataname()} - PnL: {trade.pnl:.2f}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# =============================================================================
|
|
232
|
+
# 回测引擎
|
|
233
|
+
# =============================================================================
|
|
234
|
+
|
|
235
|
+
class BacktestEngine:
|
|
236
|
+
"""回测引擎 - 封装 Cerebro
|
|
237
|
+
|
|
238
|
+
Usage:
|
|
239
|
+
>>> engine = BacktestEngine(dm, config)
|
|
240
|
+
>>> engine.add_data("600519", df) # df 包含 open/high/low/close/volume
|
|
241
|
+
>>> result = engine.run(DualMAStrategy)
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def __init__(
|
|
245
|
+
self,
|
|
246
|
+
config: Optional[BacktestConfig] = None
|
|
247
|
+
):
|
|
248
|
+
"""初始化回测引擎
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
config: 回测配置
|
|
252
|
+
"""
|
|
253
|
+
self.config = config or BacktestConfig()
|
|
254
|
+
self.cerebro = bt.Cerebro()
|
|
255
|
+
|
|
256
|
+
# 设置资金
|
|
257
|
+
self.cerebro.broker.setcash(self.config.initial_capital)
|
|
258
|
+
|
|
259
|
+
# 设置手续费
|
|
260
|
+
self.cerebro.broker.setcommission(commission=self.config.fee)
|
|
261
|
+
|
|
262
|
+
# 设置滑点
|
|
263
|
+
self.cerebro.broker.set_slippage_perc(self.config.slippage)
|
|
264
|
+
|
|
265
|
+
# 分析器
|
|
266
|
+
self.cerebro.addanalyzer(bt.analyzers.DrawDown)
|
|
267
|
+
self.cerebro.addanalyzer(bt.analyzers.SharpeRatio, riskfreerate=0.02, annualize=True)
|
|
268
|
+
self.cerebro.addanalyzer(bt.analyzers.Returns)
|
|
269
|
+
self.cerebro.addanalyzer(bt.analyzers.TimeReturn, _name="time_return")
|
|
270
|
+
|
|
271
|
+
# 交易记录
|
|
272
|
+
self.cerebro.addobserver(bt.observers.Trades)
|
|
273
|
+
|
|
274
|
+
# 数据源
|
|
275
|
+
self._data_feeds: Dict[str, Any] = {}
|
|
276
|
+
|
|
277
|
+
def add_data(
|
|
278
|
+
self,
|
|
279
|
+
symbol: str,
|
|
280
|
+
df: pd.DataFrame,
|
|
281
|
+
fromdate: Optional[Any] = None,
|
|
282
|
+
todate: Optional[Any] = None
|
|
283
|
+
):
|
|
284
|
+
"""添加数据源
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
symbol: 股票代码
|
|
288
|
+
df: DataFrame (必须包含: date, open, high, low, close, volume)
|
|
289
|
+
fromdate: 开始日期
|
|
290
|
+
todate: 结束日期
|
|
291
|
+
"""
|
|
292
|
+
# 确保日期列
|
|
293
|
+
df = df.copy()
|
|
294
|
+
if "date" in df.columns:
|
|
295
|
+
df["datetime"] = pd.to_datetime(df["date"])
|
|
296
|
+
df.set_index("datetime", inplace=True)
|
|
297
|
+
elif not isinstance(df.index, pd.DatetimeIndex):
|
|
298
|
+
df.index = pd.to_datetime(df.index)
|
|
299
|
+
|
|
300
|
+
# 过滤日期
|
|
301
|
+
if fromdate:
|
|
302
|
+
df = df[df.index >= parse_date(fromdate)]
|
|
303
|
+
if todate:
|
|
304
|
+
df = df[df.index <= parse_date(todate)]
|
|
305
|
+
|
|
306
|
+
# 创建数据源
|
|
307
|
+
data = PandasData(dataname=df, datetime=None)
|
|
308
|
+
data._name = symbol
|
|
309
|
+
|
|
310
|
+
self.cerebro.adddata(data)
|
|
311
|
+
self._data_feeds[symbol] = data
|
|
312
|
+
|
|
313
|
+
logger.info(f"Added data: {symbol} ({len(df)} rows)")
|
|
314
|
+
|
|
315
|
+
def add_data_from_datasource(
|
|
316
|
+
self,
|
|
317
|
+
symbol: str,
|
|
318
|
+
dm: Any,
|
|
319
|
+
start_date: Any,
|
|
320
|
+
end_date: Any
|
|
321
|
+
):
|
|
322
|
+
"""从 DataManager 添加数据
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
symbol: 股票代码
|
|
326
|
+
dm: DataManager 实例
|
|
327
|
+
start_date: 开始日期
|
|
328
|
+
end_date: 结束日期
|
|
329
|
+
"""
|
|
330
|
+
df = dm.get_daily(symbol, start_date, end_date)
|
|
331
|
+
if not df.empty:
|
|
332
|
+
self.add_data(symbol, df, start_date, end_date)
|
|
333
|
+
|
|
334
|
+
def run(
|
|
335
|
+
self,
|
|
336
|
+
strategy_cls: Type[Strategy],
|
|
337
|
+
**strategy_params
|
|
338
|
+
) -> BacktestResult:
|
|
339
|
+
"""运行回测
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
strategy_cls: 策略类 (继承 Strategy)
|
|
343
|
+
**strategy_params: 策略参数
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
BacktestResult: 回测结果
|
|
347
|
+
"""
|
|
348
|
+
# 添加策略
|
|
349
|
+
self.cerebro.addstrategy(strategy_cls, **strategy_params)
|
|
350
|
+
|
|
351
|
+
# 设置分析器
|
|
352
|
+
if not self.cerebro.analyzers:
|
|
353
|
+
self.cerebro.addanalyzer(bt.analyzers.DrawDown)
|
|
354
|
+
self.cerebro.addanalyzer(bt.analyzers.SharpeRatio)
|
|
355
|
+
|
|
356
|
+
# 运行回测
|
|
357
|
+
results = self.cerebro.run()
|
|
358
|
+
|
|
359
|
+
# 获取策略结果
|
|
360
|
+
strategy_result = results[0]
|
|
361
|
+
|
|
362
|
+
# 构建结果
|
|
363
|
+
backtest_result = self._build_result(strategy_result)
|
|
364
|
+
|
|
365
|
+
logger.info(f"Backtest finished: Return={backtest_result.total_return:.2%}, "
|
|
366
|
+
f"MaxDD={backtest_result.max_drawdown:.2%}, "
|
|
367
|
+
f"Sharpe={backtest_result.sharpe:.2f}")
|
|
368
|
+
|
|
369
|
+
return backtest_result
|
|
370
|
+
|
|
371
|
+
def _build_result(self, strategy_result: Strategy) -> BacktestResult:
|
|
372
|
+
"""构建回测结果"""
|
|
373
|
+
# 获取分析器数据
|
|
374
|
+
analyzers = strategy_result.analyzers
|
|
375
|
+
|
|
376
|
+
# 最大回撤
|
|
377
|
+
dd = analyzers.drawdown.get_analysis()
|
|
378
|
+
max_drawdown = dd.get("max", {}).get("drawdown", 0) / 100 if dd else 0
|
|
379
|
+
|
|
380
|
+
# 夏普比率
|
|
381
|
+
sharpe = analyzers.sharperatio.get_analysis()
|
|
382
|
+
sharpe_ratio = sharpe.get("sharperatio", 0)
|
|
383
|
+
|
|
384
|
+
# 时间收益
|
|
385
|
+
time_returns = analyzers.time_return.get_analysis()
|
|
386
|
+
returns_series = pd.Series(time_returns)
|
|
387
|
+
|
|
388
|
+
# 计算收益指标
|
|
389
|
+
total_return = (1 + returns_series).prod() - 1 if len(returns_series) > 0 else 0
|
|
390
|
+
|
|
391
|
+
# 年化收益率 (假设252交易日)
|
|
392
|
+
n_years = len(returns_series) / 252
|
|
393
|
+
annual_return = (1 + total_return) ** (1 / n_years) - 1 if n_years > 0 else 0
|
|
394
|
+
|
|
395
|
+
# 索提诺比率
|
|
396
|
+
downside_returns = returns_series[returns_series < 0]
|
|
397
|
+
downside_std = downside_returns.std() if len(downside_returns) > 0 else 0
|
|
398
|
+
sortino = (annual_return - 0.02) / downside_std if downside_std > 0 else 0
|
|
399
|
+
|
|
400
|
+
# 交易统计
|
|
401
|
+
trades_df = pd.DataFrame(strategy_result.trade_log)
|
|
402
|
+
if not trades_df.empty:
|
|
403
|
+
total_trades = len(trades_df)
|
|
404
|
+
wins = (trades_df.get("pnl", 0) > 0).sum() if "pnl" in trades_df.columns else 0
|
|
405
|
+
win_rate = wins / total_trades if total_trades > 0 else 0
|
|
406
|
+
|
|
407
|
+
profit = trades_df[trades_df.get("pnl", 0) > 0]["pnl"].sum() if "pnl" in trades_df.columns else 0
|
|
408
|
+
loss = abs(trades_df[trades_df.get("pnl", 0) < 0]["pnl"].sum()) if "pnl" in trades_df.columns else 1
|
|
409
|
+
profit_factor = profit / loss if loss > 0 else 0
|
|
410
|
+
else:
|
|
411
|
+
total_trades = 0
|
|
412
|
+
win_rate = 0
|
|
413
|
+
profit_factor = 0
|
|
414
|
+
|
|
415
|
+
# 资金曲线
|
|
416
|
+
equity_curve = self._get_equity_curve(strategy_result)
|
|
417
|
+
|
|
418
|
+
return BacktestResult(
|
|
419
|
+
total_return=total_return,
|
|
420
|
+
annual_return=annual_return,
|
|
421
|
+
max_drawdown=max_drawdown,
|
|
422
|
+
sharpe=sharpe_ratio,
|
|
423
|
+
sortino=sortino,
|
|
424
|
+
win_rate=win_rate,
|
|
425
|
+
profit_factor=profit_factor,
|
|
426
|
+
total_trades=total_trades,
|
|
427
|
+
trades=trades_df,
|
|
428
|
+
equity_curve=equity_curve,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
def _get_equity_curve(self, strategy_result: Strategy) -> pd.DataFrame:
|
|
432
|
+
"""获取资金曲线"""
|
|
433
|
+
try:
|
|
434
|
+
analyzer = strategy_result.analyzers.timers
|
|
435
|
+
if hasattr(analyzer, "get_analysis"):
|
|
436
|
+
data = analyzer.get_analysis()
|
|
437
|
+
if data:
|
|
438
|
+
dates = [datetime.fromtimestamp(k / 1000) for k in data.keys()]
|
|
439
|
+
values = list(data.values())
|
|
440
|
+
return pd.DataFrame({
|
|
441
|
+
"date": dates,
|
|
442
|
+
"equity": values
|
|
443
|
+
}).set_index("date")
|
|
444
|
+
except Exception as e:
|
|
445
|
+
logger.debug(f"Could not get equity curve: {e}")
|
|
446
|
+
|
|
447
|
+
# 备选方案: 使用 broker value
|
|
448
|
+
try:
|
|
449
|
+
values = []
|
|
450
|
+
dates = []
|
|
451
|
+
for i, data in enumerate(strategy_result.datas[0]):
|
|
452
|
+
dates.append(strategy_result.datas[0].datetime.date(i))
|
|
453
|
+
values.append(strategy_result.broker.getvalue())
|
|
454
|
+
|
|
455
|
+
return pd.DataFrame({
|
|
456
|
+
"date": dates,
|
|
457
|
+
"equity": values
|
|
458
|
+
}).set_index("date")
|
|
459
|
+
except:
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
return pd.DataFrame()
|
|
463
|
+
|
|
464
|
+
def plot(self, **kwargs):
|
|
465
|
+
"""绘图
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
**kwargs: backtrader plot 参数
|
|
469
|
+
"""
|
|
470
|
+
self.cerebro.plot(**kwargs)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# =============================================================================
|
|
474
|
+
# 便捷函数
|
|
475
|
+
# =============================================================================
|
|
476
|
+
|
|
477
|
+
def quick_backtest(
|
|
478
|
+
symbol: str,
|
|
479
|
+
df: pd.DataFrame,
|
|
480
|
+
strategy_cls: Type[Strategy],
|
|
481
|
+
initial_capital: float = 1000000.0,
|
|
482
|
+
fee: float = 0.0003,
|
|
483
|
+
**strategy_params
|
|
484
|
+
) -> BacktestResult:
|
|
485
|
+
"""快速回测 - 单行代码运行回测
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
symbol: 股票代码
|
|
489
|
+
df: 数据 DataFrame
|
|
490
|
+
strategy_cls: 策略类
|
|
491
|
+
initial_capital: 初始资金
|
|
492
|
+
fee: 手续费率
|
|
493
|
+
**strategy_params: 策略参数
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
BacktestResult
|
|
497
|
+
"""
|
|
498
|
+
config = BacktestConfig(initial_capital=initial_capital, fee=fee)
|
|
499
|
+
engine = BacktestEngine(config)
|
|
500
|
+
engine.add_data(symbol, df)
|
|
501
|
+
return engine.run(strategy_cls, **strategy_params)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def run_from_dm(
|
|
505
|
+
dm: Any,
|
|
506
|
+
symbol: str,
|
|
507
|
+
strategy_cls: Type[Strategy],
|
|
508
|
+
start_date: Any,
|
|
509
|
+
end_date: Any,
|
|
510
|
+
initial_capital: float = 1000000.0,
|
|
511
|
+
fee: float = 0.0003,
|
|
512
|
+
**strategy_params
|
|
513
|
+
) -> BacktestResult:
|
|
514
|
+
"""从 DataManager 运行回测
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
dm: DataManager 实例
|
|
518
|
+
symbol: 股票代码
|
|
519
|
+
strategy_cls: 策略类
|
|
520
|
+
start_date: 开始日期
|
|
521
|
+
end_date: 结束日期
|
|
522
|
+
initial_capital: 初始资金
|
|
523
|
+
fee: 手续费率
|
|
524
|
+
**strategy_params: 策略参数
|
|
525
|
+
"""
|
|
526
|
+
config = BacktestConfig(
|
|
527
|
+
initial_capital=initial_capital,
|
|
528
|
+
fee=fee,
|
|
529
|
+
start_date=start_date,
|
|
530
|
+
end_date=end_date
|
|
531
|
+
)
|
|
532
|
+
engine = BacktestEngine(config)
|
|
533
|
+
engine.add_data_from_datasource(symbol, dm, start_date, end_date)
|
|
534
|
+
return engine.run(strategy_cls, **strategy_params)
|