Compare commits
15 Commits
9a46bd7e4c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 48c6154fab | |||
| 3174f306bb | |||
| b90d030899 | |||
| 9f06ebf87d | |||
| d4db4f3021 | |||
| 579298d16e | |||
| 735f8858ba | |||
| fafbb6a1a9 | |||
| 0db4155a3c | |||
| 173a566f8b | |||
| e8ed2ddfe5 | |||
| 5afb8ddcd1 | |||
| 64bfd031b3 | |||
| a2a261769b | |||
| 5cc140259e |
7
.idea/dataSources.xml
generated
7
.idea/dataSources.xml
generated
@@ -8,5 +8,12 @@
|
||||
<jdbc-url>jdbc:postgresql://81.71.3.24:6785/leopard_dev</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="leopard.sqlite" uuid="c9e16f8e-81be-45cf-847c-47a6750eeee2">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$USER_HOME$/Documents/leopard_data/leopard.sqlite</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/data_source_mapping.xml
generated
Normal file
7
.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourcePerFileMappings">
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/bd7b5f2a-eb99-4aad-81ec-1fec76b3d7fc/console.sql" value="bd7b5f2a-eb99-4aad-81ec-1fec76b3d7fc" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/c9e16f8e-81be-45cf-847c-47a6750eeee2/console.sql" value="c9e16f8e-81be-45cf-847c-47a6750eeee2" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/db-forest-config.xml
generated
Normal file
6
.idea/db-forest-config.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="db-tree-configuration">
|
||||
<option name="data" value="" />
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/sqldialects.xml
generated
1
.idea/sqldialects.xml
generated
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/sql/initial.sql" dialect="SQLite" />
|
||||
<file url="PROJECT" dialect="PostgreSQL" />
|
||||
</component>
|
||||
</project>
|
||||
953
backtest.ipynb
953
backtest.ipynb
File diff suppressed because one or more lines are too long
326
backtest.py
326
backtest.py
@@ -1,326 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
量化回测主程序
|
||||
|
||||
使用方法:
|
||||
python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategy.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import importlib.util
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from backtesting import Backtest
|
||||
|
||||
# 数据库配置(直接硬编码,开发环境)
|
||||
DB_HOST = "81.71.3.24"
|
||||
DB_PORT = 6785
|
||||
DB_NAME = "leopard_dev"
|
||||
DB_USER = "leopard"
|
||||
DB_PASSWORD = "9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X"
|
||||
|
||||
|
||||
def load_data_from_db(code, start_date, end_date):
|
||||
"""
|
||||
从数据库加载历史数据
|
||||
|
||||
参数:
|
||||
code: 股票代码(如 '000001.SZ')
|
||||
start_date: 开始日期(如 '2024-01-01')
|
||||
end_date: 结束日期(如 '2025-12-31')
|
||||
|
||||
返回:
|
||||
DataFrame, 包含列: [Open, High, Low, Close, Volume, factor]
|
||||
"""
|
||||
import sqlalchemy
|
||||
import urllib.parse
|
||||
|
||||
# 构建连接字符串(URL 编码密码中的特殊字符)
|
||||
encoded_password = urllib.parse.quote_plus(DB_PASSWORD)
|
||||
conn_str = (
|
||||
f"postgresql://{DB_USER}:{encoded_password}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
)
|
||||
engine = sqlalchemy.create_engine(conn_str)
|
||||
|
||||
try:
|
||||
# 构建 SQL 查询
|
||||
query = f"""
|
||||
SELECT
|
||||
trade_date,
|
||||
open * factor AS "Open",
|
||||
close * factor AS "Close",
|
||||
high * factor AS "High",
|
||||
low * factor AS "Low",
|
||||
volume AS "Volume",
|
||||
COALESCE(factor, 1.0) AS factor
|
||||
FROM leopard_daily daily
|
||||
LEFT JOIN leopard_stock stock ON stock.id = daily.stock_id
|
||||
WHERE stock.code = '{code}'
|
||||
AND daily.trade_date BETWEEN '{start_date} 00:00:00'
|
||||
AND '{end_date} 23:59:59'
|
||||
ORDER BY daily.trade_date
|
||||
"""
|
||||
|
||||
# 执行查询
|
||||
df = pd.read_sql(query, engine)
|
||||
|
||||
if len(df) == 0:
|
||||
raise ValueError(f"未找到股票 {code} 在指定时间范围内的数据")
|
||||
|
||||
# 潬换日期并设置为索引
|
||||
df["trade_date"] = pd.to_datetime(df["trade_date"], format="%Y-%m-%d")
|
||||
df.set_index("trade_date", inplace=True)
|
||||
|
||||
return df
|
||||
|
||||
finally:
|
||||
# 清理连接
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def load_strategy(strategy_file):
|
||||
"""
|
||||
动态加载策略文件
|
||||
|
||||
参数:
|
||||
strategy_file: 策略文件路径 (如 'strategy.py' 或 'strategies/macd.py')
|
||||
|
||||
返回:
|
||||
(calculate_indicators, strategy_class) 元组
|
||||
"""
|
||||
# 获取模块名
|
||||
module_name = strategy_file.replace(".py", "").replace("/", ".")
|
||||
spec = importlib.util.spec_from_file_location(module_name, strategy_file)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# 接口验证
|
||||
if not hasattr(module, "calculate_indicators"):
|
||||
raise AttributeError(f"策略文件 {strategy_file} 缺少 calculate_indicators 函数")
|
||||
|
||||
if not hasattr(module, "get_strategy"):
|
||||
raise AttributeError(f"策略文件 {strategy_file} 缺少 get_strategy 函数")
|
||||
|
||||
calculate_indicators = module.calculate_indicators
|
||||
strategy_class = module.get_strategy()
|
||||
|
||||
# 验证 get_strategy 返回的是类
|
||||
if not isinstance(strategy_class, type):
|
||||
raise TypeError("get_strategy() 必须返回一个类")
|
||||
|
||||
# 验证策略类继承自 backtesting.Strategy
|
||||
from backtesting import Strategy
|
||||
|
||||
if not issubclass(strategy_class, Strategy):
|
||||
raise TypeError("策略类必须继承 backtesting.Strategy")
|
||||
|
||||
return calculate_indicators, strategy_class
|
||||
|
||||
|
||||
def apply_color_scheme():
|
||||
"""
|
||||
应用颜色方案:红涨绿跌(中国股市风格)
|
||||
"""
|
||||
import backtesting._plotting as plotting
|
||||
from bokeh.colors.named import tomato, lime
|
||||
|
||||
plotting.BULL_COLOR = tomato
|
||||
plotting.BEAR_COLOR = lime
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""
|
||||
解析命令行参数
|
||||
|
||||
返回:
|
||||
args: 命名空间对象
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="量化回测工具", formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
# 必需参数
|
||||
parser.add_argument(
|
||||
"--code", type=str, required=True, help="股票代码 (如: 000001.SZ)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--start-date", type=str, required=True, help="回测开始日期 (格式: YYYY-MM-DD)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--end-date", type=str, required=True, help="回测结束日期 (格式: YYYY-MM-DD)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strategy-file",
|
||||
type=str,
|
||||
required=True,
|
||||
help="策略文件路径 (如: strategy.py)",
|
||||
)
|
||||
|
||||
# 可选参数
|
||||
parser.add_argument(
|
||||
"--cash", type=float, default=100000, help="初始资金 (默认: 100000)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--commission", type=float, default=0.002, help="手续费率 (默认: 0.002)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", type=str, default=None, help="HTML 输出文件路径 (可选)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--warmup-days", type=int, default=365, help="预热天数 (默认: 365,约一年)"
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def format_value(value, cn_name, key):
|
||||
"""
|
||||
格式化数值显示
|
||||
"""
|
||||
if isinstance(value, (int, float)):
|
||||
if "%" in cn_name or key in [
|
||||
"Sharpe Ratio",
|
||||
"Sortino Ratio",
|
||||
"Calmar Ratio",
|
||||
"Profit Factor",
|
||||
]:
|
||||
formatted_value = f"{value:.2f}"
|
||||
elif "$" in cn_name:
|
||||
formatted_value = f"{value:.2f}"
|
||||
elif "次数" in cn_name:
|
||||
formatted_value = f"{value:.0f}"
|
||||
else:
|
||||
formatted_value = f"{value:.4f}"
|
||||
else:
|
||||
formatted_value = str(value)
|
||||
|
||||
return formatted_value
|
||||
|
||||
|
||||
def print_stats(stats):
|
||||
"""
|
||||
打印回测统计结果
|
||||
|
||||
参数:
|
||||
stats: backtesting 库返回的统计对象
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("回测结果")
|
||||
print("=" * 60)
|
||||
|
||||
indicator_name_mapping = {
|
||||
# 'Start': '回测开始时间',
|
||||
# 'End': '回测结束时间',
|
||||
# 'Duration': '回测持续时长',
|
||||
# 'Exposure Time [%]': '持仓时间占比(%)',
|
||||
'Equity Final [$]': '最终收益',
|
||||
'Equity Peak [$]': '峰值收益',
|
||||
'Return [%]': '总收益率(%)',
|
||||
'Buy & Hold Return [%]': '买入并持有收益率(%)',
|
||||
'Return (Ann.) [%]': '年化收益率(%)',
|
||||
'Volatility (Ann.) [%]': '年化波动率(%)',
|
||||
# 'CAGR [%]': '复合年均增长率(%)',
|
||||
# 'Sharpe Ratio': '夏普比率',
|
||||
'Sortino Ratio': '索提诺比率',
|
||||
'Calmar Ratio': '卡尔玛比率',
|
||||
# 'Alpha [%]': '阿尔法系数(%)',
|
||||
# 'Beta': '贝塔系数',
|
||||
'Max. Drawdown [%]': '最大回撤(%)',
|
||||
'Avg. Drawdown [%]': '平均回撤(%)',
|
||||
'Max. Drawdown Duration': '最大回撤持续时长',
|
||||
'Avg. Drawdown Duration': '平均回撤持续时长',
|
||||
'# Trades': '总交易次数',
|
||||
'Win Rate [%]': '胜率(%)',
|
||||
# 'Best Trade [%]': '最佳单笔交易收益率(%)',
|
||||
# 'Worst Trade [%]': '最差单笔交易收益率(%)',
|
||||
# 'Avg. Trade [%]': '平均单笔交易收益率(%)',
|
||||
# 'Max. Trade Duration': '单笔交易最长持有时长',
|
||||
# 'Avg. Trade Duration': '单笔交易平均持有时长',
|
||||
# 'Profit Factor': '盈利因子',
|
||||
# 'Expectancy [%]': '期望收益(%)',
|
||||
'SQN': '系统质量数',
|
||||
# 'Kelly Criterion': '凯利准则',
|
||||
}
|
||||
for k, v in stats.items():
|
||||
if k in indicator_name_mapping:
|
||||
cn_name = indicator_name_mapping.get(k, k)
|
||||
if isinstance(v, (int, float)):
|
||||
if "%" in cn_name or k in ['Sharpe Ratio', 'Sortino Ratio', 'Calmar Ratio', 'Profit Factor']:
|
||||
formatted_value = f"{v:.2f}"
|
||||
elif "$" in cn_name:
|
||||
formatted_value = f"{v:.2f}"
|
||||
elif "次数" in cn_name:
|
||||
formatted_value = f"{v:.0f}"
|
||||
else:
|
||||
formatted_value = f"{v:.4f}"
|
||||
else:
|
||||
formatted_value = str(v)
|
||||
print(f'{cn_name}: {formatted_value}')
|
||||
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
主函数:编排完整回测流程
|
||||
"""
|
||||
try:
|
||||
# 解析参数
|
||||
args = parse_arguments()
|
||||
|
||||
apply_color_scheme()
|
||||
|
||||
# 加载数据
|
||||
print(f"加载股票数据: {args.code} ({args.start_date} ~ {args.end_date})")
|
||||
data = load_data_from_db(args.code, args.start_date, args.end_date)
|
||||
print(f"数据加载完成,共 {len(data)} 条记录")
|
||||
|
||||
# 截取预热数据
|
||||
warmup_data = data.iloc[-args.warmup_days :]
|
||||
print(f"使用预热数据范围: {warmup_data.index[0]} ~ {warmup_data.index[-1]}")
|
||||
|
||||
# 加载策略
|
||||
print(f"加载策略: {args.strategy_file}")
|
||||
calculate_indicators, strategy_class = load_strategy(args.strategy_file)
|
||||
|
||||
# 计算指标
|
||||
print("计算指标...")
|
||||
warmup_data = calculate_indicators(warmup_data)
|
||||
print("指标计算完成")
|
||||
|
||||
# 执行回测
|
||||
print("开始回测...")
|
||||
from backtesting import Backtest
|
||||
|
||||
bt = Backtest(
|
||||
warmup_data,
|
||||
strategy_class,
|
||||
cash=args.cash,
|
||||
commission=args.commission,
|
||||
finalize_trades=True,
|
||||
)
|
||||
stats = bt.run()
|
||||
|
||||
# 输出结果
|
||||
print_stats(stats)
|
||||
|
||||
# 生成图表
|
||||
if args.output:
|
||||
print(f"\n生成图表: {args.output}")
|
||||
bt.plot(filename=args.output, open_browser=False)
|
||||
print(f"图表已保存到: {args.output}")
|
||||
|
||||
print("\n回测完成!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n错误: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
120
backtest_command.py
Executable file
120
backtest_command.py
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import tabulate
|
||||
|
||||
import backtest_core
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser(description="量化回测工具", formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
|
||||
parser.add_argument("--codes", type=str, nargs="+", required=True, help="股票代码列表 (如: 000001.SZ 600000.SH)", )
|
||||
parser.add_argument("--start-date", type=str, required=True, help="回测开始日期 (格式: YYYY-MM-DD)", )
|
||||
parser.add_argument("--end-date", type=str, required=True, help="回测结束日期 (格式: YYYY-MM-DD)", )
|
||||
parser.add_argument("--strategy-file", type=str, required=True, help="策略文件路径 (如: strategy.py)", )
|
||||
parser.add_argument("--cash", type=float, default=100000, help="初始资金 (默认: 100000)", )
|
||||
parser.add_argument("--commission", type=float, default=0.002, help="手续费率 (默认: 0.002)", )
|
||||
parser.add_argument("--warmup-days", type=int, default=365, help="预热天数 (默认: 365,约一年)", )
|
||||
parser.add_argument("--output-dir", type=str, default=None, help="HTML 图表输出目录 (可选,为每个股票生成 {code}.html)", )
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def format_single_result(result: backtest_core.BacktestResult):
|
||||
print("=" * 60)
|
||||
print(f"股票代码: {result.code}")
|
||||
print("=" * 60)
|
||||
|
||||
indicator_mapping = {
|
||||
"最终收益": f"{result.equity_final:.2f}",
|
||||
"峰值收益": f"{result.equity_peak:.2f}",
|
||||
"总收益率(%)": f"{result.return_pct:.2f}",
|
||||
"买入并持有收益率(%)": f"{result.buy_hold_return_pct:.2f}",
|
||||
"年化收益率(%)": f"{result.return_ann_pct:.2f}",
|
||||
"年化波动率(%)": f"{result.volatility_ann_pct:.2f}",
|
||||
"索提诺比率": f"{result.sortino_ratio:.2f}",
|
||||
"卡尔玛比率": f"{result.calmar_ratio:.2f}",
|
||||
"最大回撤(%)": f"{result.max_drawdown_pct:.2f}",
|
||||
"平均回撤(%)": f"{result.avg_drawdown_pct:.2f}",
|
||||
"最大回撤持续时长": f"{result.max_drawdown_duration:.0f} 天",
|
||||
"平均回撤持续时长": f"{result.avg_drawdown_duration:.0f} 天",
|
||||
"总交易次数": f"{result.num_trades:.0f}",
|
||||
"胜率(%)": f"{result.win_rate_pct:.2f}",
|
||||
"系统质量数": f"{result.sqn:.2f}",
|
||||
}
|
||||
|
||||
for name, value in indicator_mapping.items():
|
||||
print(f"{name}: {value}")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def format_batch_results(results: list[backtest_core.BacktestResult]):
|
||||
table_data = []
|
||||
for result in results:
|
||||
table_data.append(
|
||||
[
|
||||
result.code,
|
||||
f"{result.return_pct:.2f}",
|
||||
f"{result.buy_hold_return_pct:.2f}",
|
||||
f"{result.return_ann_pct:.2f}",
|
||||
f"{result.volatility_ann_pct:.2f}",
|
||||
f"{result.win_rate_pct:.2f}",
|
||||
f"{result.max_drawdown_pct:.2f}",
|
||||
f"{result.sortino_ratio:.2f}",
|
||||
f"{result.num_trades:.0f}",
|
||||
f"{result.sqn:.2f}",
|
||||
]
|
||||
)
|
||||
|
||||
headers = [
|
||||
"股票代码",
|
||||
"收益率%",
|
||||
"买入持有%",
|
||||
"年化收益%",
|
||||
"年化波动%",
|
||||
"胜率%",
|
||||
"最大回撤%",
|
||||
"索提诺比率",
|
||||
"交易次数",
|
||||
"SQN",
|
||||
]
|
||||
print(tabulate.tabulate(table_data, headers=headers, tablefmt="grid"))
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_arguments()
|
||||
|
||||
try:
|
||||
results = backtest_core.run_batch_backtest(
|
||||
codes=args.codes,
|
||||
start_date=args.start_date,
|
||||
end_date=args.end_date,
|
||||
strategy_file=args.strategy_file,
|
||||
cash=args.cash,
|
||||
commission=args.commission,
|
||||
warmup_days=args.warmup_days,
|
||||
output_dir=args.output_dir,
|
||||
show_progress=True,
|
||||
)
|
||||
|
||||
if len(results) == 1:
|
||||
format_single_result(results[0])
|
||||
else:
|
||||
format_batch_results(results)
|
||||
|
||||
if args.output_dir:
|
||||
print(f"\n图表已保存到: {args.output_dir}/")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n错误: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
208
backtest_core.py
Normal file
208
backtest_core.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import dataclasses
|
||||
import importlib.util
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
|
||||
import config
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class BacktestResult:
|
||||
code: str
|
||||
equity_final: float
|
||||
equity_peak: float
|
||||
return_pct: float
|
||||
buy_hold_return_pct: float
|
||||
return_ann_pct: float
|
||||
volatility_ann_pct: float
|
||||
sortino_ratio: float
|
||||
calmar_ratio: float
|
||||
max_drawdown_pct: float
|
||||
avg_drawdown_pct: float
|
||||
max_drawdown_duration: float
|
||||
avg_drawdown_duration: float
|
||||
num_trades: int
|
||||
win_rate_pct: float
|
||||
sqn: float
|
||||
|
||||
|
||||
def load_data_from_db(code: str, start_date: str, end_date: str) -> pd.DataFrame:
|
||||
import sqlalchemy
|
||||
import urllib.parse
|
||||
|
||||
encoded_password = urllib.parse.quote_plus(config.DB_PASSWORD)
|
||||
conn_str = f"postgresql://{config.DB_USER}:{encoded_password}@{config.DB_HOST}:{config.DB_PORT}/{config.DB_NAME}"
|
||||
engine = sqlalchemy.create_engine(conn_str)
|
||||
|
||||
try:
|
||||
query = f"""
|
||||
SELECT trade_date,
|
||||
open * factor AS "Open",
|
||||
close * factor AS "Close",
|
||||
high * factor AS "High",
|
||||
low * factor AS "Low",
|
||||
volume AS "Volume",
|
||||
COALESCE(factor, 1.0) AS factor
|
||||
FROM leopard_daily daily
|
||||
LEFT JOIN leopard_stock stock ON stock.id = daily.stock_id
|
||||
WHERE stock.code = '{code}'
|
||||
AND daily.trade_date BETWEEN '{start_date} 00:00:00'
|
||||
AND '{end_date} 23:59:59'
|
||||
ORDER BY daily.trade_date
|
||||
"""
|
||||
|
||||
df = pd.read_sql(query, engine)
|
||||
|
||||
if len(df) == 0:
|
||||
raise ValueError(f"未找到股票 {code} 在指定时间范围内的数据")
|
||||
|
||||
df["trade_date"] = pd.to_datetime(df["trade_date"], format="%Y-%m-%d")
|
||||
df.set_index("trade_date", inplace=True)
|
||||
|
||||
return df
|
||||
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def load_strategy(strategy_file: str):
|
||||
module_name = strategy_file.replace(".py", "").replace("/", ".")
|
||||
spec = importlib.util.spec_from_file_location(module_name, strategy_file)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
if not hasattr(module, "calculate_indicators"):
|
||||
raise AttributeError(f"策略文件 {strategy_file} 缺少 calculate_indicators 函数")
|
||||
|
||||
if not hasattr(module, "get_strategy"):
|
||||
raise AttributeError(f"策略文件 {strategy_file} 缺少 get_strategy 函数")
|
||||
|
||||
calculate_indicators = module.calculate_indicators
|
||||
strategy_class = module.get_strategy()
|
||||
|
||||
if not isinstance(strategy_class, type):
|
||||
raise TypeError("get_strategy() 必须返回一个类")
|
||||
|
||||
from backtesting import Strategy
|
||||
|
||||
if not issubclass(strategy_class, Strategy):
|
||||
raise TypeError("策略类必须继承 backtesting.Strategy")
|
||||
|
||||
return calculate_indicators, strategy_class
|
||||
|
||||
|
||||
def apply_color_scheme():
|
||||
import backtesting._plotting as plotting
|
||||
|
||||
plotting.BULL_COLOR = config.BULL_COLOR
|
||||
plotting.BEAR_COLOR = config.BEAR_COLOR
|
||||
|
||||
|
||||
def run_backtest(
|
||||
code: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
strategy_file: str,
|
||||
cash: float = config.DEFAULT_CASH,
|
||||
commission: float = config.DEFAULT_COMMISSION,
|
||||
warmup_days: int = config.DEFAULT_WARMUP_DAYS,
|
||||
output_dir: Optional[str] = None,
|
||||
) -> BacktestResult:
|
||||
warmup_start_date = (pd.to_datetime(start_date) - pd.Timedelta(days=warmup_days)).strftime("%Y-%m-%d")
|
||||
|
||||
data = load_data_from_db(code, warmup_start_date, end_date)
|
||||
|
||||
calculate_indicators, strategy_class = load_strategy(strategy_file)
|
||||
|
||||
data = calculate_indicators(data)
|
||||
|
||||
data = data.loc[start_date:end_date]
|
||||
|
||||
from backtesting import Backtest
|
||||
|
||||
bt = Backtest(data, strategy_class, cash=cash, commission=commission, finalize_trades=True)
|
||||
stats = bt.run()
|
||||
|
||||
apply_color_scheme()
|
||||
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
output_path = os.path.join(output_dir, f"{code}.html")
|
||||
bt.plot(filename=output_path, open_browser=False)
|
||||
|
||||
def _safe_float(value, default=0):
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
def _safe_int(value, default=0):
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
def _safe_timedelta(value, default=0):
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return float(value.total_seconds() / 86400)
|
||||
except (TypeError, AttributeError):
|
||||
return default
|
||||
|
||||
return BacktestResult(
|
||||
code=code,
|
||||
equity_final=_safe_float(stats.get("Equity Final [$]"), 0),
|
||||
equity_peak=_safe_float(stats.get("Equity Peak [$]"), 0),
|
||||
return_pct=_safe_float(stats.get("Return [%]"), 0),
|
||||
buy_hold_return_pct=_safe_float(stats.get("Buy & Hold Return [%]"), 0),
|
||||
return_ann_pct=_safe_float(stats.get("Return (Ann.) [%]"), 0),
|
||||
volatility_ann_pct=_safe_float(stats.get("Volatility (Ann.) [%]"), 0),
|
||||
sortino_ratio=_safe_float(stats.get("Sortino Ratio"), 0),
|
||||
calmar_ratio=_safe_float(stats.get("Calmar Ratio"), 0),
|
||||
max_drawdown_pct=_safe_float(stats.get("Max. Drawdown [%]"), 0),
|
||||
avg_drawdown_pct=_safe_float(stats.get("Avg. Drawdown [%]"), 0),
|
||||
max_drawdown_duration=_safe_timedelta(stats.get("Max. Drawdown Duration"), 0),
|
||||
avg_drawdown_duration=_safe_timedelta(stats.get("Avg. Drawdown Duration"), 0),
|
||||
num_trades=_safe_int(stats.get("# Trades"), 0),
|
||||
win_rate_pct=_safe_float(stats.get("Win Rate [%]"), 0),
|
||||
sqn=_safe_float(stats.get("SQN"), 0),
|
||||
)
|
||||
|
||||
|
||||
def run_batch_backtest(
|
||||
codes: list[str],
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
strategy_file: str,
|
||||
cash: float = config.DEFAULT_CASH,
|
||||
commission: float = config.DEFAULT_COMMISSION,
|
||||
warmup_days: int = config.DEFAULT_WARMUP_DAYS,
|
||||
output_dir: Optional[str] = None,
|
||||
show_progress: bool = True,
|
||||
) -> list[BacktestResult]:
|
||||
results = []
|
||||
|
||||
codes_iter = tqdm(codes, desc="批量回测") if show_progress else codes
|
||||
|
||||
for code in codes_iter:
|
||||
result = run_backtest(
|
||||
code=code,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
strategy_file=strategy_file,
|
||||
cash=cash,
|
||||
commission=commission,
|
||||
warmup_days=warmup_days,
|
||||
output_dir=output_dir,
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
20
config.py
Normal file
20
config.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
配置文件
|
||||
|
||||
集中管理数据库配置、默认回测参数、图表配色
|
||||
"""
|
||||
|
||||
DB_HOST = "81.71.3.24"
|
||||
DB_PORT = 6785
|
||||
DB_NAME = "leopard_dev"
|
||||
DB_USER = "leopard"
|
||||
DB_PASSWORD = "9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X"
|
||||
|
||||
DEFAULT_CASH = 100000
|
||||
DEFAULT_COMMISSION = 0.002
|
||||
DEFAULT_WARMUP_DAYS = 365
|
||||
|
||||
from bokeh.colors.named import tomato, lime
|
||||
|
||||
BULL_COLOR = tomato
|
||||
BEAR_COLOR = lime
|
||||
136
data.py
Normal file
136
data.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
from time import sleep
|
||||
|
||||
from sqlalchemy import Column, Double, Integer, String, create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, Session
|
||||
from tushare import pro_api
|
||||
|
||||
TUSHARE_API_KEY = '64ebff4fa679167600b905ee45dd88e76f3963c0ff39157f3f085f0e'
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Stock(Base):
|
||||
__tablename__ = 'stock'
|
||||
|
||||
code = Column(String, primary_key=True, comment="代码")
|
||||
name = Column(String, comment="名称")
|
||||
fullname = Column(String, comment="全名")
|
||||
market = Column(String, comment="市场")
|
||||
exchange = Column(String, comment="交易所")
|
||||
industry = Column(String, comment="行业")
|
||||
list_date = Column(String, comment="上市日期")
|
||||
|
||||
|
||||
class Daily(Base):
|
||||
__tablename__ = 'daily'
|
||||
|
||||
code = Column(String, primary_key=True)
|
||||
trade_date = Column(String, primary_key=True)
|
||||
open = Column(Double)
|
||||
close = Column(Double)
|
||||
high = Column(Double)
|
||||
low = Column(Double)
|
||||
previous_close = Column(Double)
|
||||
turnover = Column(Double)
|
||||
volume = Column(Integer)
|
||||
price_change_amount = Column(Double)
|
||||
factor = Column(Double)
|
||||
|
||||
|
||||
def main():
|
||||
print("开始更新数据")
|
||||
|
||||
engine = create_engine(f"sqlite:////Users/lanyuanxiaoyao/Documents/leopard_data/leopard.sqlite")
|
||||
try:
|
||||
Stock.metadata.create_all(engine, checkfirst=True)
|
||||
Daily.metadata.create_all(engine, checkfirst=True)
|
||||
pro = pro_api(TUSHARE_API_KEY)
|
||||
# with engine.connect() as connection:
|
||||
# stocks = pro.stock_basic(list_status="L", market="主板", fields="ts_code,name,fullname,market,exchange,industry,list_date")
|
||||
# for row in stocks.itertuples():
|
||||
# stmt = insert(Stock).values(
|
||||
# code=row.ts_code,
|
||||
# name=row.name,
|
||||
# fullname=row.fullname,
|
||||
# market=row.market,
|
||||
# exchange=row.exchange,
|
||||
# industry=row.industry,
|
||||
# list_date=row.list_date,
|
||||
# )
|
||||
# stmt = stmt.on_conflict_do_update(
|
||||
# index_elements=["code"],
|
||||
# set_={
|
||||
# "name": stmt.excluded.name,
|
||||
# "fullname": stmt.excluded.fullname,
|
||||
# "market": stmt.excluded.market,
|
||||
# "exchange": stmt.excluded.exchange,
|
||||
# "industry": stmt.excluded.industry,
|
||||
# "list_date": stmt.excluded.list_date,
|
||||
# },
|
||||
# )
|
||||
# print(stmt)
|
||||
# connection.execute(stmt)
|
||||
# connection.commit()
|
||||
#
|
||||
# print("清理行情数据")
|
||||
# connection.execute(text("delete from daily where code not in (select distinct code from stock)"))
|
||||
# connection.commit()
|
||||
#
|
||||
# print("清理财务数据")
|
||||
# connection.execute(text("delete from finance_indicator where code not in (select distinct code from stock)"))
|
||||
# connection.commit()
|
||||
|
||||
with Session(engine) as session:
|
||||
stock_codes = [row[0] for row in session.query(Stock.code).all()]
|
||||
|
||||
latest_date = session.query(Daily.trade_date).order_by(Daily.trade_date.desc()).first()
|
||||
if latest_date is None:
|
||||
latest_date = '1990-12-19'
|
||||
else:
|
||||
latest_date = latest_date.trade_date
|
||||
latest_date = datetime.strptime(latest_date, '%Y-%m-%d').date()
|
||||
current_date = date.today() - timedelta(days=1)
|
||||
delta = (current_date - latest_date).days
|
||||
print(f"最新数据日期:{latest_date},当前日期:{current_date},待更新天数:{delta}")
|
||||
if delta > 0:
|
||||
update_dates = []
|
||||
for i in range(delta):
|
||||
latest_date = latest_date + timedelta(days=1)
|
||||
update_dates.append(latest_date.strftime('%Y%m%d'))
|
||||
for target_date in update_dates:
|
||||
print(f"正在采集:{target_date}")
|
||||
dailies = pro.daily(trade_date=target_date)
|
||||
dailies.set_index("ts_code", inplace=True)
|
||||
factors = pro.adj_factor(trade_date=target_date)
|
||||
factors.set_index("ts_code", inplace=True)
|
||||
results = dailies.join(factors, lsuffix="_daily", rsuffix="_factor", how="left")
|
||||
rows = []
|
||||
for row in results.itertuples():
|
||||
if row.Index in stock_codes:
|
||||
rows.append(
|
||||
Daily(
|
||||
code=row.Index,
|
||||
trade_date=datetime.strptime(target_date, '%Y%m%d').strftime("%Y-%m-%d"),
|
||||
open=row.open,
|
||||
close=row.close,
|
||||
high=row.high,
|
||||
low=row.low,
|
||||
previous_close=row.pre_close,
|
||||
turnover=row.amount,
|
||||
volume=row.vol,
|
||||
price_change_amount=row.pct_chg,
|
||||
factor=row.adj_factor,
|
||||
)
|
||||
)
|
||||
session.add_all(rows)
|
||||
session.commit()
|
||||
sleep(1)
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
295
note_refactor.md
Normal file
295
note_refactor.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# 回测代码重构说明
|
||||
|
||||
## 概述
|
||||
|
||||
本次重构将原有的单一文件 `backtest.py` 拆分为模块化架构,提升代码复用性和可维护性。
|
||||
|
||||
## 文件结构变化
|
||||
|
||||
### 新增文件
|
||||
|
||||
1. **config.py** - 配置管理模块
|
||||
- 数据库配置(DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD)
|
||||
- 默认回测参数(DEFAULT_CASH, DEFAULT_COMMISSION, DEFAULT_WARMUP_DAYS)
|
||||
- 图表配色(BULL_COLOR, BEAR_COLOR)
|
||||
|
||||
2. **backtest_core.py** - 核心回测引擎
|
||||
- `BacktestResult` 数据类:结构化回测结果
|
||||
- `load_data_from_db()`:从数据库加载历史数据
|
||||
- `load_strategy()`:动态加载策略文件
|
||||
- `apply_color_scheme()`:应用图表配色
|
||||
- `run_backtest()`:单股票回测函数
|
||||
- `run_batch_backtest()`:批量回测函数(串行执行)
|
||||
|
||||
3. **backtest_command.py** - 命令行界面
|
||||
- `parse_arguments()`:解析命令行参数
|
||||
- `format_single_result()`:详细格式输出(单股票)
|
||||
- `format_batch_results()`:表格格式输出(多股票,使用 tabulate)
|
||||
- `main()`:主流程编排
|
||||
|
||||
### 删除文件
|
||||
|
||||
1. **backtest.py** - 原有单一文件(284 行)
|
||||
|
||||
## 接口变化
|
||||
|
||||
### 新增 API
|
||||
|
||||
```python
|
||||
# 单股票回测
|
||||
result = backtest_core.run_backtest(
|
||||
code='000001.SZ',
|
||||
start_date='2024-01-01',
|
||||
end_date='2024-12-31',
|
||||
strategy_file='strategies/sma_strategy.py',
|
||||
cash=100000,
|
||||
commission=0.002,
|
||||
warmup_days=365,
|
||||
output_dir=None # 可选,为 None 时不生成图表
|
||||
)
|
||||
|
||||
# 批量回测
|
||||
results = backtest_core.run_batch_backtest(
|
||||
codes=['000001.SZ', '600000.SH'],
|
||||
start_date='2024-01-01',
|
||||
end_date='2024-12-31',
|
||||
strategy_file='strategies/sma_strategy.py',
|
||||
cash=100000,
|
||||
commission=0.002,
|
||||
warmup_days=365,
|
||||
output_dir='output/', # 可选,为每个股票生成 {code}.html
|
||||
show_progress=True # 可选,是否显示 tqdm 进度条
|
||||
)
|
||||
```
|
||||
|
||||
### 新增数据结构
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass
|
||||
class BacktestResult:
|
||||
code: str
|
||||
equity_final: float
|
||||
equity_peak: float
|
||||
return_pct: float
|
||||
buy_hold_return_pct: float
|
||||
return_ann_pct: float
|
||||
volatility_ann_pct: float
|
||||
sortino_ratio: float
|
||||
calmar_ratio: float
|
||||
max_drawdown_pct: float
|
||||
avg_drawdown_pct: float
|
||||
max_drawdown_duration: float
|
||||
avg_drawdown_duration: float
|
||||
num_trades: int
|
||||
win_rate_pct: float
|
||||
sqn: float
|
||||
```
|
||||
|
||||
## 命令行使用方式变化
|
||||
|
||||
### 旧方式(已删除)
|
||||
|
||||
```bash
|
||||
python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2024-12-31 --strategy-file strategy.py
|
||||
```
|
||||
|
||||
### 新方式
|
||||
|
||||
```bash
|
||||
uv run python backtest_command.py --codes 000001.SZ --start-date 2024-01-01 --end-date 2024-12-31 --strategy-file strategies/sma_strategy.py
|
||||
```
|
||||
|
||||
### 参数变化
|
||||
|
||||
| 参数名 | 变化 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `--code` | 改为 `--codes` | 从单一参数改为多值参数(`nargs='+'`) |
|
||||
| `--output` | 改为 `--output-dir` | 指定目录而非文件路径 |
|
||||
|
||||
### 新增参数
|
||||
|
||||
- `--output-dir`:指定图表输出目录(可选)
|
||||
- 单股票时:生成 `{code}.html` 在指定目录
|
||||
- 多股票时:为每个股票生成 `{code}.html` 在指定目录
|
||||
- 不指定时不生成图表
|
||||
|
||||
## 输出格式变化
|
||||
|
||||
### 单股票输出
|
||||
|
||||
保持原有的详细格式输出,每个指标单独一行:
|
||||
|
||||
```
|
||||
============================================================
|
||||
股票代码: 000001.SZ
|
||||
============================================================
|
||||
最终收益: 100981.58
|
||||
峰值收益: 103731.54
|
||||
总收益率(%): 0.98
|
||||
...
|
||||
============================================================
|
||||
```
|
||||
|
||||
### 多股票输出
|
||||
|
||||
新增表格格式输出(使用 tabulate,grid 格式):
|
||||
|
||||
```
|
||||
+------------+-----------+---------+-------------+------------+-------+
|
||||
| 股票代码 | 收益率% | 胜率% | 最大回撤% | 交易次数 | SQN |
|
||||
+============+===========+=========+=============+============+=======+
|
||||
| 000001.SZ | 0.98 | 100 | -2.65 | 1 | nan |
|
||||
| 600000.SH | 0.04 | 100 | -1.5 | 1 | nan |
|
||||
+------------+-----------+---------+-------------+------------+-------+
|
||||
```
|
||||
|
||||
### 进度条
|
||||
|
||||
多股票回测时显示 tqdm 进度条:
|
||||
|
||||
```
|
||||
批量回测: 50%|█████ | 1/2 [00:07<00:07, 7.82s/it]
|
||||
```
|
||||
|
||||
## 依赖变化
|
||||
|
||||
### 新增依赖
|
||||
|
||||
- `tabulate`:表格格式化
|
||||
- 版本:0.9.0
|
||||
- 用途:批量回测结果的表格化输出
|
||||
|
||||
- `tqdm`:进度条显示
|
||||
- 版本:4.67.1
|
||||
- 用途:批量回测时的实时进度反馈
|
||||
|
||||
## 特性增强
|
||||
|
||||
### 新增功能
|
||||
|
||||
1. **批量回测**:支持传入多个股票代码进行串行回测
|
||||
- 命令:`--codes 000001.SZ 600000.SH`
|
||||
- 输出:表格化结果对比
|
||||
- 进度条:实时显示回测进度
|
||||
|
||||
2. **图表生成**:为每个股票生成独立 HTML 图表
|
||||
- 参数:`--output-dir output/`
|
||||
- 输出:`{code}.html` 在指定目录
|
||||
- 自动创建目录:`os.makedirs(output_dir, exist_ok=True)`
|
||||
|
||||
3. **进度条显示**:使用 tqdm 提供实时反馈
|
||||
- 多股票时自动显示
|
||||
- 可通过 `show_progress=False` 禁用
|
||||
|
||||
## 兼容性说明
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
1. **命令行入口变化**
|
||||
- 旧:`python backtest.py`
|
||||
- 新:`uv run python backtest_command.py`
|
||||
|
||||
2. **参数名称变化**
|
||||
- `--code` → `--codes`(从单值改为多值)
|
||||
|
||||
### 兼容性保证
|
||||
|
||||
- 所有原有功能完整保留
|
||||
- 核心回测逻辑无变化
|
||||
- 策略加载方式不变
|
||||
- 数据访问接口不变
|
||||
|
||||
## 代码行数对比
|
||||
|
||||
| 文件 | 旧行数 | 新行数 | 变化 |
|
||||
|------|---------|---------|------|
|
||||
| backtest.py | 284 | - | -284 |
|
||||
| config.py | - | 20 | +20 |
|
||||
| backtest_core.py | - | ~200 | +200 |
|
||||
| backtest_command.py | - | ~120 | +120 |
|
||||
| **总计** | **284** | **~340** | **+56** |
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 对于开发者
|
||||
|
||||
如果需要在其他模块中调用回测功能:
|
||||
|
||||
```python
|
||||
from backtest_core import run_backtest, run_batch_backtest, BacktestResult
|
||||
|
||||
# 单股票回测
|
||||
result = run_backtest(
|
||||
code='000001.SZ',
|
||||
start_date='2024-01-01',
|
||||
end_date='2024-12-31',
|
||||
strategy_file='strategies/sma_strategy.py'
|
||||
)
|
||||
|
||||
# 批量回测
|
||||
results = run_batch_backtest(
|
||||
codes=['000001.SZ', '600000.SH'],
|
||||
start_date='2024-01-01',
|
||||
end_date='2024-12-31',
|
||||
strategy_file='strategies/sma_strategy.py'
|
||||
)
|
||||
|
||||
# 访问结果
|
||||
print(result.return_pct)
|
||||
print(result.win_rate_pct)
|
||||
```
|
||||
|
||||
### 对于终端用户
|
||||
|
||||
**单股票回测示例:**
|
||||
|
||||
```bash
|
||||
uv run python backtest_command.py \
|
||||
--codes 000001.SZ \
|
||||
--start-date 2024-01-01 \
|
||||
--end-date 2024-12-31 \
|
||||
--strategy-file strategies/sma_strategy.py
|
||||
```
|
||||
|
||||
**多股票回测示例:**
|
||||
|
||||
```bash
|
||||
uv run python backtest_command.py \
|
||||
--codes 000001.SZ 600000.SH \
|
||||
--start-date 2024-01-01 \
|
||||
--end-date 2024-12-31 \
|
||||
--strategy-file strategies/sma_strategy.py
|
||||
```
|
||||
|
||||
**生成图表示例:**
|
||||
|
||||
```bash
|
||||
uv run python backtest_command.py \
|
||||
--codes 000001.SZ \
|
||||
--start-date 2024-01-01 \
|
||||
--end-date 2024-12-31 \
|
||||
--strategy-file strategies/sma_strategy.py \
|
||||
--output-dir output/
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
- **立即失败策略**:遇到第一个错误立即停止,不继续执行其他股票
|
||||
- **友好错误提示**:捕获异常并打印清晰的错误信息
|
||||
- **退出状态码**:成功返回 0,失败返回非零
|
||||
- **回溯信息**:打印完整的堆栈跟踪以便调试
|
||||
|
||||
## 性能考虑
|
||||
|
||||
- **串行执行**:当前采用串行执行,确保简单可靠
|
||||
- **未来扩展**:未来可改为并行执行(ThreadPoolExecutor)以提升性能
|
||||
- **数据加载**:每次回测创建独立的数据库连接,避免连接池复杂度
|
||||
|
||||
## 总结
|
||||
|
||||
本次重构实现了:
|
||||
- ✅ 代码模块化:核心逻辑与 CLI 界面分离
|
||||
- ✅ 可复用性:提供标准化 API 供其他模块调用
|
||||
- ✅ 功能增强:支持批量回测和图表生成
|
||||
- ✅ 用户体验:表格化结果和进度条显示
|
||||
- ✅ 代码质量:更清晰的模块划分和类型提示
|
||||
1088
notebook/backtest.ipynb
Normal file
1088
notebook/backtest.ipynb
Normal file
File diff suppressed because one or more lines are too long
1300
notebook/datasource/data.ipynb
Normal file
1300
notebook/datasource/data.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
455
notebook/datasource/data_old.ipynb
Normal file
455
notebook/datasource/data_old.ipynb
Normal file
@@ -0,0 +1,455 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-01-30T05:41:51.291397Z",
|
||||
"start_time": "2026-01-30T04:34:22.917761Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import urllib.parse\n",
|
||||
"\n",
|
||||
"import pandas as pd\n",
|
||||
"import sqlalchemy\n",
|
||||
"from sqlalchemy import text\n",
|
||||
"from sqlalchemy.orm import DeclarativeBase, Session\n",
|
||||
"\n",
|
||||
"postgresql_engin = sqlalchemy.create_engine(\n",
|
||||
" f\"postgresql://leopard:{urllib.parse.quote_plus(\"9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X\")}@81.71.3.24:6785/leopard\"\n",
|
||||
")\n",
|
||||
"sqlite_engine = sqlalchemy.create_engine(f\"sqlite:////Users/lanyuanxiaoyao/Documents/leopard_data/leopard.sqlite\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Base(DeclarativeBase):\n",
|
||||
" pass\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Daily(Base):\n",
|
||||
" __tablename__ = 'daily'\n",
|
||||
"\n",
|
||||
" code = sqlalchemy.Column(sqlalchemy.String, primary_key=True)\n",
|
||||
" trade_date = sqlalchemy.Column(sqlalchemy.Date, primary_key=True)\n",
|
||||
" open = sqlalchemy.Column(sqlalchemy.Double)\n",
|
||||
" close = sqlalchemy.Column(sqlalchemy.Double)\n",
|
||||
" high = sqlalchemy.Column(sqlalchemy.Double)\n",
|
||||
" low = sqlalchemy.Column(sqlalchemy.Double)\n",
|
||||
" previous_close = sqlalchemy.Column(sqlalchemy.Double)\n",
|
||||
" turnover = sqlalchemy.Column(sqlalchemy.Double)\n",
|
||||
" volume = sqlalchemy.Column(sqlalchemy.Integer)\n",
|
||||
" price_change_amount = sqlalchemy.Column(sqlalchemy.Double)\n",
|
||||
" factor = sqlalchemy.Column(sqlalchemy.Double)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" with Session(postgresql_engin) as pg_session:\n",
|
||||
" results = pg_session.execute(text(\"select distinct trade_date from leopard_daily\")).fetchall()\n",
|
||||
" results = list(map(lambda x: x[0].strftime(\"%Y-%m-%d\"), results))\n",
|
||||
" dates = [results[i: i + 30] for i in range(0, len(results), 30)]\n",
|
||||
"\n",
|
||||
" for index, date in enumerate(dates):\n",
|
||||
" print(date)\n",
|
||||
" daily_df = pd.read_sql(\n",
|
||||
" f\"\"\"\n",
|
||||
" select code,\n",
|
||||
" trade_date,\n",
|
||||
" open,\n",
|
||||
" close,\n",
|
||||
" high,\n",
|
||||
" low,\n",
|
||||
" previous_close,\n",
|
||||
" turnover,\n",
|
||||
" volume,\n",
|
||||
" price_change_amount,\n",
|
||||
" factor\n",
|
||||
" from leopard_daily d\n",
|
||||
" left join leopard_stock s on d.stock_id = s.id\n",
|
||||
" where d.trade_date in ('{\"','\".join(date)}')\n",
|
||||
" \"\"\",\n",
|
||||
" postgresql_engin\n",
|
||||
" )\n",
|
||||
" with Session(sqlite_engine) as session:\n",
|
||||
" rows = []\n",
|
||||
" for _, row in daily_df.iterrows():\n",
|
||||
" rows.append(\n",
|
||||
" Daily(\n",
|
||||
" code=row[\"code\"],\n",
|
||||
" trade_date=row[\"trade_date\"],\n",
|
||||
" open=row[\"open\"],\n",
|
||||
" close=row[\"close\"],\n",
|
||||
" high=row[\"high\"],\n",
|
||||
" low=row[\"low\"],\n",
|
||||
" previous_close=row[\"previous_close\"],\n",
|
||||
" turnover=row[\"turnover\"],\n",
|
||||
" volume=row[\"volume\"],\n",
|
||||
" price_change_amount=row[\"price_change_amount\"],\n",
|
||||
" factor=row[\"factor\"]\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" session.add_all(rows)\n",
|
||||
" session.commit()\n",
|
||||
"finally:\n",
|
||||
" postgresql_engin.dispose()\n",
|
||||
" sqlite_engine.dispose()"
|
||||
],
|
||||
"id": "48821306efc640a1",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"['2025-12-25', '2025-12-26', '2025-12-29', '2025-12-30', '2025-12-31', '2026-01-05', '2026-01-06', '2026-01-07', '2026-01-08', '2026-01-09']\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 22
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-01-30T09:24:09.859231Z",
|
||||
"start_time": "2026-01-30T09:24:09.746912Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import tushare as ts\n",
|
||||
"\n",
|
||||
"pro = ts.pro_api(\"64ebff4fa679167600b905ee45dd88e76f3963c0ff39157f3f085f0e\")\n",
|
||||
"# stocks = pro.stock_basic(ts_code=\"600200.SH\", list_status=\"D\", fields=\"ts_code,name,fullname,market,exchange,industry,list_date,delist_date\")\n",
|
||||
"# stocks"
|
||||
],
|
||||
"id": "ed58a1faaf2cdb8e",
|
||||
"outputs": [],
|
||||
"execution_count": 34
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-01-30T07:14:29.897120Z",
|
||||
"start_time": "2026-01-30T07:14:29.664124Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": "# stocks.to_csv(\"dlist.csv\")",
|
||||
"id": "3c8c0a38d6b2992e",
|
||||
"outputs": [],
|
||||
"execution_count": 24
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-01-30T09:46:34.808300Z",
|
||||
"start_time": "2026-01-30T09:46:34.129412Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"daily_df = pro.daily(trade_date=\"20251231\")\n",
|
||||
"daily_df.set_index(\"ts_code\", inplace=True)\n",
|
||||
"factor_df = pro.adj_factor(trade_date=\"20251231\")\n",
|
||||
"factor_df.set_index(\"ts_code\", inplace=True)"
|
||||
],
|
||||
"id": "c052a945869aa329",
|
||||
"outputs": [],
|
||||
"execution_count": 50
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-01-30T09:46:36.697015Z",
|
||||
"start_time": "2026-01-30T09:46:36.642975Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"result_df = daily_df.join(factor_df, lsuffix=\"_daily\", rsuffix=\"_factor\", how=\"left\")\n",
|
||||
"result_df\n",
|
||||
"# factor_df"
|
||||
],
|
||||
"id": "d61ee80d2cd9f06b",
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
" trade_date_daily open high low close pre_close change \\\n",
|
||||
"ts_code \n",
|
||||
"000001.SZ 20251231 11.48 11.49 11.40 11.41 11.48 -0.07 \n",
|
||||
"000002.SZ 20251231 4.66 4.68 4.62 4.65 4.62 0.03 \n",
|
||||
"000004.SZ 20251231 11.30 11.35 11.07 11.08 11.27 -0.19 \n",
|
||||
"000006.SZ 20251231 9.95 10.03 9.69 9.95 9.86 0.09 \n",
|
||||
"000007.SZ 20251231 11.72 11.75 11.28 11.44 11.62 -0.18 \n",
|
||||
"... ... ... ... ... ... ... ... \n",
|
||||
"920978.BJ 20251231 37.64 38.39 36.88 36.90 37.78 -0.88 \n",
|
||||
"920981.BJ 20251231 32.20 32.29 31.75 31.96 32.07 -0.11 \n",
|
||||
"920982.BJ 20251231 233.00 238.49 232.10 233.70 234.80 -1.10 \n",
|
||||
"920985.BJ 20251231 7.32 7.35 7.17 7.19 7.30 -0.11 \n",
|
||||
"920992.BJ 20251231 17.33 17.60 17.29 17.39 17.38 0.01 \n",
|
||||
"\n",
|
||||
" pct_chg vol amount trade_date_factor adj_factor \n",
|
||||
"ts_code \n",
|
||||
"000001.SZ -0.6098 590620.37 675457.357 20251231 134.5794 \n",
|
||||
"000002.SZ 0.6494 1075561.25 499883.113 20251231 181.7040 \n",
|
||||
"000004.SZ -1.6859 18056.00 20248.567 20251231 4.0640 \n",
|
||||
"000006.SZ 0.9128 270369.08 267758.676 20251231 39.7400 \n",
|
||||
"000007.SZ -1.5491 80556.00 92109.366 20251231 8.2840 \n",
|
||||
"... ... ... ... ... ... \n",
|
||||
"920978.BJ -2.3293 33945.04 126954.937 20251231 1.2885 \n",
|
||||
"920981.BJ -0.3430 8237.16 26301.206 20251231 1.4343 \n",
|
||||
"920982.BJ -0.4685 5210.09 122452.646 20251231 4.2831 \n",
|
||||
"920985.BJ -1.5068 35174.30 25350.257 20251231 1.6280 \n",
|
||||
"920992.BJ 0.0575 6991.87 12193.445 20251231 1.4932 \n",
|
||||
"\n",
|
||||
"[5458 rows x 12 columns]"
|
||||
],
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>trade_date_daily</th>\n",
|
||||
" <th>open</th>\n",
|
||||
" <th>high</th>\n",
|
||||
" <th>low</th>\n",
|
||||
" <th>close</th>\n",
|
||||
" <th>pre_close</th>\n",
|
||||
" <th>change</th>\n",
|
||||
" <th>pct_chg</th>\n",
|
||||
" <th>vol</th>\n",
|
||||
" <th>amount</th>\n",
|
||||
" <th>trade_date_factor</th>\n",
|
||||
" <th>adj_factor</th>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>ts_code</th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" <th></th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>000001.SZ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>11.48</td>\n",
|
||||
" <td>11.49</td>\n",
|
||||
" <td>11.40</td>\n",
|
||||
" <td>11.41</td>\n",
|
||||
" <td>11.48</td>\n",
|
||||
" <td>-0.07</td>\n",
|
||||
" <td>-0.6098</td>\n",
|
||||
" <td>590620.37</td>\n",
|
||||
" <td>675457.357</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>134.5794</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>000002.SZ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>4.66</td>\n",
|
||||
" <td>4.68</td>\n",
|
||||
" <td>4.62</td>\n",
|
||||
" <td>4.65</td>\n",
|
||||
" <td>4.62</td>\n",
|
||||
" <td>0.03</td>\n",
|
||||
" <td>0.6494</td>\n",
|
||||
" <td>1075561.25</td>\n",
|
||||
" <td>499883.113</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>181.7040</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>000004.SZ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>11.30</td>\n",
|
||||
" <td>11.35</td>\n",
|
||||
" <td>11.07</td>\n",
|
||||
" <td>11.08</td>\n",
|
||||
" <td>11.27</td>\n",
|
||||
" <td>-0.19</td>\n",
|
||||
" <td>-1.6859</td>\n",
|
||||
" <td>18056.00</td>\n",
|
||||
" <td>20248.567</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>4.0640</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>000006.SZ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>9.95</td>\n",
|
||||
" <td>10.03</td>\n",
|
||||
" <td>9.69</td>\n",
|
||||
" <td>9.95</td>\n",
|
||||
" <td>9.86</td>\n",
|
||||
" <td>0.09</td>\n",
|
||||
" <td>0.9128</td>\n",
|
||||
" <td>270369.08</td>\n",
|
||||
" <td>267758.676</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>39.7400</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>000007.SZ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>11.72</td>\n",
|
||||
" <td>11.75</td>\n",
|
||||
" <td>11.28</td>\n",
|
||||
" <td>11.44</td>\n",
|
||||
" <td>11.62</td>\n",
|
||||
" <td>-0.18</td>\n",
|
||||
" <td>-1.5491</td>\n",
|
||||
" <td>80556.00</td>\n",
|
||||
" <td>92109.366</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>8.2840</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>...</th>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>920978.BJ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>37.64</td>\n",
|
||||
" <td>38.39</td>\n",
|
||||
" <td>36.88</td>\n",
|
||||
" <td>36.90</td>\n",
|
||||
" <td>37.78</td>\n",
|
||||
" <td>-0.88</td>\n",
|
||||
" <td>-2.3293</td>\n",
|
||||
" <td>33945.04</td>\n",
|
||||
" <td>126954.937</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>1.2885</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>920981.BJ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>32.20</td>\n",
|
||||
" <td>32.29</td>\n",
|
||||
" <td>31.75</td>\n",
|
||||
" <td>31.96</td>\n",
|
||||
" <td>32.07</td>\n",
|
||||
" <td>-0.11</td>\n",
|
||||
" <td>-0.3430</td>\n",
|
||||
" <td>8237.16</td>\n",
|
||||
" <td>26301.206</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>1.4343</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>920982.BJ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>233.00</td>\n",
|
||||
" <td>238.49</td>\n",
|
||||
" <td>232.10</td>\n",
|
||||
" <td>233.70</td>\n",
|
||||
" <td>234.80</td>\n",
|
||||
" <td>-1.10</td>\n",
|
||||
" <td>-0.4685</td>\n",
|
||||
" <td>5210.09</td>\n",
|
||||
" <td>122452.646</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>4.2831</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>920985.BJ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>7.32</td>\n",
|
||||
" <td>7.35</td>\n",
|
||||
" <td>7.17</td>\n",
|
||||
" <td>7.19</td>\n",
|
||||
" <td>7.30</td>\n",
|
||||
" <td>-0.11</td>\n",
|
||||
" <td>-1.5068</td>\n",
|
||||
" <td>35174.30</td>\n",
|
||||
" <td>25350.257</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>1.6280</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>920992.BJ</th>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>17.33</td>\n",
|
||||
" <td>17.60</td>\n",
|
||||
" <td>17.29</td>\n",
|
||||
" <td>17.39</td>\n",
|
||||
" <td>17.38</td>\n",
|
||||
" <td>0.01</td>\n",
|
||||
" <td>0.0575</td>\n",
|
||||
" <td>6991.87</td>\n",
|
||||
" <td>12193.445</td>\n",
|
||||
" <td>20251231</td>\n",
|
||||
" <td>1.4932</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"<p>5458 rows × 12 columns</p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
"execution_count": 51,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"execution_count": 51
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 2
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython2",
|
||||
"version": "2.7.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
1157
notebook/indicator.ipynb
Normal file
1157
notebook/indicator.ipynb
Normal file
File diff suppressed because one or more lines are too long
82
notebook/sqlalchemy.ipynb
Normal file
82
notebook/sqlalchemy.ipynb
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-28
|
||||
@@ -0,0 +1,287 @@
|
||||
## Context
|
||||
|
||||
**Current State:**
|
||||
`backtest.py` (284 lines) 是单一文件,包含:
|
||||
- 命令行参数解析
|
||||
- 数据库连接和数据加载
|
||||
- 策略动态加载和验证
|
||||
- 回测执行逻辑
|
||||
- 结果格式化输出
|
||||
- 图表生成
|
||||
|
||||
**Constraints:**
|
||||
- 需要保持现有功能完整性(数据加载、策略加载、回测执行、结果展示)
|
||||
- 需要支持多股票回测(串行执行)
|
||||
- 不考虑并发实现(保持简单)
|
||||
- 错误处理采用立即失败策略
|
||||
- 数据库配置明文存储,不考虑环境变量
|
||||
|
||||
**Stakeholders:**
|
||||
- 开发者:需要清晰的模块划分和可复用的接口
|
||||
- 终端用户:需要友好的 CLI 输出(进度条、表格化结果)
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
1. 分离核心逻辑与 CLI 界面,提升代码复用性
|
||||
2. 提供标准化函数接口,供其他模块调用回测功能
|
||||
3. 支持多股票批量回测(串行执行)
|
||||
4. 集中管理配置(数据库、参数、配色)
|
||||
5. 优化 CLI 输出体验(tabulate 表格化、tqdm 进度条)
|
||||
|
||||
**Non-Goals:**
|
||||
1. 并行执行多股票回测(性能优化非目标)
|
||||
2. 环境变量管理配置(配置明文存储即可)
|
||||
3. 复杂的聚合统计(仅单股票结果拼接)
|
||||
4. 图表文件合并(每个股票生成独立 HTML)
|
||||
5. 配置文件热重载(启动时加载一次)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: 三层模块划分
|
||||
**选择:** 分离为 `config.py`、`backtest_core.py`、`backtest_command.py` 三个文件
|
||||
|
||||
**理由:**
|
||||
- **config.py**:集中管理所有配置,避免硬编码分散
|
||||
- **backtest_core.py**:纯粹的业务逻辑,提供可复用的函数接口
|
||||
- **backtest_command.py**:CLI 界面,负责参数解析和结果展示
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:保留单一文件,但改进内部结构(函数分离)
|
||||
- 拒绝理由:仍无法复用,CLI 和业务逻辑耦合
|
||||
- 方案 B:使用类封装(如 `BacktestEngine` 类)
|
||||
- 拒绝理由:增加复杂度,函数接口已足够
|
||||
|
||||
### Decision 2: BacktestResult 数据类
|
||||
**选择:** 使用 `dataclasses.dataclass` 定义 `BacktestResult`
|
||||
|
||||
**理由:**
|
||||
- 结构化返回结果,便于序列化和导出
|
||||
- 类型提示支持,提升代码可读性
|
||||
- 自动生成 `__init__`、`__repr__` 等方法,减少样板代码
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:直接返回原始 `stats` 对象(backtesting 库返回)
|
||||
- 拒绝理由:依赖 backtesting 库内部结构,耦合度高
|
||||
- 方案 B:返回字典
|
||||
- 拒绝理由:缺乏类型提示,容易拼写错误
|
||||
|
||||
### Decision 3: 批量回测策略
|
||||
**选择:** 串行执行(`for` 循环),立即失败
|
||||
|
||||
**理由:**
|
||||
- 简单可靠,易于调试
|
||||
- 错误处理清晰(第一个失败就停止)
|
||||
- 避免并发带来的资源竞争和复杂度
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:并行执行(ThreadPoolExecutor)
|
||||
- 拒绝理由:性能非目标,并发增加复杂度
|
||||
- 方案 B:继续执行其他股票,最后统一报告错误
|
||||
- 拒绝理由:用户需求是立即失败
|
||||
|
||||
### Decision 4: CLI 参数设计
|
||||
**选择:** `--codes` 多值参数(`nargs='+'`),`--output-dir` 目录参数
|
||||
|
||||
**理由:**
|
||||
- `--codes` 支持传入多个股票代码,如 `--codes 000001.SZ 600000.SH`
|
||||
- `--output-dir` 为每个股票生成 `{code}.html`,如 `output/000001.SZ.html`
|
||||
- 保持原有参数(`--start-date`、`--end-date`、`--strategy-file`、`--cash`、`--commission`、`--warmup-days`)
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:`--code` 逗号分隔(如 `--code 000001.SZ,600000.SH`)
|
||||
- 拒绝理由:需要额外解析逻辑,不直观
|
||||
- 方案 B:`--code` 多次调用(如 `--code 000001.SZ --code 600000.SH`)
|
||||
- 拒绝理由:argparse 的 `nargs='+'` 更符合习惯
|
||||
|
||||
### Decision 5: 输出优化库
|
||||
**选择:** 使用 `tabulate` 表格化批量结果,使用 `tqdm` 显示进度条
|
||||
|
||||
**理由:**
|
||||
- **tabulate**:提供美观的表格输出,支持多种格式(grid、simple 等)
|
||||
- **tqdm**:提供实时进度条,提升用户体验
|
||||
- 两个库都是轻量级,不引入复杂依赖
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:手动格式化表格(字符串拼接)
|
||||
- 拒绝理由:代码冗余,格式不够美观
|
||||
- 方案 B:不使用进度条(仅输出完成提示)
|
||||
- 拒绝理由:多股票回测耗时较长,用户需要进度反馈
|
||||
|
||||
### Decision 6: 结果展示策略
|
||||
**选择:** 单股票使用详细格式(现有),多股票使用表格格式(新增)
|
||||
|
||||
**理由:**
|
||||
- 单股票:保持原有的详细输出(每个指标单独一行)
|
||||
- 多股票:使用 `tabulate` 表格横向对比,节省垂直空间
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:所有情况都使用详细格式(拼接)
|
||||
- 拒绝理由:多股票时输出过长,难以阅读
|
||||
- 方案 B:所有情况都使用表格格式
|
||||
- 拒绝理由:单股票时表格优势不明显,详细格式更清晰
|
||||
|
||||
### Decision 7: 配置管理方式
|
||||
**选择:** 明文常量存储在 `config.py`
|
||||
|
||||
**理由:**
|
||||
- 满足用户需求(不考虑信息安全)
|
||||
- 避免引入 `python-dotenv` 依赖
|
||||
- 代码简洁,修改直接
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:环境变量(`os.getenv`)
|
||||
- 拒绝理由:用户明确不需要
|
||||
- 方案 B:配置文件(JSON/YAML)
|
||||
- 拒绝理由:增加文件管理和解析复杂度
|
||||
|
||||
### Decision 8: 数据访问接口
|
||||
**选择:** `load_data_from_db(code, start_date, end_date)` 函数签名保持不变
|
||||
|
||||
**理由:**
|
||||
- 现有接口已满足需求(单次查询一个股票)
|
||||
- 迁移成本低,直接复制到 `backtest_core.py`
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:批量查询(`load_data_from_db(codes, start_date, end_date)`)
|
||||
- 拒绝理由:需要修改 SQL 为 `IN` 子句,且结果聚合复杂
|
||||
- 方案 B:连接池复用(全局 engine 对象)
|
||||
- 拒绝理由:每次创建引擎的开销可接受(串行执行)
|
||||
|
||||
### Decision 9: 策略加载接口
|
||||
**选择:** `load_strategy(strategy_file)` 返回 `(calculate_indicators, strategy_class)` 元组
|
||||
|
||||
**理由:**
|
||||
- 保持现有接口,迁移成本低
|
||||
- 函数返回两个值符合 Python 惯例
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:返回类对象(策略类自带指标计算方法)
|
||||
- 拒绝理由:现有策略文件结构分离了两者,修改成本高
|
||||
- 方案 B:返回命名空间对象(封装两个属性)
|
||||
- 拒绝理由:增加复杂度,元组足够
|
||||
|
||||
### Decision 10: 错误处理策略
|
||||
**选择:** 立即失败(不捕获部分错误继续执行)
|
||||
|
||||
**理由:**
|
||||
- 符合用户需求
|
||||
- 简化错误追踪(第一个错误直接暴露)
|
||||
- 避免"部分成功"的歧义状态
|
||||
|
||||
**替代方案:**
|
||||
- 方案 A:捕获错误但继续执行,最后统一报告
|
||||
- 拒绝理由:用户明确要求立即失败
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### Risk 1: CLI 命令变化导致用户习惯中断
|
||||
**风险:** 用户习惯使用 `python backtest.py`,需要切换到 `uv run python backtest_command.py`
|
||||
|
||||
**缓解:**
|
||||
- 在项目根目录创建软链接 `backtest.py -> backtest_command.py`(可选)
|
||||
- 或在 README 中明确说明新的使用方式
|
||||
- 提供迁移指南(参数变化说明)
|
||||
|
||||
### Risk 2: 多股票串行执行耗时较长
|
||||
**风险:** 10 个股票可能需要 10 倍时间(每个 30 秒 → 总计 5 分钟)
|
||||
|
||||
**缓解:**
|
||||
- 使用 `tqdm` 进度条提供实时反馈
|
||||
- 在 README 中说明性能限制
|
||||
- 未来可扩展为并行执行(非当前目标)
|
||||
|
||||
### Risk 3: BacktestResult 字段可能与 backtesting 库不兼容
|
||||
**风险:** backtesting 库升级后,stats 对象的键名可能变化
|
||||
|
||||
**缓解:**
|
||||
- 使用 `.get(key, default)` 方法访问,避免 KeyError
|
||||
- 提供默认值(0 或空字符串)
|
||||
- 在文档中说明依赖的 backtesting 版本
|
||||
|
||||
### Risk 4: tabulate/tqdm 依赖未安装
|
||||
**风险:** 用户运行时缺少依赖,导致 ImportError
|
||||
|
||||
**缓解:**
|
||||
- 使用 `uv add` 明确添加依赖到 pyproject.toml
|
||||
- 在 README 中说明安装步骤
|
||||
- 错误信息中提示安装命令(`uv add tabulate tqdm`)
|
||||
|
||||
### Risk 5: 策略文件路径处理不一致
|
||||
**风险:** 策略文件路径可能是相对路径或绝对路径,导致加载失败
|
||||
|
||||
**缓解:**
|
||||
- 使用 `os.path.abspath()` 转换为绝对路径
|
||||
- 在错误信息中提示用户检查路径
|
||||
- 测试相对路径和绝对路径两种情况
|
||||
|
||||
### Risk 6: 图表输出目录不存在
|
||||
**风险:** 用户指定的 `--output-dir` 不存在,导致保存失败
|
||||
|
||||
**缓解:**
|
||||
- 使用 `os.makedirs(output_dir, exist_ok=True)` 自动创建
|
||||
- 在错误信息中提示用户检查目录权限
|
||||
|
||||
### Risk 7: 内存占用(多股票同时加载数据)
|
||||
**风险:** 如果同时加载多个股票数据,内存占用可能较高
|
||||
|
||||
**缓解:**
|
||||
- 串行执行确保一次只加载一个股票的数据
|
||||
- 单个股票的数据量可控(10 年约几 MB)
|
||||
- future 可考虑流式处理(非当前目标)
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Step 1: 创建 config.py
|
||||
1. 从 `backtest.py` 提取数据库配置
|
||||
2. 添加默认回测参数
|
||||
3. 添加图表配色配置
|
||||
4. 测试导入无错误
|
||||
|
||||
### Step 2: 创建 backtest_core.py
|
||||
1. 迁移 `load_data_from_db()` 函数(导入 config)
|
||||
2. 迁移 `load_strategy()` 函数
|
||||
3. 迁移 `apply_color_scheme()` 函数(使用 config 配置)
|
||||
4. 定义 `BacktestResult` 数据类
|
||||
5. 实现 `run_backtest()` 函数
|
||||
6. 实现 `run_batch_backtest()` 函数
|
||||
7. 单元测试核心函数
|
||||
|
||||
### Step 3: 创建 backtest_command.py
|
||||
1. 实现 `parse_arguments()` 函数(支持 `--codes`)
|
||||
2. 实现 `format_single_result()` 函数(详细格式)
|
||||
3. 实现 `format_batch_results()` 函数(使用 tabulate)
|
||||
4. 实现 `main()` 函数(调用 `run_batch_backtest()`)
|
||||
5. 测试单股票回测
|
||||
6. 测试多股票回测
|
||||
|
||||
### Step 4: 更新依赖
|
||||
1. 运行 `uv add tabulate` 添加依赖
|
||||
2. 运行 `uv add tqdm` 添加依赖
|
||||
3. 运行 `uv sync` 同步依赖
|
||||
|
||||
### Step 5: 删除 backtest.py
|
||||
1. 确认新功能完整(单股票、多股票、图表输出)
|
||||
2. 确认错误处理正确(立即失败)
|
||||
3. 删除 `backtest.py` 文件
|
||||
4. 更新 README 说明新的使用方式
|
||||
|
||||
### Rollback Strategy
|
||||
如果迁移过程中发现问题:
|
||||
1. 保留 `backtest.py` 直到 `backtest_command.py` 完全可用
|
||||
2. 使用 `git` 版本控制,可随时回退
|
||||
3. 逐步迁移(先核心函数,后 CLI),确保每步可验证
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **BacktestResult 字段完整性:** 是否需要包含所有 backtesting.stats 的键,或仅包含当前用到的字段?
|
||||
- 倾向:仅包含当前用到的字段(未来可扩展)
|
||||
|
||||
2. **表格格式选择:** tabulate 支持多种格式(grid、simple、pipe、html),多股票结果使用哪种?
|
||||
- 倾向:grid(美观的边框格式)
|
||||
|
||||
3. **进度条粒度:** tqdm 进度条应该显示每个股票的回测进度,还是仅显示批量回测的总进度?
|
||||
- 倾向:仅显示批量回测的总进度(股票 N/M)
|
||||
|
||||
4. **图表输出目录结构:** 多股票图表是平铺在 `output/` 下,还是按日期/策略分组?
|
||||
- 倾向:平铺在 `output/` 下(简单)
|
||||
@@ -0,0 +1,54 @@
|
||||
## Why
|
||||
|
||||
当前 `backtest.py` 存在职责混杂的问题:命令行参数解析、核心回测逻辑、数据访问、结果展示都耦合在单一文件中,导致:
|
||||
- 难以在其他模块中复用回测功能
|
||||
- 无法进行单元测试
|
||||
- 仅支持单股票回测,无法批量处理
|
||||
|
||||
需要重构为分层架构,将核心逻辑与 CLI 界面分离,提升代码复用性和可维护性。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **创建 `config.py`**:集中管理数据库配置、默认回测参数、图表配色
|
||||
- **创建 `backtest_core.py`**:核心回测引擎
|
||||
- 提供标准化接口 `run_backtest()`(单股票)
|
||||
- 提供批量接口 `run_batch_backtest()`(多股票,串行执行)
|
||||
- 封装数据访问和策略加载逻辑
|
||||
- 返回结构化结果对象 `BacktestResult`
|
||||
- **创建 `backtest_command.py`**:命令行界面
|
||||
- 支持多股票代码参数 `--codes`(接受多个值)
|
||||
- 使用 `tabulate` 优化批量结果的表格展示
|
||||
- 使用 `tqdm` 显示批量回测进度条
|
||||
- 保留原有的单股票详细输出格式
|
||||
- **删除 `backtest.py`**:不再需要,功能已迁移
|
||||
- **依赖更新**:添加 `tabulate`、`tqdm` 到项目依赖
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `batch-backtest`: 批量回测功能,支持传入多个股票代码进行串行回测,并提供进度条和表格化结果展示
|
||||
|
||||
### Modified Capabilities
|
||||
- 无(其他均为实现重构,不改变 spec 级别行为)
|
||||
|
||||
## Impact
|
||||
|
||||
- **代码影响**:
|
||||
- 删除 `backtest.py`(284 行)
|
||||
- 新增 `config.py`(约 30 行)
|
||||
- 新增 `backtest_core.py`(约 250 行)
|
||||
- 新增 `backtest_command.py`(约 150 行)
|
||||
- **API 变化**:
|
||||
- 新增 `run_backtest(code, start_date, end_date, strategy_file, ...)` 函数
|
||||
- 新增 `run_batch_backtest(codes, start_date, end_date, strategy_file, ...)` 函数
|
||||
- 新增 `BacktestResult` 数据类
|
||||
- **命令行变化**:
|
||||
- 单参数 `--code` 改为多值参数 `--codes`
|
||||
- 新增 `--output-dir` 参数,为每个股票生成独立 HTML 图表
|
||||
- 批量回测时显示进度条和表格化结果
|
||||
- **依赖变化**:
|
||||
- 新增 `tabulate`(表格格式化)
|
||||
- 新增 `tqdm`(进度条显示)
|
||||
- **兼容性**:
|
||||
- **BREAKING**: 删除原有 `backtest.py`,命令行使用方式从 `python backtest.py` 改为 `uv run python backtest_command.py`
|
||||
- 参数名称从 `--code` 改为 `--codes`
|
||||
@@ -0,0 +1,310 @@
|
||||
# Spec: Batch Backtest
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 多股票回测参数
|
||||
系统 SHALL 支持通过命令行参数传入多个股票代码进行批量回测。
|
||||
|
||||
#### Scenario: 传入多个股票代码
|
||||
- **WHEN** 用户执行 `python backtest_command.py --codes 000001.SZ 600000.SH --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategies/macd_strategy.py`
|
||||
- **THEN** 系统解析所有股票代码到列表 `['000001.SZ', '600000.SH']`
|
||||
- **THEN** 系统按顺序依次执行每个股票的回测
|
||||
- **THEN** 系统为每个股票生成独立的回测结果
|
||||
|
||||
#### Scenario: 传入单个股票代码
|
||||
- **WHEN** 用户执行 `python backtest_command.py --codes 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategies/macd_strategy.py`
|
||||
- **THEN** 系统解析为单个股票代码列表 `['000001.SZ']`
|
||||
- **THEN** 系统执行单个股票回测
|
||||
- **THEN** 系统输出详细格式的回测结果
|
||||
|
||||
#### Scenario: 缺少 --codes 参数
|
||||
- **WHEN** 用户未提供 `--codes` 参数
|
||||
- **THEN** 系统输出错误信息:"错误: 需要以下参数: --codes"
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 批量回测执行
|
||||
系统 SHALL 串行执行多个股票的回测,每次加载一个股票的数据并执行回测。
|
||||
|
||||
#### Scenario: 成功执行多个股票回测
|
||||
- **WHEN** 用户传入 N 个股票代码
|
||||
- **THEN** 系统循环 N 次,每次加载一个股票的数据
|
||||
- **THEN** 系统每次执行完整的回测流程(数据加载、指标计算、回测执行)
|
||||
- **THEN** 系统每次执行完成后生成 `BacktestResult` 对象
|
||||
- **THEN** 系统返回包含 N 个 `BacktestResult` 的列表
|
||||
|
||||
#### Scenario: 每个股票独立预热期
|
||||
- **WHEN** 系统执行第 i 个股票的回测
|
||||
- **THEN** 系统使用 `start_date - warmup_days` 计算该股票的预热开始日期
|
||||
- **THEN** 系统独立加载该股票的预热期数据
|
||||
- **THEN** 不同股票的预热期互不影响
|
||||
|
||||
#### Scenario: 第一个股票回测失败
|
||||
- **WHEN** 系统执行第一个股票回测时发生错误(数据库连接失败、策略加载失败等)
|
||||
- **THEN** 系统捕获异常并输出错误信息
|
||||
- **THEN** 系统停止执行后续股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码(立即失败策略)
|
||||
|
||||
#### Scenario: 中间股票回测失败
|
||||
- **WHEN** 系统执行第 i 个股票回测时发生错误
|
||||
- **THEN** 系统输出错误信息(包含股票代码)
|
||||
- **THEN** 系统停止执行后续股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
#### Scenario: 资源管理
|
||||
- **WHEN** 系统完成第 i 个股票的回测
|
||||
- **THEN** 系统关闭该股票的数据库连接(`engine.dispose()`)
|
||||
- **THEN** 系统释放该股票的数据内存
|
||||
- **THEN** 系统开始加载第 i+1 个股票的数据
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 批量回测进度显示
|
||||
系统 SHALL 使用 tqdm 显示批量回测的实时进度,提供用户反馈。
|
||||
|
||||
#### Scenario: 显示进度条
|
||||
- **WHEN** 系统开始执行 N 个股票的批量回测
|
||||
- **THEN** 系统显示进度条格式:`回测进度: 25%|█████▌ | 1/4 [00:30<01:30, 12.5s/it]`
|
||||
- **THEN** 系统在完成每个股票回测后更新进度条
|
||||
- **THEN** 进度条显示当前进度(i/N)、已用时间、预计剩余时间
|
||||
- **THEN** 进度条在所有股票回测完成后消失
|
||||
|
||||
#### Scenario: 单股票回测不显示进度条
|
||||
- **WHEN** 用户传入单个股票代码
|
||||
- **THEN** 系统不显示 tqdm 进度条
|
||||
- **THEN** 系统直接输出回测结果
|
||||
|
||||
#### Scenario: 进度条描述文本
|
||||
- **WHEN** 系统显示批量回测进度
|
||||
- **THEN** 进度条描述 SHALL 为 "回测进度"(中文)
|
||||
- **THEN** 进度条显示已完成/总数(如 "1/4", "2/4")
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 批量回测结果展示
|
||||
系统 SHALL 使用 tabulate 将多个股票的回测结果格式化为表格,便于横向对比。
|
||||
|
||||
#### Scenario: 表格化输出多股票结果
|
||||
- **WHEN** 用户传入多个股票代码且回测成功
|
||||
- **THEN** 系统使用 tabulate 生成表格
|
||||
- **THEN** 表格格式 SHALL 为 grid(带边框)
|
||||
- **THEN** 表格列 SHALL 包含:股票代码、收益率%、胜率%、最大回撤%、交易次数、SQN
|
||||
- **THEN** 系统在表格上方显示表头(中文列名)
|
||||
- **THEN** 数值保留 2 位小数(交易次数为整数)
|
||||
|
||||
#### Scenario: 表格内容填充
|
||||
- **WHEN** 系统格式化第 i 个股票的结果
|
||||
- **THEN** 系统从 `BacktestResult` 对象提取字段
|
||||
- **THEN** "股票代码" 列填充 `result.code`
|
||||
- **THEN** "收益率%" 列填充 `result.return_pct`
|
||||
- **THEN** "胜率%" 列填充 `result.win_rate`
|
||||
- **THEN** "最大回撤%" 列填充 `result.max_drawdown`
|
||||
- **THEN** "交易次数" 列填充 `result.trades`
|
||||
- **THEN** "SQN" 列填充 `result.sqn`
|
||||
|
||||
#### Scenario: 单股票回测不使用表格
|
||||
- **WHEN** 用户传入单个股票代码
|
||||
- **THEN** 系统不使用 tabulate 生成表格
|
||||
- **THEN** 系统使用详细格式输出(每个指标单独一行)
|
||||
- **THEN** 系统保持原有 `print_stats()` 的输出格式
|
||||
|
||||
#### Scenario: 表格示例输出
|
||||
- **WHEN** 用户传入 2 个股票代码
|
||||
- **THEN** 系统输出格式 SHALL 为:
|
||||
```
|
||||
+-------------+-----------+--------+------------+----------+-------+
|
||||
| 股票代码 | 收益率% | 胜率% | 最大回撤% | 交易次数 | SQN |
|
||||
+-------------+-----------+--------+------------+----------+-------+
|
||||
| 000001.SZ | 20.35 | 55.00 | -8.50 | 45 | 1.85 |
|
||||
| 600000.SH | 15.00 | 48.00 | -12.30 | 38 | 1.42 |
|
||||
+-------------+-----------+--------+------------+----------+-------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 多股票图表输出
|
||||
系统 SHALL 为每个股票生成独立的 HTML 图表文件,文件名格式为 `{code}.html`。
|
||||
|
||||
#### Scenario: 指定 --output-dir 参数
|
||||
- **WHEN** 用户传入 `--output-dir output/`
|
||||
- **THEN** 系统为每个股票生成 HTML 文件到 `output/{code}.html`
|
||||
- **THEN** 文件名 SHALL 为股票代码,如 `000001.SZ.html`, `600000.SH.html`
|
||||
- **THEN** 系统自动创建 `output/` 目录(`exist_ok=True`)
|
||||
- **THEN** 系统在完成后输出提示:"图表已保存到目录: output/" 后列出所有文件
|
||||
|
||||
#### Scenario: 未指定 --output-dir 参数
|
||||
- **WHEN** 用户未传入 `--output-dir` 参数
|
||||
- **THEN** 系统不为任何股票生成图表文件
|
||||
- **THEN** 系统仅输出控制台统计信息
|
||||
|
||||
#### Scenario: 图表文件覆盖
|
||||
- **WHEN** 系统再次执行相同的批量回测
|
||||
- **THEN** 系统覆盖已存在的 HTML 文件
|
||||
- **THEN** 系统不提示文件已存在
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 结构化回测结果
|
||||
系统 SHALL 返回标准化的 `BacktestResult` 对象,包含所有关键指标。
|
||||
|
||||
#### Scenario: BacktestResult 对象创建
|
||||
- **WHEN** 系统完成单股票回测
|
||||
- **THEN** 系统从 `stats` 对象提取指标到 `BacktestResult`
|
||||
- **THEN** `BacktestResult.code` 设置为股票代码
|
||||
- **THEN** `BacktestResult.start_date` 设置为回测开始日期
|
||||
- **THEN** `BacktestResult.end_date` 设置为回测结束日期
|
||||
- **THEN** `BacktestResult.equity_final` 设置为最终权益
|
||||
- **THEN** `BacktestResult.equity_peak` 设置为峰值收益
|
||||
- **THEN** `BacktestResult.return_pct` 设置为总收益率
|
||||
- **THEN** `BacktestResult.buy_hold_return` 设置为买入持有收益率
|
||||
- **THEN** `BacktestResult.return_annual` 设置为年化收益率
|
||||
- **THEN** `BacktestResult.volatility_annual` 设置为年化波动率
|
||||
- **THEN** `BacktestResult.max_drawdown` 设置为最大回撤
|
||||
- **THEN** `BacktestResult.avg_drawdown` 设置为平均回撤
|
||||
- **THEN** `BacktestResult.max_drawdown_duration` 设置为最大回撤持续时长
|
||||
- **THEN** `BacktestResult.avg_drawdown_duration` 设置为平均回撤持续时长
|
||||
- **THEN** `BacktestResult.sortino_ratio` 设置为索提诺比率
|
||||
- **THEN** `BacktestResult.calmar_ratio` 设置为卡尔玛比率
|
||||
- **THEN** `BacktestResult.trades` 设置为交易次数
|
||||
- **THEN** `BacktestResult.win_rate` 设置为胜率
|
||||
- **THEN** `BacktestResult.sqn` 设置为系统质量数
|
||||
- **THEN** `BacktestResult.cash` 设置为初始资金
|
||||
- **THEN** `BacktestResult.commission` 设置为手续费率
|
||||
|
||||
#### Scenario: BacktestResult 列表返回
|
||||
- **WHEN** 系统完成批量回测
|
||||
- **THEN** 系统返回 `List[BacktestResult]`
|
||||
- **THEN** 列表顺序 SHALL 与输入股票代码顺序一致
|
||||
- **THEN** 列表长度 SHALL 等于输入股票代码数量(成功时)
|
||||
|
||||
#### Scenario: BacktestResult 数据类型
|
||||
- **WHEN** 系统创建 `BacktestResult` 对象
|
||||
- **THEN** 数值字段 SHALL 为 float 类型(除 `trades`, `max_drawdown_duration` 为 int)
|
||||
- **THEN** 日期字段 SHALL 为 str 类型(YYYY-MM-DD 格式)
|
||||
- **THEN** 系统支持 `result.to_dict()` 方法(dataclass 自动生成)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 可复用回测引擎接口
|
||||
系统 SHALL 提供标准化的函数接口,供其他模块调用回测功能。
|
||||
|
||||
#### Scenario: run_backtest 函数调用
|
||||
- **WHEN** 其他模块调用 `run_backtest(code, start_date, end_date, strategy_file, cash, commission, warmup_days, output_file)`
|
||||
- **THEN** 函数接收股票代码、日期范围、策略文件、回测参数、输出文件路径
|
||||
- **THEN** 函数执行完整回测流程(数据加载、策略加载、指标计算、回测执行)
|
||||
- **THEN** 函数返回 `BacktestResult` 对象
|
||||
- **THEN** 函数不打印任何输出(纯函数)
|
||||
|
||||
#### Scenario: run_batch_backtest 函数调用
|
||||
- **WHEN** 其他模块调用 `run_batch_backtest(codes, start_date, end_date, strategy_file, cash, commission, warmup_days, output_dir)`
|
||||
- **THEN** 函数接收股票代码列表、日期范围、策略文件、回测参数、输出目录
|
||||
- **THEN** 函数串行执行每个股票的回测
|
||||
- **THEN** 函数返回 `List[BacktestResult]`
|
||||
- **THEN** 函数显示 tqdm 进度条(批量时)
|
||||
|
||||
#### Scenario: 函数参数默认值
|
||||
- **WHEN** 调用者不指定可选参数
|
||||
- **THEN** `cash` 默认为 100000
|
||||
- **THEN** `commission` 默认为 0.002
|
||||
- **THEN** `warmup_days` 默认为 365
|
||||
- **THEN** `output_file` 默认为 None(不生成图表)
|
||||
- **THEN** `output_dir` 默认为 None(不生成图表)
|
||||
|
||||
#### Scenario: 函数异常抛出
|
||||
- **WHEN** `run_backtest` 或 `run_batch_backtest` 执行时发生错误
|
||||
- **THEN** 函数 SHALL 抛出异常(不捕获)
|
||||
- **THEN** 异常类型 SHALL 为 ValueError、TypeError 或原始异常
|
||||
- **THEN** 异常信息 SHALL 包含具体错误原因
|
||||
- **THEN** 调用者负责捕获和处理异常
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 集中配置管理
|
||||
系统 SHALL 在 config.py 中集中管理数据库配置、默认回测参数、图表配色。
|
||||
|
||||
#### Scenario: 数据库配置访问
|
||||
- **WHEN** backtest_core.py 需要数据库连接参数
|
||||
- **THEN** 模块从 config 导入 `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
|
||||
- **THEN** 模块使用这些常量构建连接字符串
|
||||
- **THEN** 模块不重复定义数据库配置
|
||||
|
||||
#### Scenario: 默认参数访问
|
||||
- **WHEN** backtest_core.py 需要默认回测参数
|
||||
- **THEN** 模块从 config 导入 `DEFAULT_CASH`, `DEFAULT_COMMISSION`, `DEFAULT_WARMUP_DAYS`
|
||||
- **THEN** 模块使用这些常量作为函数默认值
|
||||
- **THEN** 模块不重复定义默认参数
|
||||
|
||||
#### Scenario: 图表配色访问
|
||||
- **WHEN** backtest_core.py 需要设置图表配色
|
||||
- **THEN** 模块从 config 导入 `BULL_COLOR`, `BEAR_COLOR`
|
||||
- **THEN** 模块使用这些颜色设置 `plotting.BULL_COLOR` 和 `plotting.BEAR_COLOR`
|
||||
- **THEN** 模块不重复定义颜色配置
|
||||
|
||||
#### Scenario: 配置文件内容
|
||||
- **WHEN** 查看 config.py 文件
|
||||
- **THEN** 文件包含数据库配置(DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD)
|
||||
- **THEN** 文件包含默认回测参数(DEFAULT_CASH, DEFAULT_COMMISSION, DEFAULT_WARMUP_DAYS)
|
||||
- **THEN** 文件包含图表配色(BULL_COLOR, BEAR_COLOR)
|
||||
- **THEN** 所有配置使用明文常量(不使用环境变量)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 错误处理策略
|
||||
系统 SHALL 在批量回测失败时立即停止执行,不继续处理后续股票。
|
||||
|
||||
#### Scenario: 数据加载失败
|
||||
- **WHEN** 系统加载第 i 个股票数据时失败(数据库错误、数据不存在)
|
||||
- **THEN** 系统捕获异常
|
||||
- **THEN** 系统输出错误信息:"回测失败 [{code}]: {error}"
|
||||
- **THEN** 系统停止执行后续股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
#### Scenario: 策略加载失败
|
||||
- **WHEN** 系统加载策略文件时失败(文件不存在、接口不完整)
|
||||
- **THEN** 系统捕获异常
|
||||
- **THEN** 系统输出错误信息:"策略加载失败: {error}"
|
||||
- **THEN** 系统停止执行所有股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
#### Scenario: 回测执行失败
|
||||
- **WHEN** 系统执行第 i 个股票回测时失败(策略逻辑错误)
|
||||
- **THEN** 系统捕获异常
|
||||
- **THEN** 系统输出错误信息和完整堆栈跟踪
|
||||
- **THEN** 系统停止执行后续股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
#### Scenario: 图表生成失败
|
||||
- **WHEN** 系统生成第 i 个股票图表时失败
|
||||
- **THEN** 系统捕获异常
|
||||
- **THEN** 系统输出警告:"图表生成失败 [{code}]: {error},但回测已完成"
|
||||
- **THEN** 系统继续执行后续股票的回测
|
||||
- **THEN** 系统在返回的 `BacktestResult` 中设置 `error` 字段(如果设计支持)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 依赖管理
|
||||
系统 SHALL 在 pyproject.toml 中添加 tabulate 和 tqdm 依赖。
|
||||
|
||||
#### Scenario: 添加 tabulate 依赖
|
||||
- **WHEN** 查看 pyproject.toml 文件
|
||||
- **THEN** 文件包含 `tabulate` 依赖
|
||||
- **THEN** 依赖版本 SHALL 为兼容当前 Python 版本的版本
|
||||
- **THEN** 系统可以导入 `import tabulate` 无错误
|
||||
|
||||
#### Scenario: 添加 tqdm 依赖
|
||||
- **WHEN** 查看 pyproject.toml 文件
|
||||
- **THEN** 文件包含 `tqdm` 依赖
|
||||
- **THEN** 依赖版本 SHALL 为兼容当前 Python 版本的版本
|
||||
- **THEN** 系统可以导入 `from tqdm import tqdm` 无错误
|
||||
|
||||
#### Scenario: 依赖安装
|
||||
- **WHEN** 用户运行 `uv sync` 或 `pip install -e .`
|
||||
- **THEN** 系统自动安装 tabulate 和 tqdm
|
||||
- **THEN** 系统显示依赖安装进度
|
||||
- **THEN** 系统完成安装后可以正常使用回测工具
|
||||
|
||||
#### Scenario: 依赖缺失提示
|
||||
- **WHEN** 系统导入 tabulate 或 tqdm 时失败
|
||||
- **THEN** 系统输出友好错误信息:"缺少依赖: {package_name},请运行: uv add {package_name}"
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
@@ -0,0 +1,96 @@
|
||||
## 1. 依赖管理
|
||||
|
||||
- [x] 1.1 运行 `uv add tabulate` 添加依赖
|
||||
- [x] 1.2 运行 `uv add tqdm` 添加依赖
|
||||
- [x] 1.3 运行 `uv sync` 同步依赖
|
||||
|
||||
## 2. 配置管理模块
|
||||
|
||||
- [x] 2.1 创建 config.py 文件
|
||||
- [x] 2.2 在 config.py 中定义数据库配置常量(DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD)
|
||||
- [x] 2.3 在 config.py 中定义默认回测参数(DEFAULT_CASH, DEFAULT_COMMISSION, DEFAULT_WARMUP_DAYS)
|
||||
- [x] 2.4 在 config.py 中定义图表配色(BULL_COLOR, BEAR_COLOR)
|
||||
- [x] 2.5 测试 config.py 导入无错误
|
||||
|
||||
## 3. 核心回测引擎
|
||||
|
||||
- [x] 3.1 创建 backtest_core.py 文件
|
||||
- [x] 3.2 在 backtest_core.py 中导入必要模块和 config
|
||||
- [x] 3.3 定义 BacktestResult dataclass(包含所有回测指标字段)
|
||||
- [x] 3.4 迁移 load_data_from_db() 函数(使用 config 数据库配置)
|
||||
- [x] 3.5 迁移 load_strategy() 函数(保持原有逻辑)
|
||||
- [x] 3.6 迁移 apply_color_scheme() 函数(使用 config 配色)
|
||||
- [x] 3.7 实现 run_backtest() 函数(单股票回测)
|
||||
- [x] 3.7.1 实现预热期日期计算逻辑
|
||||
- [x] 3.7.2 实现数据加载和策略加载调用
|
||||
- [x] 3.7.3 实现指标计算和数据截取
|
||||
- [x] 3.7.4 实现 Backtest 执行
|
||||
- [x] 3.7.5 实现图表生成(可选)
|
||||
- [x] 3.7.6 实现 BacktestResult 对象构建和返回
|
||||
- [x] 3.8 实现 run_batch_backtest() 函数(批量回测,串行)
|
||||
- [x] 3.8.1 实现循环遍历股票代码
|
||||
- [x] 3.8.2 实现为每个股票调用 run_backtest()
|
||||
- [x] 3.8.3 实现为每个股票生成独立 HTML 文件
|
||||
- [x] 3.8.4 实现结果列表收集和返回
|
||||
- [x] 3.8.5 实现 tqdm 进度条显示(批量时)
|
||||
- [x] 3.9 测试 run_backtest() 单股票回测
|
||||
- [x] 3.10 测试 run_batch_backtest() 多股票回测
|
||||
|
||||
## 4. CLI 界面模块
|
||||
|
||||
- [x] 4.1 创建 backtest_command.py 文件
|
||||
- [x] 4.2 在 backtest_command.py 中导入必要模块和 backtest_core
|
||||
- [x] 4.3 实现 parse_arguments() 函数
|
||||
- [x] 4.3.1 定义 --codes 多值参数(nargs='+')
|
||||
- [x] 4.3.2 定义 --output-dir 目录参数
|
||||
- [x] 4.3.3 保持原有参数(--start-date, --end-date, --strategy-file, --cash, --commission, --warmup-days)
|
||||
- [x] 4.3.4 添加参数帮助文档和示例
|
||||
- [x] 4.4 实现 format_single_result() 函数(详细格式输出)
|
||||
- [x] 4.4.1 实现每个指标单独一行的格式化
|
||||
- [x] 4.4.2 保持原有 print_stats() 的输出格式
|
||||
- [x] 4.5 实现 format_batch_results() 函数(表格格式输出)
|
||||
- [x] 4.5.1 实现使用 tabulate 生成表格
|
||||
- [x] 4.5.2 定义表格列:股票代码、收益率%、胜率%、最大回撤%、交易次数、SQN
|
||||
- [x] 4.5.3 实现表格数据填充(从 BacktestResult 对象提取)
|
||||
- [x] 4.5.4 实现表格格式为 grid
|
||||
- [x] 4.6 实现 main() 函数
|
||||
- [x] 4.6.1 调用 parse_arguments() 解析参数
|
||||
- [x] 4.6.2 调用 run_batch_backtest() 执行批量回测
|
||||
- [x] 4.6.3 根据结果数量调用 format_single_result() 或 format_batch_results()
|
||||
- [x] 4.6.4 实现图表保存提示(指定 --output-dir 时)
|
||||
- [x] 4.6.5 实现错误捕获和友好错误信息输出
|
||||
- [x] 4.6.6 实现退出状态码设置(成功 0,失败非零)
|
||||
- [x] 4.7 添加 `if __name__ == "__main__": main()` 入口
|
||||
- [x] 4.8 测试单股票回测命令行调用 (`uv run python backtest_command.py`)
|
||||
- [x] 4.9 测试多股票回测命令行调用 (`uv run python backtest_command.py`)
|
||||
- [x] 4.10 测试错误处理(参数缺失、文件不存在等)
|
||||
|
||||
## 5. 清理旧代码
|
||||
|
||||
- [x] 5.1 确认新功能完整(单股票、多股票、图表输出)
|
||||
- [x] 5.2 确认错误处理正确(立即失败)
|
||||
- [x] 5.3 删除 backtest.py 文件
|
||||
- [x] 5.4 验证 git 状态(仅删除旧文件,无其他修改)
|
||||
|
||||
## 6. 文档更新
|
||||
|
||||
- [x] 6.1 更新 README.md(如果存在)
|
||||
- [x] 6.1.1 说明新的命令行使用方式(`uv run python backtest_command.py`)
|
||||
- [x] 6.1.2 说明参数变化(--code 改为 --codes)
|
||||
- [x] 6.1.3 提供单股票和多股票示例
|
||||
- [x] 6.1.4 说明 --output-dir 用法(多股票图表)
|
||||
- [x] 6.2 创建 note_refactor.md(可选,记录重构说明)
|
||||
- [x] 6.2.1 说明文件结构变化
|
||||
- [x] 6.2.2 说明接口变化
|
||||
- [x] 6.2.3 提供迁移指南
|
||||
|
||||
## 7. 集成测试
|
||||
|
||||
- [x] 7.1 测试单个股票完整流程(000001.SZ)
|
||||
- [x] 7.2 测试多个股票完整流程(000001.SZ 600000.SH)
|
||||
- [x] 7.3 测试指定 --output-dir 生成图表
|
||||
- [x] 7.4 测试不指定 --output-dir(不生成图表)
|
||||
- [x] 7.5 测试错误情况(无效股票代码、不存在的策略文件等)
|
||||
- [x] 7.6 验证进度条显示(多股票时)
|
||||
- [x] 7.7 验证表格格式输出(多股票时)
|
||||
- [x] 7.8 验证详细格式输出(单股票时)
|
||||
@@ -2,6 +2,6 @@ schema: spec-driven
|
||||
|
||||
Example:
|
||||
context: |
|
||||
使用 uv 工具进行 python 环境的管理和三方依赖的管理
|
||||
严禁在主机环境直接运行 pip、pip3 安装依赖包,必须使用 uv 虚拟环境
|
||||
使用 uv 工具进行 python 环境的管理和三方依赖的管理,运行python命令的时候使用uv run python xxx
|
||||
严禁在主机环境直接运行 pip、pip3 安装依赖包,必须使用 uv add xxx命令安装
|
||||
项目面向中文开发者,文档输出、日志输出、agent 交流时都要使用中文
|
||||
|
||||
312
openspec/specs/batch-backtest/spec.md
Normal file
312
openspec/specs/batch-backtest/spec.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# batch-backtest Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change refactor-backtest-separate-cli. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: 多股票回测参数
|
||||
系统 SHALL 支持通过命令行参数传入多个股票代码进行批量回测。
|
||||
|
||||
#### Scenario: 传入多个股票代码
|
||||
- **WHEN** 用户执行 `python backtest_command.py --codes 000001.SZ 600000.SH --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategies/macd_strategy.py`
|
||||
- **THEN** 系统解析所有股票代码到列表 `['000001.SZ', '600000.SH']`
|
||||
- **THEN** 系统按顺序依次执行每个股票的回测
|
||||
- **THEN** 系统为每个股票生成独立的回测结果
|
||||
|
||||
#### Scenario: 传入单个股票代码
|
||||
- **WHEN** 用户执行 `python backtest_command.py --codes 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategies/macd_strategy.py`
|
||||
- **THEN** 系统解析为单个股票代码列表 `['000001.SZ']`
|
||||
- **THEN** 系统执行单个股票回测
|
||||
- **THEN** 系统输出详细格式的回测结果
|
||||
|
||||
#### Scenario: 缺少 --codes 参数
|
||||
- **WHEN** 用户未提供 `--codes` 参数
|
||||
- **THEN** 系统输出错误信息:"错误: 需要以下参数: --codes"
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 批量回测执行
|
||||
系统 SHALL 串行执行多个股票的回测,每次加载一个股票的数据并执行回测。
|
||||
|
||||
#### Scenario: 成功执行多个股票回测
|
||||
- **WHEN** 用户传入 N 个股票代码
|
||||
- **THEN** 系统循环 N 次,每次加载一个股票的数据
|
||||
- **THEN** 系统每次执行完整的回测流程(数据加载、指标计算、回测执行)
|
||||
- **THEN** 系统每次执行完成后生成 `BacktestResult` 对象
|
||||
- **THEN** 系统返回包含 N 个 `BacktestResult` 的列表
|
||||
|
||||
#### Scenario: 每个股票独立预热期
|
||||
- **WHEN** 系统执行第 i 个股票的回测
|
||||
- **THEN** 系统使用 `start_date - warmup_days` 计算该股票的预热开始日期
|
||||
- **THEN** 系统独立加载该股票的预热期数据
|
||||
- **THEN** 不同股票的预热期互不影响
|
||||
|
||||
#### Scenario: 第一个股票回测失败
|
||||
- **WHEN** 系统执行第一个股票回测时发生错误(数据库连接失败、策略加载失败等)
|
||||
- **THEN** 系统捕获异常并输出错误信息
|
||||
- **THEN** 系统停止执行后续股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码(立即失败策略)
|
||||
|
||||
#### Scenario: 中间股票回测失败
|
||||
- **WHEN** 系统执行第 i 个股票回测时发生错误
|
||||
- **THEN** 系统输出错误信息(包含股票代码)
|
||||
- **THEN** 系统停止执行后续股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
#### Scenario: 资源管理
|
||||
- **WHEN** 系统完成第 i 个股票的回测
|
||||
- **THEN** 系统关闭该股票的数据库连接(`engine.dispose()`)
|
||||
- **THEN** 系统释放该股票的数据内存
|
||||
- **THEN** 系统开始加载第 i+1 个股票的数据
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 批量回测进度显示
|
||||
系统 SHALL 使用 tqdm 显示批量回测的实时进度,提供用户反馈。
|
||||
|
||||
#### Scenario: 显示进度条
|
||||
- **WHEN** 系统开始执行 N 个股票的批量回测
|
||||
- **THEN** 系统显示进度条格式:`回测进度: 25%|█████▌ | 1/4 [00:30<01:30, 12.5s/it]`
|
||||
- **THEN** 系统在完成每个股票回测后更新进度条
|
||||
- **THEN** 进度条显示当前进度(i/N)、已用时间、预计剩余时间
|
||||
- **THEN** 进度条在所有股票回测完成后消失
|
||||
|
||||
#### Scenario: 单股票回测不显示进度条
|
||||
- **WHEN** 用户传入单个股票代码
|
||||
- **THEN** 系统不显示 tqdm 进度条
|
||||
- **THEN** 系统直接输出回测结果
|
||||
|
||||
#### Scenario: 进度条描述文本
|
||||
- **WHEN** 系统显示批量回测进度
|
||||
- **THEN** 进度条描述 SHALL 为 "回测进度"(中文)
|
||||
- **THEN** 进度条显示已完成/总数(如 "1/4", "2/4")
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 批量回测结果展示
|
||||
系统 SHALL 使用 tabulate 将多个股票的回测结果格式化为表格,便于横向对比。
|
||||
|
||||
#### Scenario: 表格化输出多股票结果
|
||||
- **WHEN** 用户传入多个股票代码且回测成功
|
||||
- **THEN** 系统使用 tabulate 生成表格
|
||||
- **THEN** 表格格式 SHALL 为 grid(带边框)
|
||||
- **THEN** 表格列 SHALL 包含:股票代码、收益率%、胜率%、最大回撤%、交易次数、SQN
|
||||
- **THEN** 系统在表格上方显示表头(中文列名)
|
||||
- **THEN** 数值保留 2 位小数(交易次数为整数)
|
||||
|
||||
#### Scenario: 表格内容填充
|
||||
- **WHEN** 系统格式化第 i 个股票的结果
|
||||
- **THEN** 系统从 `BacktestResult` 对象提取字段
|
||||
- **THEN** "股票代码" 列填充 `result.code`
|
||||
- **THEN** "收益率%" 列填充 `result.return_pct`
|
||||
- **THEN** "胜率%" 列填充 `result.win_rate`
|
||||
- **THEN** "最大回撤%" 列填充 `result.max_drawdown`
|
||||
- **THEN** "交易次数" 列填充 `result.trades`
|
||||
- **THEN** "SQN" 列填充 `result.sqn`
|
||||
|
||||
#### Scenario: 单股票回测不使用表格
|
||||
- **WHEN** 用户传入单个股票代码
|
||||
- **THEN** 系统不使用 tabulate 生成表格
|
||||
- **THEN** 系统使用详细格式输出(每个指标单独一行)
|
||||
- **THEN** 系统保持原有 `print_stats()` 的输出格式
|
||||
|
||||
#### Scenario: 表格示例输出
|
||||
- **WHEN** 用户传入 2 个股票代码
|
||||
- **THEN** 系统输出格式 SHALL 为:
|
||||
```
|
||||
+-------------+-----------+--------+------------+----------+-------+
|
||||
| 股票代码 | 收益率% | 胜率% | 最大回撤% | 交易次数 | SQN |
|
||||
+-------------+-----------+--------+------------+----------+-------+
|
||||
| 000001.SZ | 20.35 | 55.00 | -8.50 | 45 | 1.85 |
|
||||
| 600000.SH | 15.00 | 48.00 | -12.30 | 38 | 1.42 |
|
||||
+-------------+-----------+--------+------------+----------+-------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 多股票图表输出
|
||||
系统 SHALL 为每个股票生成独立的 HTML 图表文件,文件名格式为 `{code}.html`。
|
||||
|
||||
#### Scenario: 指定 --output-dir 参数
|
||||
- **WHEN** 用户传入 `--output-dir output/`
|
||||
- **THEN** 系统为每个股票生成 HTML 文件到 `output/{code}.html`
|
||||
- **THEN** 文件名 SHALL 为股票代码,如 `000001.SZ.html`, `600000.SH.html`
|
||||
- **THEN** 系统自动创建 `output/` 目录(`exist_ok=True`)
|
||||
- **THEN** 系统在完成后输出提示:"图表已保存到目录: output/" 后列出所有文件
|
||||
|
||||
#### Scenario: 未指定 --output-dir 参数
|
||||
- **WHEN** 用户未传入 `--output-dir` 参数
|
||||
- **THEN** 系统不为任何股票生成图表文件
|
||||
- **THEN** 系统仅输出控制台统计信息
|
||||
|
||||
#### Scenario: 图表文件覆盖
|
||||
- **WHEN** 系统再次执行相同的批量回测
|
||||
- **THEN** 系统覆盖已存在的 HTML 文件
|
||||
- **THEN** 系统不提示文件已存在
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 结构化回测结果
|
||||
系统 SHALL 返回标准化的 `BacktestResult` 对象,包含所有关键指标。
|
||||
|
||||
#### Scenario: BacktestResult 对象创建
|
||||
- **WHEN** 系统完成单股票回测
|
||||
- **THEN** 系统从 `stats` 对象提取指标到 `BacktestResult`
|
||||
- **THEN** `BacktestResult.code` 设置为股票代码
|
||||
- **THEN** `BacktestResult.start_date` 设置为回测开始日期
|
||||
- **THEN** `BacktestResult.end_date` 设置为回测结束日期
|
||||
- **THEN** `BacktestResult.equity_final` 设置为最终权益
|
||||
- **THEN** `BacktestResult.equity_peak` 设置为峰值收益
|
||||
- **THEN** `BacktestResult.return_pct` 设置为总收益率
|
||||
- **THEN** `BacktestResult.buy_hold_return` 设置为买入持有收益率
|
||||
- **THEN** `BacktestResult.return_annual` 设置为年化收益率
|
||||
- **THEN** `BacktestResult.volatility_annual` 设置为年化波动率
|
||||
- **THEN** `BacktestResult.max_drawdown` 设置为最大回撤
|
||||
- **THEN** `BacktestResult.avg_drawdown` 设置为平均回撤
|
||||
- **THEN** `BacktestResult.max_drawdown_duration` 设置为最大回撤持续时长
|
||||
- **THEN** `BacktestResult.avg_drawdown_duration` 设置为平均回撤持续时长
|
||||
- **THEN** `BacktestResult.sortino_ratio` 设置为索提诺比率
|
||||
- **THEN** `BacktestResult.calmar_ratio` 设置为卡尔玛比率
|
||||
- **THEN** `BacktestResult.trades` 设置为交易次数
|
||||
- **THEN** `BacktestResult.win_rate` 设置为胜率
|
||||
- **THEN** `BacktestResult.sqn` 设置为系统质量数
|
||||
- **THEN** `BacktestResult.cash` 设置为初始资金
|
||||
- **THEN** `BacktestResult.commission` 设置为手续费率
|
||||
|
||||
#### Scenario: BacktestResult 列表返回
|
||||
- **WHEN** 系统完成批量回测
|
||||
- **THEN** 系统返回 `List[BacktestResult]`
|
||||
- **THEN** 列表顺序 SHALL 与输入股票代码顺序一致
|
||||
- **THEN** 列表长度 SHALL 等于输入股票代码数量(成功时)
|
||||
|
||||
#### Scenario: BacktestResult 数据类型
|
||||
- **WHEN** 系统创建 `BacktestResult` 对象
|
||||
- **THEN** 数值字段 SHALL 为 float 类型(除 `trades`, `max_drawdown_duration` 为 int)
|
||||
- **THEN** 日期字段 SHALL 为 str 类型(YYYY-MM-DD 格式)
|
||||
- **THEN** 系统支持 `result.to_dict()` 方法(dataclass 自动生成)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 可复用回测引擎接口
|
||||
系统 SHALL 提供标准化的函数接口,供其他模块调用回测功能。
|
||||
|
||||
#### Scenario: run_backtest 函数调用
|
||||
- **WHEN** 其他模块调用 `run_backtest(code, start_date, end_date, strategy_file, cash, commission, warmup_days, output_file)`
|
||||
- **THEN** 函数接收股票代码、日期范围、策略文件、回测参数、输出文件路径
|
||||
- **THEN** 函数执行完整回测流程(数据加载、策略加载、指标计算、回测执行)
|
||||
- **THEN** 函数返回 `BacktestResult` 对象
|
||||
- **THEN** 函数不打印任何输出(纯函数)
|
||||
|
||||
#### Scenario: run_batch_backtest 函数调用
|
||||
- **WHEN** 其他模块调用 `run_batch_backtest(codes, start_date, end_date, strategy_file, cash, commission, warmup_days, output_dir)`
|
||||
- **THEN** 函数接收股票代码列表、日期范围、策略文件、回测参数、输出目录
|
||||
- **THEN** 函数串行执行每个股票的回测
|
||||
- **THEN** 函数返回 `List[BacktestResult]`
|
||||
- **THEN** 函数显示 tqdm 进度条(批量时)
|
||||
|
||||
#### Scenario: 函数参数默认值
|
||||
- **WHEN** 调用者不指定可选参数
|
||||
- **THEN** `cash` 默认为 100000
|
||||
- **THEN** `commission` 默认为 0.002
|
||||
- **THEN** `warmup_days` 默认为 365
|
||||
- **THEN** `output_file` 默认为 None(不生成图表)
|
||||
- **THEN** `output_dir` 默认为 None(不生成图表)
|
||||
|
||||
#### Scenario: 函数异常抛出
|
||||
- **WHEN** `run_backtest` 或 `run_batch_backtest` 执行时发生错误
|
||||
- **THEN** 函数 SHALL 抛出异常(不捕获)
|
||||
- **THEN** 异常类型 SHALL 为 ValueError、TypeError 或原始异常
|
||||
- **THEN** 异常信息 SHALL 包含具体错误原因
|
||||
- **THEN** 调用者负责捕获和处理异常
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 集中配置管理
|
||||
系统 SHALL 在 config.py 中集中管理数据库配置、默认回测参数、图表配色。
|
||||
|
||||
#### Scenario: 数据库配置访问
|
||||
- **WHEN** backtest_core.py 需要数据库连接参数
|
||||
- **THEN** 模块从 config 导入 `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
|
||||
- **THEN** 模块使用这些常量构建连接字符串
|
||||
- **THEN** 模块不重复定义数据库配置
|
||||
|
||||
#### Scenario: 默认参数访问
|
||||
- **WHEN** backtest_core.py 需要默认回测参数
|
||||
- **THEN** 模块从 config 导入 `DEFAULT_CASH`, `DEFAULT_COMMISSION`, `DEFAULT_WARMUP_DAYS`
|
||||
- **THEN** 模块使用这些常量作为函数默认值
|
||||
- **THEN** 模块不重复定义默认参数
|
||||
|
||||
#### Scenario: 图表配色访问
|
||||
- **WHEN** backtest_core.py 需要设置图表配色
|
||||
- **THEN** 模块从 config 导入 `BULL_COLOR`, `BEAR_COLOR`
|
||||
- **THEN** 模块使用这些颜色设置 `plotting.BULL_COLOR` 和 `plotting.BEAR_COLOR`
|
||||
- **THEN** 模块不重复定义颜色配置
|
||||
|
||||
#### Scenario: 配置文件内容
|
||||
- **WHEN** 查看 config.py 文件
|
||||
- **THEN** 文件包含数据库配置(DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD)
|
||||
- **THEN** 文件包含默认回测参数(DEFAULT_CASH, DEFAULT_COMMISSION, DEFAULT_WARMUP_DAYS)
|
||||
- **THEN** 文件包含图表配色(BULL_COLOR, BEAR_COLOR)
|
||||
- **THEN** 所有配置使用明文常量(不使用环境变量)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 错误处理策略
|
||||
系统 SHALL 在批量回测失败时立即停止执行,不继续处理后续股票。
|
||||
|
||||
#### Scenario: 数据加载失败
|
||||
- **WHEN** 系统加载第 i 个股票数据时失败(数据库错误、数据不存在)
|
||||
- **THEN** 系统捕获异常
|
||||
- **THEN** 系统输出错误信息:"回测失败 [{code}]: {error}"
|
||||
- **THEN** 系统停止执行后续股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
#### Scenario: 策略加载失败
|
||||
- **WHEN** 系统加载策略文件时失败(文件不存在、接口不完整)
|
||||
- **THEN** 系统捕获异常
|
||||
- **THEN** 系统输出错误信息:"策略加载失败: {error}"
|
||||
- **THEN** 系统停止执行所有股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
#### Scenario: 回测执行失败
|
||||
- **WHEN** 系统执行第 i 个股票回测时失败(策略逻辑错误)
|
||||
- **THEN** 系统捕获异常
|
||||
- **THEN** 系统输出错误信息和完整堆栈跟踪
|
||||
- **THEN** 系统停止执行后续股票的回测
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
#### Scenario: 图表生成失败
|
||||
- **WHEN** 系统生成第 i 个股票图表时失败
|
||||
- **THEN** 系统捕获异常
|
||||
- **THEN** 系统输出警告:"图表生成失败 [{code}]: {error},但回测已完成"
|
||||
- **THEN** 系统继续执行后续股票的回测
|
||||
- **THEN** 系统在返回的 `BacktestResult` 中设置 `error` 字段(如果设计支持)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 依赖管理
|
||||
系统 SHALL 在 pyproject.toml 中添加 tabulate 和 tqdm 依赖。
|
||||
|
||||
#### Scenario: 添加 tabulate 依赖
|
||||
- **WHEN** 查看 pyproject.toml 文件
|
||||
- **THEN** 文件包含 `tabulate` 依赖
|
||||
- **THEN** 依赖版本 SHALL 为兼容当前 Python 版本的版本
|
||||
- **THEN** 系统可以导入 `import tabulate` 无错误
|
||||
|
||||
#### Scenario: 添加 tqdm 依赖
|
||||
- **WHEN** 查看 pyproject.toml 文件
|
||||
- **THEN** 文件包含 `tqdm` 依赖
|
||||
- **THEN** 依赖版本 SHALL 为兼容当前 Python 版本的版本
|
||||
- **THEN** 系统可以导入 `from tqdm import tqdm` 无错误
|
||||
|
||||
#### Scenario: 依赖安装
|
||||
- **WHEN** 用户运行 `uv sync` 或 `pip install -e .`
|
||||
- **THEN** 系统自动安装 tabulate 和 tqdm
|
||||
- **THEN** 系统显示依赖安装进度
|
||||
- **THEN** 系统完成安装后可以正常使用回测工具
|
||||
|
||||
#### Scenario: 依赖缺失提示
|
||||
- **WHEN** 系统导入 tabulate 或 tqdm 时失败
|
||||
- **THEN** 系统输出友好错误信息:"缺少依赖: {package_name},请运行: uv add {package_name}"
|
||||
- **THEN** 系统退出并返回非零状态码
|
||||
|
||||
@@ -4,9 +4,13 @@ version = "0.1.0"
|
||||
description = "Stock analysis"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"adata>=2.9.5",
|
||||
"akshare>=1.18.20",
|
||||
"backtesting~=0.6.5",
|
||||
"duckdb>=1.4.3",
|
||||
"baostock>=0.8.9",
|
||||
"duckdb>=1.4.4",
|
||||
"jupyter~=1.1.1",
|
||||
"jupyter-bokeh>=4.0.5",
|
||||
"matplotlib~=3.10.8",
|
||||
"mplfinance>=0.12.10b0",
|
||||
"pandas~=2.3.3",
|
||||
@@ -15,4 +19,7 @@ dependencies = [
|
||||
"psycopg2-binary~=2.9.11",
|
||||
"sqlalchemy>=2.0.46",
|
||||
"ta-lib>=0.6.8",
|
||||
"tabulate>=0.9.0",
|
||||
"tqdm>=4.67.1",
|
||||
"tushare>=1.4.24",
|
||||
]
|
||||
|
||||
94
sql/initial.sql
Normal file
94
sql/initial.sql
Normal file
@@ -0,0 +1,94 @@
|
||||
CREATE TABLE stock
|
||||
(
|
||||
code varchar not null,
|
||||
name varchar not null,
|
||||
fullname varchar,
|
||||
industry varchar,
|
||||
listed_date date,
|
||||
market varchar,
|
||||
exchange varchar,
|
||||
primary key (code)
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE daily
|
||||
(
|
||||
code varchar not null,
|
||||
trade_date date not null,
|
||||
open double,
|
||||
close double,
|
||||
high double,
|
||||
low double,
|
||||
previous_close double,
|
||||
turnover double,
|
||||
volume integer,
|
||||
price_change_amount double,
|
||||
factor double,
|
||||
primary key (code, trade_date)
|
||||
);
|
||||
|
||||
CREATE TABLE finance_indicator
|
||||
(
|
||||
code varchar not null,
|
||||
year integer not null,
|
||||
accounts_payable double,
|
||||
accounts_payable_to_total_assets_ratio double,
|
||||
accounts_receivable double,
|
||||
accounts_receivable_to_total_assets_ratio double,
|
||||
accounts_receivable_turnover double,
|
||||
capital_surplus double,
|
||||
cash_and_cash_equivalents double,
|
||||
cash_and_cash_equivalents_to_total_assets_ratio double,
|
||||
cash_flow_adequacy_ratio double,
|
||||
cash_flow_from_financing_activities double,
|
||||
cash_flow_from_investing_activities double,
|
||||
cash_flow_from_operating_activities double,
|
||||
cash_flow_ratio double,
|
||||
cash_reinvestment_ratio double,
|
||||
current_assets double,
|
||||
current_assets_to_total_assets_ratio double,
|
||||
current_liabilities double,
|
||||
current_liabilities_to_total_assets_ratio double,
|
||||
current_liabilities_to_total_liabilities_ratio double,
|
||||
current_ratio double,
|
||||
days_accounts_receivable_turnover double,
|
||||
days_fixed_assets_turnover double,
|
||||
days_inventory_turnover double,
|
||||
days_total_assets_turnover double,
|
||||
earnings_per_share double,
|
||||
fixed_assets double,
|
||||
fixed_assets_to_total_assets_ratio double,
|
||||
fixed_assets_turnover double,
|
||||
goodwill double,
|
||||
goodwill_to_total_assets_ratio double,
|
||||
inventory double,
|
||||
inventory_to_total_assets_ratio double,
|
||||
inventory_turnover double,
|
||||
liabilities_to_total_assets_ratio double,
|
||||
long_term_funds_to_fixed_assets_ratio double,
|
||||
long_term_liabilities double,
|
||||
long_term_liabilities_to_total_assets_ratio double,
|
||||
long_term_liabilities_to_total_liabilities_ratio double,
|
||||
net_cash_flow_from_operating_activities double,
|
||||
net_profit double,
|
||||
net_profit_margin double,
|
||||
operating_cost double,
|
||||
operating_expenses double,
|
||||
operating_gross_profit_margin double,
|
||||
operating_profit double,
|
||||
operating_profit_margin double,
|
||||
operating_revenue double,
|
||||
operating_safety_margin_ratio double,
|
||||
quick_ratio double,
|
||||
return_on_assets double,
|
||||
return_on_equity double,
|
||||
shareholders_equity double,
|
||||
shareholders_equity_to_total_assets_ratio double,
|
||||
surplus_reserve double,
|
||||
total_assets double,
|
||||
total_assets_turnover double,
|
||||
total_liabilities double,
|
||||
total_share_capital double,
|
||||
undistributed_profit double,
|
||||
primary key (code, year)
|
||||
)
|
||||
@@ -2,28 +2,27 @@
|
||||
MACD 趋势跟踪策略
|
||||
|
||||
策略逻辑:
|
||||
- 当 MACD 线上穿信号线时 (金叉),且价格 > EMA200 时,买入
|
||||
- 当 MACD 线下穿信号线时 (死叉),或价格 < EMA200 时,卖出
|
||||
- 当 MACD 线上穿信号线时 (金叉),且价格 > EMA 时,买入
|
||||
- 当 MACD 线下穿信号线时 (死叉),或价格 < EMA 时,卖出
|
||||
|
||||
指标计算:
|
||||
- MACD(10, 20, 9): 快线 10 日,慢线 20 日,信号线 9 日
|
||||
- EMA200: 200 日指数移动平均线(趋势确认)
|
||||
- EMA: 200 日指数移动平均线(趋势确认)
|
||||
|
||||
参数选择理由:
|
||||
- 快线 10: 比标准 12 更敏感,适应 A 股较高波动性
|
||||
- 慢线 20: 比标准 26 更快响应,同时保持趋势跟踪稳定性
|
||||
- 信号线 9: 保持标准,避免信号过于频繁
|
||||
- EMA200: 被广泛认可为牛熊分界线,避免逆势交易
|
||||
- EMA: 被广泛认可为牛熊分界线,避免逆势交易
|
||||
|
||||
趋势过滤:
|
||||
- EMA200 上方: 确认为上升趋势,允许开多仓
|
||||
- EMA200 下方: 确认为下降趋势,不开多仓,强制平仓
|
||||
- EMA 上方: 确认为上升趋势,允许开多仓
|
||||
- EMA 下方: 确认为下降趋势,不开多仓,强制平仓
|
||||
|
||||
Author: Sisyphus
|
||||
Date: 2025-01-27
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from backtesting import Strategy
|
||||
from backtesting.lib import crossover
|
||||
|
||||
@@ -32,32 +31,30 @@ def calculate_indicators(data):
|
||||
"""
|
||||
计算策略所需的技术指标
|
||||
|
||||
使用 ta-lib 库计算 MACD 和 EMA200 指标
|
||||
使用 ta-lib 库计算 MACD 和 EMA 指标
|
||||
|
||||
参数:
|
||||
data: DataFrame, 包含 [Open, High, Low, Close, Volume, factor]
|
||||
|
||||
返回:
|
||||
DataFrame, 添加了指标列:
|
||||
- MACD_10_20_9: MACD 线 (DIF)
|
||||
- MACDs_10_20_9: MACD 信号线 (DEA)
|
||||
- MACDh_10_20_9: MACD 柱状图 (Histogram)
|
||||
- EMA_200: 200 日指数移动平均线
|
||||
- macd: MACD 线 (macd)
|
||||
- signal: MACD 信号线 (DEA)
|
||||
- hist: MACD 柱状图 (Histogram)
|
||||
- ema: 日指数移动平均线
|
||||
"""
|
||||
data = data.copy()
|
||||
|
||||
# 计算 MACD 指标 (10, 20, 9)
|
||||
# talib.MACD 返回三个值: (macd, macdsignal, macdhist)
|
||||
macd, macdsignal, macdhist = talib.MACD(
|
||||
data["Close"], fastperiod=10, slowperiod=20, signalperiod=9
|
||||
)
|
||||
macd, macdsignal, macdhist = talib.MACD(data["Close"], fastperiod=10, slowperiod=20, signalperiod=9)
|
||||
|
||||
data["MACD_10_20_9"] = macd
|
||||
data["MACDs_10_20_9"] = macdsignal
|
||||
data["MACDh_10_20_9"] = macdhist
|
||||
data["macd"] = macd
|
||||
data["signal"] = macdsignal
|
||||
data["hist"] = macdhist
|
||||
|
||||
# 计算 EMA200 趋势线
|
||||
data["EMA_200"] = talib.EMA(data["Close"], timeperiod=200)
|
||||
# 计算 EMA 趋势线
|
||||
data["ema"] = talib.SMA(data["Close"], timeperiod=120)
|
||||
|
||||
return data
|
||||
|
||||
@@ -76,7 +73,7 @@ class MacdTrendStrategy(Strategy):
|
||||
"""
|
||||
MACD 趋势跟踪策略
|
||||
|
||||
结合 MACD 金叉/死叉信号和 EMA200 趋势过滤
|
||||
结合 MACD 金叉/死叉信号和 EMA 趋势过滤
|
||||
|
||||
参数:
|
||||
fast_period: MACD 快线周期 (默认: 10)
|
||||
@@ -95,13 +92,13 @@ class MacdTrendStrategy(Strategy):
|
||||
注册指标到 backtesting 框架
|
||||
"""
|
||||
# 注册 MACD 线
|
||||
self.macd = self.I(lambda x: x, self.data.MACD_10_20_9)
|
||||
self.macd = self.I(lambda x: x, self.data.macd)
|
||||
|
||||
# 注册 MACD 信号线
|
||||
self.macd_signal = self.I(lambda x: x, self.data.MACDs_10_20_9)
|
||||
self.signal = self.I(lambda x: x, self.data.signal)
|
||||
|
||||
# 注册 EMA200 趋势线
|
||||
self.ema200 = self.I(lambda x: x, self.data.EMA_200)
|
||||
# 注册 EMA 趋势线
|
||||
self.ema = self.I(lambda x: x, self.data.ema)
|
||||
|
||||
def next(self):
|
||||
"""
|
||||
@@ -109,25 +106,18 @@ class MacdTrendStrategy(Strategy):
|
||||
|
||||
买入条件:
|
||||
- MACD 金叉 (MACD 线上穿信号线)
|
||||
- 价格 > EMA200 (确认上升趋势)
|
||||
- 价格 > EMA (确认上升趋势)
|
||||
|
||||
卖出条件:
|
||||
- MACD 死叉 (MACD 线下穿信号线)
|
||||
- 或价格 < EMA200 (趋势转向,强制平仓)
|
||||
- 或价格 < EMA (趋势转向,强制平仓)
|
||||
"""
|
||||
# 买入条件: MACD 金叉 AND 价格 > EMA200
|
||||
if (
|
||||
crossover(self.macd, self.macd_signal)
|
||||
and self.data.Close[-1] > self.ema200[-1]
|
||||
):
|
||||
self.position.close() # 先平掉现有仓位
|
||||
# 买入条件: MACD 金叉 AND 价格 > EMA
|
||||
if crossover(self.macd, self.signal) and self.data.Close[-1] > self.ema[-1]:
|
||||
self.buy() # 开多仓
|
||||
|
||||
# 卖出条件: MACD 死叉 OR 价格 < EMA200
|
||||
elif (
|
||||
crossover(self.macd_signal, self.macd)
|
||||
or self.data.Close[-1] < self.ema200[-1]
|
||||
):
|
||||
# 卖出条件: MACD 死叉 OR 价格 < EMA
|
||||
elif self.position.size > 0 and (crossover(self.signal, self.macd) or self.data.Close[-1] < self.ema[-1]):
|
||||
self.position.close() # 平掉多仓
|
||||
|
||||
|
||||
|
||||
@@ -5,14 +5,13 @@ SMA 双均线交叉策略
|
||||
- 当短期均线上穿长期均线时 (金叉),买入
|
||||
- 当短期均线下穿长期均线时 (死叉),卖出
|
||||
|
||||
指标计算:
|
||||
指标计算 (使用 ta-lib):
|
||||
- SMA10: 10 日简单移动平均线
|
||||
- SMA30: 30 日简单移动平均线
|
||||
- SMA60: 60 日简单移动平均线
|
||||
- SMA120: 120 日简单移动平均线
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from backtesting import Strategy
|
||||
from backtesting.lib import crossover
|
||||
|
||||
@@ -21,19 +20,25 @@ def calculate_indicators(data):
|
||||
"""
|
||||
计算策略所需的技术指标
|
||||
|
||||
使用 ta-lib 库计算 SMA 指标
|
||||
|
||||
参数:
|
||||
data: DataFrame, 包含 [Open, High, Low, Close, Volume, factor]
|
||||
|
||||
返回:
|
||||
DataFrame, 添加了指标列
|
||||
DataFrame, 添加了指标列:
|
||||
- sma10: 10 日简单移动平均线
|
||||
- sma30: 30 日简单移动平均线
|
||||
- sma60: 60 日简单移动平均线
|
||||
- sma120: 120 日简单移动平均线
|
||||
"""
|
||||
data = data.copy()
|
||||
|
||||
# 计算不同周期的移动平均线
|
||||
data["sma10"] = data["Close"].rolling(window=10).mean()
|
||||
data["sma30"] = data["Close"].rolling(window=30).mean()
|
||||
data["sma60"] = data["Close"].rolling(window=60).mean()
|
||||
data["sma120"] = data["Close"].rolling(window=120).mean()
|
||||
data["sma10"] = talib.SMA(data["Close"], timeperiod=10)
|
||||
data["sma30"] = talib.SMA(data["Close"], timeperiod=30)
|
||||
data["sma60"] = talib.SMA(data["Close"], timeperiod=60)
|
||||
data["sma120"] = talib.SMA(data["Close"], timeperiod=120)
|
||||
|
||||
return data
|
||||
|
||||
@@ -78,10 +83,12 @@ class SmaCross(Strategy):
|
||||
"""
|
||||
# 金叉:短期均线上穿长期均线
|
||||
if crossover(self.data.sma10, self.data.sma30):
|
||||
self.position.close() # 先平掉现有仓位
|
||||
self.buy() # 开多仓
|
||||
|
||||
# 死叉:短期均线下穿长期均线
|
||||
elif crossover(self.data.sma30, self.data.sma10):
|
||||
self.position.close() # 先平掉现有仓位
|
||||
self.sell() # 开空仓
|
||||
elif self.position.size > 0 and crossover(self.data.sma30, self.data.sma10):
|
||||
self.position.close() # 开空仓
|
||||
|
||||
|
||||
# 导入 talib (必须在文件末尾,因为 calculate_indicators 函数中使用了 talib)
|
||||
import talib
|
||||
|
||||
309
uv.lock
generated
309
uv.lock
generated
@@ -2,6 +2,57 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "adata"
|
||||
version = "2.9.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "pandas" },
|
||||
{ name = "py-mini-racer" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/da/1eb2f05b14e4d41edcc017b9d6b428f30712d0d046f1b85cd54201b423a5/adata-2.9.5.tar.gz", hash = "sha256:b398fd885ee31baca41b8a141c586d3430ef0fec633f6088a830429437210cf6", size = 188823, upload-time = "2025-12-26T11:09:29.759Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/9f/51a65fb438febc0ab38f493a837c5aeb9135dfee2e2c1224920038bbc686/adata-2.9.5-py3-none-any.whl", hash = "sha256:f9dc5d276f8771cf5a5f11fb81c6d97a00d188e20cfcef67022f210c8b23cbf1", size = 229158, upload-time = "2025-12-26T11:09:23.007Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "akracer"
|
||||
version = "0.0.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/c6/f38feed5b961d73e1b4cb049fdb45338356e0f5b828b230c00d0e51f3137/akracer-0.0.14.tar.gz", hash = "sha256:e084c14bf6d9a02d5da375e3af1cba3d46f103aa1cf3a2010593b3e95bf1c29a", size = 10047643, upload-time = "2025-09-10T13:47:34.811Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/cb/1041355b14cd4b76ac082e8c676858f6eddb78f0ba37c59284adf36e5103/akracer-0.0.14-py3-none-any.whl", hash = "sha256:629eaccd0e1d18366804b797eb2692ed47bed0028f55b5a5af3cc277d521df04", size = 10076442, upload-time = "2025-09-10T13:47:29.061Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "akshare"
|
||||
version = "1.18.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "akracer", marker = "sys_platform == 'linux'" },
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "curl-cffi" },
|
||||
{ name = "decorator" },
|
||||
{ name = "html5lib" },
|
||||
{ name = "jsonpath" },
|
||||
{ name = "lxml" },
|
||||
{ name = "mini-racer", marker = "sys_platform != 'linux'" },
|
||||
{ name = "openpyxl" },
|
||||
{ name = "pandas" },
|
||||
{ name = "py-mini-racer", marker = "sys_platform == 'linux'" },
|
||||
{ name = "requests" },
|
||||
{ name = "tabulate" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "urllib3" },
|
||||
{ name = "xlrd" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/e0/48c0d7fc2527787b3179960454037dbe5b8d3409aa00eab23748a34317be/akshare-1.18.20.tar.gz", hash = "sha256:f3797d454fd2bc9e75f85e24abdd2af2c29989d4f89379b3385998bbf1464d16", size = 855384, upload-time = "2026-01-27T14:35:25.261Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/b4/2743787e5366eb281b966f8c3fcc85d6e3a8456cefbac27d30ca7baafedd/akshare-1.18.20-py3-none-any.whl", hash = "sha256:9ba6cb3a17ee4cf957cf81e01cec59d55962a3fd867ab669d151a213bb5a9fc3", size = 1074968, upload-time = "2026-01-27T14:35:23.937Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.12.1"
|
||||
@@ -129,6 +180,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/b6/cf57538b968c5caa60ee626ec8be1c31e420067d2a4cf710d81605356f8c/backtesting-0.6.5-py3-none-any.whl", hash = "sha256:8ac2fa500c8fd83dc783b72957b600653a72687986fe3ca86d6ef6c8b8d74363", size = 192105, upload-time = "2025-07-30T05:57:03.322Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "baostock"
|
||||
version = "0.8.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pandas" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/d5/0fb2c61f392f89b1655490acb17a02861f1f1c38e973c9fc6aa049e54401/baostock-0.8.9.tar.gz", hash = "sha256:8169cdbed14fa442ace63c59549bef3f92b0c3dd1df9e5d9069f7bd04a76b0da", size = 21876, upload-time = "2024-05-31T02:56:54.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/37/bbabac2d33723d71bd8dbd5e819d9cbe5dc1e031b7dd12ed7de8fa040816/baostock-0.8.9-py3-none-any.whl", hash = "sha256:7a51fb30cd6b4325f5517198e350dc2fffaaab2923cd132b9f747b8b73ae7303", size = 45923, upload-time = "2024-05-31T02:56:53.161Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.14.3"
|
||||
@@ -180,6 +243,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/a8/877f306720bc114c612579c5af36bcb359026b83d051226945499b306b1a/bokeh-3.8.2-py3-none-any.whl", hash = "sha256:5e2c0d84f75acb25d60efb9e4d2f434a791c4639b47d685534194c4e07bd0111", size = 7207131, upload-time = "2026-01-06T00:20:04.917Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bs4"
|
||||
version = "0.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698, upload-time = "2024-01-17T18:15:47.371Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189, upload-time = "2024-01-17T18:15:48.613Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build"
|
||||
version = "1.4.0"
|
||||
@@ -312,6 +387,29 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curl-cffi"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/c9/0067d9a25ed4592b022d4558157fcdb6e123516083700786d38091688767/curl_cffi-0.14.0.tar.gz", hash = "sha256:5ffbc82e59f05008ec08ea432f0e535418823cda44178ee518906a54f27a5f0f", size = 162633, upload-time = "2025-12-16T03:25:07.931Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/f0/0f21e9688eaac85e705537b3a87a5588d0cefb2f09d83e83e0e8be93aa99/curl_cffi-0.14.0-cp39-abi3-macosx_14_0_arm64.whl", hash = "sha256:e35e89c6a69872f9749d6d5fda642ed4fc159619329e99d577d0104c9aad5893", size = 3087277, upload-time = "2025-12-16T03:24:49.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/a3/0419bd48fce5b145cb6a2344c6ac17efa588f5b0061f212c88e0723da026/curl_cffi-0.14.0-cp39-abi3-macosx_15_0_x86_64.whl", hash = "sha256:5945478cd28ad7dfb5c54473bcfb6743ee1d66554d57951fdf8fc0e7d8cf4e45", size = 5804650, upload-time = "2025-12-16T03:24:51.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/07/a238dd062b7841b8caa2fa8a359eb997147ff3161288f0dd46654d898b4d/curl_cffi-0.14.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c42e8fa3c667db9ccd2e696ee47adcd3cd5b0838d7282f3fc45f6c0ef3cfdfa7", size = 8231918, upload-time = "2025-12-16T03:24:52.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/d2/ce907c9b37b5caf76ac08db40cc4ce3d9f94c5500db68a195af3513eacbc/curl_cffi-0.14.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:060fe2c99c41d3cb7f894de318ddf4b0301b08dca70453d769bd4e74b36b8483", size = 8654624, upload-time = "2025-12-16T03:24:54.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/ae/6256995b18c75e6ef76b30753a5109e786813aa79088b27c8eabb1ef85c9/curl_cffi-0.14.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b158c41a25388690dd0d40b5bc38d1e0f512135f17fdb8029868cbc1993d2e5b", size = 8010654, upload-time = "2025-12-16T03:24:56.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/10/ff64249e516b103cb762e0a9dca3ee0f04cf25e2a1d5d9838e0f1273d071/curl_cffi-0.14.0-cp39-abi3-manylinux_2_28_i686.whl", hash = "sha256:1439fbef3500fb723333c826adf0efb0e2e5065a703fb5eccce637a2250db34a", size = 7781969, upload-time = "2025-12-16T03:24:57.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/76/d6f7bb76c2d12811aa7ff16f5e17b678abdd1b357b9a8ac56310ceccabd5/curl_cffi-0.14.0-cp39-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7176f2c2d22b542e3cf261072a81deb018cfa7688930f95dddef215caddb469", size = 7969133, upload-time = "2025-12-16T03:24:59.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/7c/cca39c0ed4e1772613d3cba13091c0e9d3b89365e84b9bf9838259a3cd8f/curl_cffi-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:03f21ade2d72978c2bb8670e9b6de5260e2755092b02d94b70b906813662998d", size = 9080167, upload-time = "2025-12-16T03:25:00.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/03/a942d7119d3e8911094d157598ae0169b1c6ca1bd3f27d7991b279bcc45b/curl_cffi-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:58ebf02de64ee5c95613209ddacb014c2d2f86298d7080c0a1c12ed876ee0690", size = 9520464, upload-time = "2025-12-16T03:25:02.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/77/78900e9b0833066d2274bda75cba426fdb4cef7fbf6a4f6a6ca447607bec/curl_cffi-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:6e503f9a103f6ae7acfb3890c843b53ec030785a22ae7682a22cc43afb94123e", size = 1677416, upload-time = "2025-12-16T03:25:04.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/7c/d2ba86b0b3e1e2830bd94163d047de122c69a8df03c5c7c36326c456ad82/curl_cffi-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:2eed50a969201605c863c4c31269dfc3e0da52916086ac54553cfa353022425c", size = 1425067, upload-time = "2025-12-16T03:25:06.454Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cycler"
|
||||
version = "0.12.1"
|
||||
@@ -354,17 +452,26 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "duckdb"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7f/da/17c3eb5458af69d54dedc8d18e4a32ceaa8ce4d4c699d45d6d8287e790c3/duckdb-1.4.3.tar.gz", hash = "sha256:fea43e03604c713e25a25211ada87d30cd2a044d8f27afab5deba26ac49e5268", size = 18478418, upload-time = "2025-12-09T10:59:22.945Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/9d/ab66a06e416d71b7bdcb9904cdf8d4db3379ef632bb8e9495646702d9718/duckdb-1.4.4.tar.gz", hash = "sha256:8bba52fd2acb67668a4615ee17ee51814124223de836d9e2fdcbc4c9021b3d3c", size = 18419763, upload-time = "2026-01-26T11:50:37.68Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f4/a38651e478fa41eeb8e43a0a9c0d4cd8633adea856e3ac5ac95124b0fdbf/duckdb-1.4.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:316711a9e852bcfe1ed6241a5f654983f67e909e290495f3562cccdf43be8180", size = 29042272, upload-time = "2025-12-09T10:58:51.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/de/2cf171a66098ce5aeeb7371511bd2b3d7b73a2090603b0b9df39f8aaf814/duckdb-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9e625b2b4d52bafa1fd0ebdb0990c3961dac8bb00e30d327185de95b68202131", size = 15419343, upload-time = "2025-12-09T10:58:54.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/28/6b0a7830828d4e9a37420d87e80fe6171d2869a9d3d960bf5d7c3b8c7ee4/duckdb-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:130c6760f6c573f9c9fe9aba56adba0fab48811a4871b7b8fd667318b4a3e8da", size = 13748905, upload-time = "2025-12-09T10:58:56.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/4d/778628e194d63967870873b9581c8a6b4626974aa4fbe09f32708a2d3d3a/duckdb-1.4.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c88effaa557a11267706b01419c542fe42f893dee66e5a6daa5974ea2d4a46", size = 18487261, upload-time = "2025-12-09T10:58:58.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/5f/87e43af2e4a0135f9675449563e7c2f9b6f1fe6a2d1691c96b091f3904dd/duckdb-1.4.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b35491db98ccd11d151165497c084a9d29d3dc42fc80abea2715a6c861ca43d", size = 20497138, upload-time = "2025-12-09T10:59:01.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/41/abec537cc7c519121a2a83b9a6f180af8915fabb433777dc147744513e74/duckdb-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:23b12854032c1a58d0452e2b212afa908d4ce64171862f3792ba9a596ba7c765", size = 12836056, upload-time = "2025-12-09T10:59:03.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5a/8af5b96ce5622b6168854f479ce846cf7fb589813dcc7d8724233c37ded3/duckdb-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:90f241f25cffe7241bf9f376754a5845c74775e00e1c5731119dc88cd71e0cb2", size = 13527759, upload-time = "2025-12-09T10:59:05.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/a6/f19e2864e651b0bd8e4db2b0c455e7e0d71e0d4cd2cd9cc052f518e43eb3/duckdb-1.4.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:25874f8b1355e96178079e37312c3ba6d61a2354f51319dae860cf21335c3a20", size = 28909554, upload-time = "2026-01-26T11:50:00.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/93/8a24e932c67414fd2c45bed83218e62b73348996bf859eda020c224774b2/duckdb-1.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:452c5b5d6c349dc5d1154eb2062ee547296fcbd0c20e9df1ed00b5e1809089da", size = 15353804, upload-time = "2026-01-26T11:50:03.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/13/e5378ff5bb1d4397655d840b34b642b1b23cdd82ae19599e62dc4b9461c9/duckdb-1.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8e5c2d8a0452df55e092959c0bfc8ab8897ac3ea0f754cb3b0ab3e165cd79aff", size = 13676157, upload-time = "2026-01-26T11:50:06.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/94/24364da564b27aeebe44481f15bd0197a0b535ec93f188a6b1b98c22f082/duckdb-1.4.4-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af6e76fe8bd24875dc56dd8e38300d64dc708cd2e772f67b9fbc635cc3066a3", size = 18426882, upload-time = "2026-01-26T11:50:08.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/0a/6ae31b2914b4dc34243279b2301554bcbc5f1a09ccc82600486c49ab71d1/duckdb-1.4.4-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0440f59e0cd9936a9ebfcf7a13312eda480c79214ffed3878d75947fc3b7d6d", size = 20435641, upload-time = "2026-01-26T11:50:12.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b1/fd5c37c53d45efe979f67e9bd49aaceef640147bb18f0699a19edd1874d6/duckdb-1.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:59c8d76016dde854beab844935b1ec31de358d4053e792988108e995b18c08e7", size = 12762360, upload-time = "2026-01-26T11:50:14.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/2d/13e6024e613679d8a489dd922f199ef4b1d08a456a58eadd96dc2f05171f/duckdb-1.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:53cd6423136ab44383ec9955aefe7599b3fb3dd1fe006161e6396d8167e0e0d4", size = 13458633, upload-time = "2026-01-26T11:50:17.657Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "et-xmlfile"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -451,6 +558,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5lib"
|
||||
version = "1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
{ name = "webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215, upload-time = "2020-06-22T23:32:38.834Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173, upload-time = "2020-06-22T23:32:36.781Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
@@ -606,6 +726,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonpath"
|
||||
version = "0.82.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/a1/693351acd0a9edca4de9153372a65e75398898ea7f8a5c722ab00f464929/jsonpath-0.82.2.tar.gz", hash = "sha256:d87ef2bcbcded68ee96bc34c1809b69457ecec9b0c4dd471658a12bd391002d1", size = 10353, upload-time = "2023-08-24T18:57:55.459Z" }
|
||||
|
||||
[[package]]
|
||||
name = "jsonpointer"
|
||||
version = "3.0.0"
|
||||
@@ -672,6 +798,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-bokeh"
|
||||
version = "4.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "bokeh" },
|
||||
{ name = "ipywidgets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/fd/8f0213c704bf36b5f523ae5bf7dc367f3687e75dcc2354084b75c05d2b53/jupyter_bokeh-4.0.5.tar.gz", hash = "sha256:a33d6ab85588f13640b30765fa15d1111b055cbe44f67a65ca57d3593af8245d", size = 149140, upload-time = "2024-06-03T06:33:33.488Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/78/33b2294aad62e5f95b89a89379c5995c2bd978018387ef8bec79f6dc272c/jupyter_bokeh-4.0.5-py3-none-any.whl", hash = "sha256:1110076c14c779071cf492646a1a871aefa8a477261e4721327a666e65df1a2c", size = 148593, upload-time = "2024-06-03T06:33:35.82Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-client"
|
||||
version = "8.8.0"
|
||||
@@ -901,9 +1040,13 @@ name = "leopard-analysis"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "adata" },
|
||||
{ name = "akshare" },
|
||||
{ name = "backtesting" },
|
||||
{ name = "baostock" },
|
||||
{ name = "duckdb" },
|
||||
{ name = "jupyter" },
|
||||
{ name = "jupyter-bokeh" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "mplfinance" },
|
||||
{ name = "pandas" },
|
||||
@@ -912,13 +1055,20 @@ dependencies = [
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "ta-lib" },
|
||||
{ name = "tabulate" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "tushare" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "adata", specifier = ">=2.9.5" },
|
||||
{ name = "akshare", specifier = ">=1.18.20" },
|
||||
{ name = "backtesting", specifier = "~=0.6.5" },
|
||||
{ name = "duckdb", specifier = ">=1.4.3" },
|
||||
{ name = "baostock", specifier = ">=0.8.9" },
|
||||
{ name = "duckdb", specifier = ">=1.4.4" },
|
||||
{ name = "jupyter", specifier = "~=1.1.1" },
|
||||
{ name = "jupyter-bokeh", specifier = ">=4.0.5" },
|
||||
{ name = "matplotlib", specifier = "~=3.10.8" },
|
||||
{ name = "mplfinance", specifier = ">=0.12.10b0" },
|
||||
{ name = "pandas", specifier = "~=2.3.3" },
|
||||
@@ -927,6 +1077,53 @@ requires-dist = [
|
||||
{ name = "psycopg2-binary", specifier = "~=2.9.11" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.46" },
|
||||
{ name = "ta-lib", specifier = ">=0.6.8" },
|
||||
{ name = "tabulate", specifier = ">=0.9.0" },
|
||||
{ name = "tqdm", specifier = ">=4.67.1" },
|
||||
{ name = "tushare", specifier = ">=1.4.24" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1004,6 +1201,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mini-racer"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/7b/2f417069fb8fcb85c1458e51ea83c12d37f892a41544ef28479e37a315a3/mini_racer-0.14.0.tar.gz", hash = "sha256:7f812d6f21a8828e99e986bf4bb184c04bd906c845061aa43d7dd3edc8b8e6f5", size = 41238, upload-time = "2026-01-05T07:28:50.336Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b5/d184a34787edae8301ec5bd1a454c9bfdce2c58fb3c887f8d12416589057/mini_racer-0.14.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:b02b3e15c548958a75afec12b9c21afa01c4a3aacbea66f5856036ff9b6c1a36", size = 19847149, upload-time = "2026-01-05T07:28:24.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/09/f7afb45b4e54ccacc88fb543d7d87040904c7bbcbeed3f944959189f93c1/mini_racer-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:049a239a1174d40e2a38da71b55aa0ad73a1a7be90956d4ab9ddf9a1dcfa8178", size = 18396834, upload-time = "2026-01-05T07:28:27.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/c5/305d16ea858e9be168e00b2cd5d4e7b74524d9c4b1349b1267386c25964e/mini_racer-0.14.0-py3-none-win_amd64.whl", hash = "sha256:7e4cd3fef3df603c0d1feea6e258cf02c6c09e8619d43d4ff0f0a8595cf96715", size = 15474619, upload-time = "2026-01-05T07:28:45.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/27/e313b5ff8f6583253e5f9fee64ab88476a570c7307554acb0e2899668a97/mini_racer-0.14.0-py3-none-win_arm64.whl", hash = "sha256:2cb21a959c7045c46d727db015e614903217f3648d24fcdbde6de3b4bd17a498", size = 14795219, upload-time = "2026-01-05T07:28:48.25Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mistune"
|
||||
version = "3.2.0"
|
||||
@@ -1156,6 +1365,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openpyxl"
|
||||
version = "3.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "et-xmlfile" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
@@ -1366,6 +1587,17 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-mini-racer"
|
||||
version = "0.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/97/a578b918b2e5923dd754cb60bb8b8aeffc85255ffb92566e3c65b148ff72/py_mini_racer-0.6.0.tar.gz", hash = "sha256:f71e36b643d947ba698c57cd9bd2232c83ca997b0802fc2f7f79582377040c11", size = 5994836, upload-time = "2021-04-22T07:58:35.993Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/13/058240c7fd1fbf29a24bda048d93346c2a56275736b76b56afe64050a161/py_mini_racer-0.6.0-py2.py3-none-macosx_10_10_x86_64.whl", hash = "sha256:346e73bb89a2024888244d487834be24a121089ceb0641dd0200cb96c4e24b57", size = 5280865, upload-time = "2021-04-22T07:58:29.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a9/8ce0ca222ef04d602924a1e099be93f5435ca6f3294182a30574d4159ca2/py_mini_racer-0.6.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:42896c24968481dd953eeeb11de331f6870917811961c9b26ba09071e07180e2", size = 5416149, upload-time = "2021-04-22T07:58:25.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/71/76ac5d593e14b148a4847b608c5ad9a2c7c4827c796c33b396d0437fa113/py_mini_racer-0.6.0-py2.py3-none-win_amd64.whl", hash = "sha256:97cab31bbf63ce462ba4cd6e978c572c916d8b15586156c7c5e0b2e42c10baab", size = 4797809, upload-time = "2021-04-22T07:58:32.286Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.23"
|
||||
@@ -1615,6 +1847,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simplejson"
|
||||
version = "3.20.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
@@ -1692,6 +1933,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/4c/d341020377f8b183405bdf3c5717fc2ca04a8d33b5c59b2348377ee459d9/ta_lib-0.6.8-cp314-cp314-win_arm64.whl", hash = "sha256:bfad1202fb1f9140e3810cc607058395f59032d9128cc0d716900c78bea5f337", size = 755896, upload-time = "2025-10-20T20:49:39.9Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tabulate"
|
||||
version = "0.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminado"
|
||||
version = "0.18.1"
|
||||
@@ -1737,6 +1987,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "traitlets"
|
||||
version = "5.14.3"
|
||||
@@ -1746,6 +2008,24 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tushare"
|
||||
version = "1.4.24"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "bs4" },
|
||||
{ name = "lxml" },
|
||||
{ name = "pandas" },
|
||||
{ name = "requests" },
|
||||
{ name = "simplejson" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "websocket-client" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/09/2141aaccb90a8249edb42d6b31330606d8cf9345237773775a3aa4c71986/tushare-1.4.24.tar.gz", hash = "sha256:786acbf6ee7dfb0b152bdd570b673f74e58b86a0d9908a221c6bdc4254a4e0ea", size = 128539, upload-time = "2025-08-25T02:02:05.451Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/75/63810958023595b460f2a5ef6baf5a60ffd8166e5fc06a3c2f22e9ca7b34/tushare-1.4.24-py3-none-any.whl", hash = "sha256:778e3128262747cb0cdadac2e5a5e6cd1a520c239b4ffbde2776652424451b08", size = 143587, upload-time = "2025-08-25T02:02:03.554Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-pytz"
|
||||
version = "2025.2.0.20251108"
|
||||
@@ -1836,6 +2116,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xlrd"
|
||||
version = "2.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/5a/377161c2d3538d1990d7af382c79f3b2372e880b65de21b01b1a2b78691e/xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9", size = 100167, upload-time = "2025-06-14T08:46:39.039Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/62/c8d562e7766786ba6587d09c5a8ba9f718ed3fa8af7f4553e8f91c36f302/xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9", size = 96555, upload-time = "2025-06-14T08:46:37.766Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xyzservices"
|
||||
version = "2025.11.0"
|
||||
|
||||
Reference in New Issue
Block a user