diff --git a/README.md b/README.md index 4a203ec..5287ca1 100644 --- a/README.md +++ b/README.md @@ -295,8 +295,13 @@ easy-tdx backtest SZ 300308 --strategy-file strategies/expma_cross.py --count 20 ```bash python -X utf8 run_all_strategies.py SZ 300308 --count 2000 --cash 1000000 --adjust QFQ + +# 加 --show 自动弹出最佳策略的资金曲线 vs 股价对比图 +python -X utf8 run_all_strategies.py SZ 300308 --count 2000 --cash 1000000 --adjust QFQ --show ``` +`--show` 会用 matplotlib 弹出一个双轴对比窗口:左轴蓝色线是归一化股价,右轴红色线是最佳策略的资金曲线,绿三角=买入、黄三角=卖出,标题显示股票名称和关键绩效指标。需要 `pip install matplotlib`。 + 输出示例(以 SZ 300308 为例): ``` @@ -345,6 +350,25 @@ python -X utf8 run_all_strategies.py SZ 300308 --count 2000 --cash 1000000 --adj python -X utf8 run_all_strategies.py SH 600519 --count 2000 --cash 1000000 --adjust QFQ ``` +#### `--show` 可视化效果 + +

+
+ SH601088 中国神华 — bollinger_breakout 策略 | 收益 1281.8% +

+ +

+
+ SH600522 中天科技 — kdj_golden 策略 | 收益 568.6% +

+ +

+
+ SH601179 中国西电 — expma_cross 策略 | 收益 168.0% +

+ +> **⚠️ Demo 展示,不作为操作依据。** 历史回测收益不代表未来表现,策略参数未经过样本外验证。 + #### 自带策略示例 `strategies/` 目录下有 9 个开箱即用的策略文件,可直接用于 `--strategy-file`: diff --git a/run_all_strategies.py b/run_all_strategies.py index 5b8d08e..2037cdc 100644 --- a/run_all_strategies.py +++ b/run_all_strategies.py @@ -9,7 +9,6 @@ from __future__ import annotations -import json import sys import time from pathlib import Path @@ -21,6 +20,128 @@ sys.path.insert(0, str(Path(__file__).parent / "src")) import click +def _setup_chinese_font() -> None: + """配置 matplotlib 中文字体,按平台自动选择。""" + import platform + + import matplotlib + + system = platform.system() + if system == "Windows": + candidates = ["Microsoft YaHei", "SimHei", "KaiTi", "FangSong"] + elif system == "Darwin": + candidates = ["PingFang SC", "Heiti SC", "STHeiti"] + else: + candidates = ["WenQuanYi Micro Hei", "Noto Sans CJK SC", "Droid Sans Fallback"] + + import matplotlib.font_manager as fm + + available = {f.name for f in fm.fontManager.ttflist} + for font in candidates: + if font in available: + matplotlib.rcParams["font.sans-serif"] = [font, "DejaVu Sans"] + break + matplotlib.rcParams["axes.unicode_minus"] = False + + +def _map_trade_values(trades_df: Any, equity: Any, initial_cash: float) -> list[float]: + """将交易的 datetime 映射到 equity_curve 对应的归一化值。""" + eq_dt = equity["datetime"].values + eq_norm = equity["total"].values / initial_cash + result_vals: list[float] = [] + for dt in trades_df["datetime"].values: + idx = eq_dt.searchsorted(dt, side="right") - 1 + if idx < 0: + idx = 0 + if idx >= len(eq_norm): + idx = len(eq_norm) - 1 + result_vals.append(float(eq_norm[idx])) + return result_vals + + +def _show_best_chart( + df: Any, + result: Any, + strategy_name: str, + stock_label: str, + stock_name: str, + initial_cash: float, +) -> None: + """展示最佳策略资金曲线与股价归一化对比图。""" + try: + import matplotlib.pyplot as plt + except ImportError: + click.echo("[!] 需要 matplotlib 才能展示图表: pip install matplotlib") + return + + _setup_chinese_font() + + equity = result.equity_curve + if equity.empty: + click.echo("[!] 最佳策略无资金曲线数据,跳过绘图") + return + + fig, ax1 = plt.subplots(figsize=(14, 7)) + + # 归一化股价(以第一天收盘价为基准) + close_prices = df["close"].values + norm_price = close_prices / close_prices[0] + dates = df["datetime"] if "datetime" in df.columns else df.index + ax1.plot(dates, norm_price, color="steelblue", linewidth=1.2, label="股价 (归一化)") + ax1.set_ylabel("股价归一化", color="steelblue", fontsize=11) + ax1.tick_params(axis="y", labelcolor="steelblue") + + # 归一化资金曲线(以初始资金为基准) + eq_dates = equity["datetime"] + eq_values = equity["total"].values / initial_cash + ax2 = ax1.twinx() + ax2.plot(eq_dates, eq_values, color="crimson", linewidth=1.5, + label=f"策略: {strategy_name}") + ax2.set_ylabel("资金曲线 (归一化)", color="crimson", fontsize=11) + ax2.tick_params(axis="y", labelcolor="crimson") + + # 标记买卖点 + trades = result.trades + if not trades.empty: + buy_trades = trades[trades["direction"] == "BUY"] + sell_trades = trades[trades["direction"] == "SELL"] + if not buy_trades.empty: + ax2.scatter( + buy_trades["datetime"].values, + _map_trade_values(buy_trades, equity, initial_cash), + marker="^", color="green", s=30, alpha=0.7, zorder=5, label="买入", + ) + if not sell_trades.empty: + ax2.scatter( + sell_trades["datetime"].values, + _map_trade_values(sell_trades, equity, initial_cash), + marker="v", color="orange", s=30, alpha=0.7, zorder=5, label="卖出", + ) + + # 标题:股票代码 + 名称 + 策略绩效 + title = f"{stock_label}" + if stock_name: + title += f" {stock_name}" + perf = result.performance + ret_str = f"{perf.get('total_return', 0):.1%}" + dd_str = f"{perf.get('max_drawdown', 0):.1%}" + sharpe_str = f"{perf.get('sharpe', 0):.2f}" + title += f" | 最佳策略: {strategy_name} | 收益 {ret_str} 回撤 {dd_str} 夏普 {sharpe_str}" + + ax1.set_title(title, fontsize=12, pad=15) + ax1.set_xlabel("日期", fontsize=11) + + # 合并两个轴的图例 + lines1, labels1 = ax1.get_legend_handles_labels() + lines2, labels2 = ax2.get_legend_handles_labels() + ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left", fontsize=9) + + fig.autofmt_xdate() + plt.tight_layout() + click.echo("\n正在显示图表,关闭窗口后继续...") + plt.show() + + @click.command() @click.argument("market") @click.argument("code") @@ -29,6 +150,7 @@ import click @click.option("--commission", default=0.0003, type=float, help="佣金率") @click.option("--adjust", default="QFQ", help="复权: NONE/QFQ/HFQ") @click.option("--period", default="DAILY", help="K线周期") +@click.option("--show", "show_chart", is_flag=True, help="显示最佳策略资金曲线 vs 股价对比图") def run_all( market: str, code: str, @@ -37,6 +159,7 @@ def run_all( commission: float, adjust: str, period: str, + show_chart: bool, ) -> None: """批量运行 strategies/ 目录下所有策略并比较结果。""" from easy_tdx.backtest.engine import BacktestEngine @@ -60,6 +183,7 @@ def run_all( click.echo("正在获取行情数据...") client = MacClient.from_best_host() client.connect() + stock_name = "" try: df = client.get_stock_kline( mkt, @@ -69,6 +193,14 @@ def run_all( count=count, adjust=parse_adjust(adjust), ) + # 获取股票名称 + if show_chart: + try: + quotes_df = client.get_stock_quotes([(mkt, code)]) + if not quotes_df.empty and "name" in quotes_df.columns: + stock_name = str(quotes_df.iloc[0]["name"]) + except Exception: + pass finally: client.close() click.echo(f"获取到 {len(df)} 条K线数据") @@ -268,6 +400,19 @@ def run_all( for r in errored: click.echo(f" {r['strategy']}: {r['error']}") + # 5. 展示最佳策略曲线图 + if show_chart and valid: + best_name = valid[0]["strategy"] + if best_name in backtest_results: + _show_best_chart( + df=df, + result=backtest_results[best_name], + strategy_name=best_name, + stock_label=f"{market}{code}", + stock_name=stock_name, + initial_cash=cash, + ) + if __name__ == "__main__": run_all() diff --git a/strategies/demo/1.png b/strategies/demo/1.png new file mode 100644 index 0000000..807a97f Binary files /dev/null and b/strategies/demo/1.png differ diff --git a/strategies/demo/2.png b/strategies/demo/2.png new file mode 100644 index 0000000..ebb1c89 Binary files /dev/null and b/strategies/demo/2.png differ diff --git a/strategies/demo/3.png b/strategies/demo/3.png new file mode 100644 index 0000000..93c41f4 Binary files /dev/null and b/strategies/demo/3.png differ