From 862f7134835407228ddb4797a3dffe7b97185b3e Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Wed, 10 Jun 2026 03:24:31 +0800 Subject: [PATCH] feat: add 'run-all' CLI command for batch strategy backtesting (v1.9.3) --- README.md | 34 +- pyproject.toml | 2 +- src/easy_tdx/cli/__init__.py | 2 + src/easy_tdx/cli/cmd_run_all.py | 556 ++++++++++++++++++++++++++++++++ 4 files changed, 589 insertions(+), 5 deletions(-) create mode 100644 src/easy_tdx/cli/cmd_run_all.py diff --git a/README.md b/README.md index c475722..d76ade9 100644 --- a/README.md +++ b/README.md @@ -289,9 +289,24 @@ easy-tdx backtest SZ 300308 --strategy-file strategies/expma_cross.py --count 20 交易次数: 24 ``` -**全策略批量对比:** +**全策略批量对比(CLI):** -项目自带 `run_all_strategies.py`,一次跑完 `strategies/` 下所有策略并排名: +`easy-tdx run-all` 一行命令跑完 `strategies/` 下所有策略并排名: + +```bash +easy-tdx run-all SZ 300308 --count 2000 --cash 1000000 --adjust QFQ + +# 多因子组合回测 +easy-tdx run-all SZ 300308 --combo 2 --combo-mode MAJORITY + +# 加 --show 自动弹出最佳策略的资金曲线 vs 股价对比图 +easy-tdx run-all SZ 300308 --count 2000 --cash 1000000 --adjust QFQ --show + +# 自定义策略目录 +easy-tdx run-all SZ 300308 --strategies-dir my_strategies/ +``` + +也可使用项目自带的 `run_all_strategies.py` 脚本(功能相同): ```bash python -X utf8 run_all_strategies.py SZ 300308 --count 2000 --cash 1000000 --adjust QFQ @@ -308,8 +323,8 @@ python -X utf8 run_all_strategies.py SZ 300308 --count 2000 --cash 1000000 --adj # 自动寻找最佳 2 因子和 3 因子组合(MAJORITY 模式) python -X utf8 run_all_strategies.py SZ 300308 --combo 2 --combo 3 --combo-mode majority -# 也可用 AND / OR 模式 -python -X utf8 run_all_strategies.py SZ 300308 --combo 2 --combo-mode and +# CLI 方式 +easy-tdx run-all SZ 300308 --combo 2 --combo 3 --combo-mode majority ``` CLI 指定策略文件组合: @@ -679,6 +694,7 @@ easy-tdx offline sync-all | `indicator` | 技术指标计算(32 个:MACD/KDJ/RSI/BOLL/DMI/ATR...) | | `indicator-list` | 列出可用技术指标 | | `backtest` | 回测引擎(加载策略文件,输出绩效报告) | +| `run-all` | 批量运行所有策略并排名(绩效排名 + 综合评分 + 可选图表) | | `screen scan` | 策略选股扫描(纯离线,全市场信号扫描) | | `screen rank` | 扫描结果回测排名(按夏普/回撤等指标排序) | | `f10` | F10 公司信息 | @@ -1230,6 +1246,16 @@ ruff format --check src/ tests/ # format check ## Changelog +### 1.9.3 (2026-06-10) + +**新增 `run-all` CLI 命令** — 一行命令批量运行 strategies/ 目录下所有策略并排名,与 `run_all_strategies.py` 脚本功能完全一致。 + +- 新增 `easy-tdx run-all` CLI 命令,支持 `--count`、`--cash`、`--commission`、`--adjust`、`--period`、`--combo`、`--combo-mode`、`--show`、`--strategies-dir` 参数 +- 绩效排名 + 综合评分 + 最佳策略交易明细,输出与脚本完全一致 +- 支持多因子组合回测(`--combo 2 --combo 3`)和资金曲线图表展示(`--show`) +- 支持自定义策略目录(`--strategies-dir`) +- `run_all_strategies.py` 保持不变,两种方式并存 + ### 1.9.2 (2026-06-10) **策略选股扫描器** — 新增 `screen` 命令组,用策略扫描全市场找出触发买入信号的股票,再做历史回测排名。纯离线数据,零网络 IO。 diff --git a/pyproject.toml b/pyproject.toml index 1bd8400..ecaf3e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "easy-tdx" -version = "1.9.2" +version = "1.9.3" description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步" readme = "README.md" requires-python = ">=3.10" diff --git a/src/easy_tdx/cli/__init__.py b/src/easy_tdx/cli/__init__.py index a8d1db5..302e49f 100644 --- a/src/easy_tdx/cli/__init__.py +++ b/src/easy_tdx/cli/__init__.py @@ -19,6 +19,7 @@ from .cmd_kline import kline from .cmd_monitor import market_stat, unusual from .cmd_offline import offline from .cmd_quote import quote, quote_list +from .cmd_run_all import run_all from .cmd_tick import tick from .cmd_transaction import transaction @@ -72,4 +73,5 @@ cli.add_command(indicator_list) cli.add_command(offline) cli.add_command(chanlun) cli.add_command(backtest) +cli.add_command(run_all) cli.add_command(screen) diff --git a/src/easy_tdx/cli/cmd_run_all.py b/src/easy_tdx/cli/cmd_run_all.py new file mode 100644 index 0000000..ebe186a --- /dev/null +++ b/src/easy_tdx/cli/cmd_run_all.py @@ -0,0 +1,556 @@ +"""run-all 命令 — 批量运行 strategies/ 目录下所有策略并比较结果。 + +用法:: + + easy-tdx run-all SZ 300308 --count 2000 --cash 1000000 --adjust QFQ + +输出每个策略的绩效指标,并按总收益率排名。 +""" + +from __future__ import annotations + +import importlib.util +import time +from pathlib import Path +from typing import Any + +import click + +# ── 辅助函数 ────────────────────────────────────────────────────────────────── + + +def _load_strategy_class(file_path: Path) -> type | None: + """从 Python 文件加载 Strategy 子类,失败返回 None。""" + from ..backtest.strategy import Strategy + + spec = importlib.util.spec_from_file_location("strategy_module", file_path) + if spec is None or spec.loader is None: + return None + + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + except Exception: + return None + + for attr_name in dir(module): + obj = getattr(module, attr_name) + try: + if isinstance(obj, type) and issubclass(obj, Strategy) and obj is not Strategy: + return obj + except TypeError: + pass + + return None + + +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 _print_ranking( + results: list[dict[str, Any]], + backtest_results: dict[str, Any], +) -> bool: + """输出策略绩效排名、综合评分和最佳策略明细。 + + Returns: + True 表示有有效结果,False 表示全部失败。 + """ + valid = [r for r in results if "error" not in r] + errored = [r for r in results if "error" in r] + + if not valid: + click.echo("所有策略均运行失败!") + for r in errored: + click.echo(f" {r['strategy']}: {r['error']}") + return False + + valid.sort(key=lambda x: x["total_return"], reverse=True) + + # ── 绩效排名 ────────────────────────────────────────────────────────────── + click.echo("\n" + "=" * 80) + click.echo("[*] 策略绩效排名 (按总收益率降序)") + click.echo("=" * 80) + + click.echo( + f"{'排名':>4} {'策略':<22} {'总收益率':>10} {'年化收益':>10} " + f"{'最大回撤':>10} {'夏普':>8} {'胜率':>8} {'交易次数':>8} {'盈亏比':>8}" + ) + click.echo("-" * 100) + + for i, r in enumerate(valid, 1): + medal = " *1*" if i == 1 else " *2*" if i == 2 else " *3*" if i == 3 else " " + click.echo( + f"{medal}{i:>2} {r['strategy']:<22} " + f"{r['total_return']:>9.2%} " + f"{r['annual_return']:>9.2%} " + f"{r['max_drawdown']:>9.2%} " + f"{r['sharpe']:>8.2f} " + f"{r['win_rate']:>7.1%} " + f"{r['total_trades']:>8} " + f"{r['profit_factor']:>8.2f}" + ) + + # ── 最佳策略详细报告 ────────────────────────────────────────────────────── + best = valid[0] + click.echo("\n" + "=" * 80) + click.echo(f"[BEST] 最佳策略: {best['strategy']}") + click.echo("=" * 80) + click.echo(f" 总收益率: {best['total_return']:.2%}") + click.echo(f" 年化收益: {best['annual_return']:.2%}") + click.echo(f" 最大回撤: {best['max_drawdown']:.2%}") + click.echo(f" 夏普比率: {best['sharpe']:.2f}") + click.echo(f" 索提诺: {best['sortino']:.2f}") + click.echo(f" 卡玛比率: {best['calmar']:.2f}") + click.echo(f" 胜率: {best['win_rate']:.1%}") + click.echo(f" 交易次数: {best['total_trades']}") + click.echo(f" 盈亏比: {best['profit_factor']:.2f}") + click.echo(f" 年化波动: {best['volatility']:.4f}") + + # ── 综合评分 ────────────────────────────────────────────────────────────── + click.echo("\n" + "=" * 80) + click.echo("[*] 综合评分排名 (Sharpe*0.4 + Ret/DD*0.3 + WinRate*0.3)") + click.echo("=" * 80) + + scored: list[tuple[dict[str, Any], float]] = [] + for r in valid: + ret_dd_ratio = r["annual_return"] / r["max_drawdown"] if r["max_drawdown"] > 1e-6 else 999.0 + score = r["sharpe"] * 0.4 + ret_dd_ratio * 0.3 + r["win_rate"] * 100 * 0.3 + scored.append((r, score)) + + scored.sort(key=lambda x: x[1], reverse=True) + + click.echo( + f"{'排名':>4} {'策略':<22} {'综合评分':>10} {'夏普':>8} {'收益/回撤':>10} {'胜率':>8}" + ) + click.echo("-" * 70) + for i, (r, score) in enumerate(scored, 1): + ret_dd_ratio = r["annual_return"] / r["max_drawdown"] if r["max_drawdown"] > 1e-6 else 999.0 + medal = " *1*" if i == 1 else " *2*" if i == 2 else " *3*" if i == 3 else " " + click.echo( + f"{medal}{i:>2} {r['strategy']:<22} {score:>10.2f} " + f"{r['sharpe']:>8.2f} {ret_dd_ratio:>10.2f} {r['win_rate']:>7.1%}" + ) + + # ── 最佳策略交易明细 ────────────────────────────────────────────────────── + best_name = valid[0]["strategy"] + if best_name in backtest_results: + bt = backtest_results[best_name] + bp = bt.performance + bc = bt.config + + click.echo("\n" + "=" * 80) + click.echo(f"[DETAIL] 最佳策略交易明细: {best_name}") + click.echo("=" * 80) + + click.echo("=== 回测绩效概要 ===") + click.echo(f"总收益率: {bp.get('total_return', 0):.2%}") + click.echo(f"年化收益: {bp.get('annual_return', 0):.2%}") + click.echo(f"最大回撤: {bp.get('max_drawdown', 0):.2%}") + click.echo(f"夏普比率: {bp.get('sharpe', 0):.2f}") + click.echo(f"胜率: {bp.get('win_rate', 0):.2%}") + click.echo(f"交易次数: {bp.get('total_trades', 0)}") + click.echo() + click.echo("=== 配置参数 ===") + click.echo(f"初始资金: {bc.get('cash', 0):.2f}") + click.echo(f"佣金率: {bc.get('commission', 0):.4f}") + click.echo(f"成交规则: {bc.get('execution', 'next_open')}") + click.echo() + + if not bt.trades.empty: + click.echo("=== 最近交易记录 ===") + recent_trades = bt.trades.tail(10) + for _, trade in recent_trades.iterrows(): + direction = "买入" if trade["direction"] == "BUY" else "卖出" + status = "拒绝" if trade["rejected"] else "成交" + click.echo( + f" [{trade['datetime']}] {direction} " + f"数量={trade['size']:.0f} 价格={trade['price']:.2f} " + f"盈亏={trade['pnl']:.2f} [{status}]" + ) + else: + click.echo("无交易记录") + + # ── 报告错误 ────────────────────────────────────────────────────────────── + if errored: + click.echo("\n[!] 以下策略运行失败:") + for r in errored: + click.echo(f" {r['strategy']}: {r['error']}") + + return True + + +def _run_combo_screen( + strategy_classes: dict[str, type], + df: Any, + cash: float, + commission: float, + combo_sizes: tuple[int, ...], + combo_mode: str, +) -> None: + """运行多因子组合回测并输出排名。""" + from math import comb + + from ..backtest.combo import CombinationRunner + + classes_list = list(strategy_classes.values()) + if len(classes_list) < 2: + click.echo("[!] 策略数量不足 2 个,跳过组合回测") + return + + # 单个 Runner 跨 size 复用信号缓存,避免重复提取 + runner = CombinationRunner( + strategy_classes=classes_list, + df=df, + cash=cash, + commission=commission, + ) + + for size in combo_sizes: + total = comb(len(classes_list), size) + click.echo("\n" + "=" * 80) + click.echo(f"[*] {size}因子组合回测 (共{total}组, 模式={combo_mode})") + click.echo("=" * 80) + + results = runner.screen(combo_sizes=(size,), mode=combo_mode.upper()) + + if not results: + click.echo(" 无有效交易组合(所有组合均为零交易)") + continue + + click.echo( + f"{'排名':>4} {'因子组合':<50} {'总收益率':>10} {'年化收益':>10} " + f"{'最大回撤':>10} {'夏普':>8} {'胜率':>8} {'交易':>6}" + ) + click.echo("-" * 120) + + for i, r in enumerate(results[:20], 1): + medal = " *1*" if i == 1 else " *2*" if i == 2 else " *3*" if i == 3 else " " + perf = r.result.performance + click.echo( + f"{medal}{i:>2} {r.name:<50} " + f"{perf.get('total_return', 0):>9.2%} " + f"{perf.get('annual_return', 0):>9.2%} " + f"{perf.get('max_drawdown', 0):>9.2%} " + f"{perf.get('sharpe', 0):>8.2f} " + f"{perf.get('win_rate', 0):>7.1%} " + f"{perf.get('total_trades', 0):>6}" + ) + + if len(results) > 20: + click.echo(f" ... 共 {len(results)} 个有效组合,仅显示前 20") + + # 最佳组合详细报告 + best = results[0] + bp = best.result.performance + click.echo(f"\n[BEST {size}因子] {best.name}") + click.echo(f" 总收益率: {bp.get('total_return', 0):.2%}") + click.echo(f" 年化收益: {bp.get('annual_return', 0):.2%}") + click.echo(f" 最大回撤: {bp.get('max_drawdown', 0):.2%}") + click.echo(f" 夏普比率: {bp.get('sharpe', 0):.2f}") + click.echo(f" 胜率: {bp.get('win_rate', 0):.1%}") + + +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("run-all") +@click.argument("market") +@click.argument("code") +@click.option("--count", default=2000, type=int, help="K线数量") +@click.option("--cash", default=1000000.0, type=float, help="初始资金") +@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( + "--combo", + "combo_sizes", + multiple=True, + type=int, + help="多因子组合回测(可多次指定,如 --combo 2 --combo 3)", +) +@click.option( + "--combo-mode", + "combo_mode", + default="MAJORITY", + type=click.Choice(["AND", "OR", "MAJORITY"], case_sensitive=False), + help="多因子信号合并模式(默认 MAJORITY)", +) +@click.option("--show", "show_chart", is_flag=True, help="显示最佳策略资金曲线 vs 股价对比图") +@click.option( + "--strategies-dir", + "strategies_dir", + default="strategies", + help="策略文件目录(默认 strategies/)", +) +def run_all( + market: str, + code: str, + count: int, + cash: float, + commission: float, + adjust: str, + period: str, + combo_sizes: tuple[int, ...], + combo_mode: str, + show_chart: bool, + strategies_dir: str, +) -> None: + """批量运行 strategies/ 目录下所有策略并比较结果。 + + 依次运行指定目录下所有策略文件,输出绩效排名和综合评分。 + + 示例: + + easy-tdx run-all SZ 300308 --count 2000 --cash 1000000 --adjust QFQ + + easy-tdx run-all SZ 300308 --combo 2 --combo-mode MAJORITY + + easy-tdx run-all SZ 300308 --show + """ + from ..backtest.engine import BacktestEngine + from ..cli.conn import get_mac_client + from ..cli.parsers import parse_adjust, parse_market, parse_period + + # 1. 发现策略文件 + sdir = Path(strategies_dir) + strategy_files = sorted(sdir.glob("*.py")) + if not strategy_files: + click.echo(f"未找到策略文件 ({strategies_dir}/*.py)", err=True) + raise SystemExit(1) + + click.echo(f"发现 {len(strategy_files)} 个策略文件") + click.echo(f"标的: {market} {code} | K线: {count} | 资金: {cash:,.0f} | 复权: {adjust}") + click.echo("=" * 80) + + # 2. 获取数据(所有策略共享同一份数据) + mkt = parse_market(market) + click.echo("正在获取行情数据...") + stock_name = "" + with get_mac_client() as client: + df = client.get_stock_kline( + mkt, + code, + period=parse_period(period), + start=0, + 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 + click.echo(f"获取到 {len(df)} 条K线数据") + click.echo("=" * 80) + + # 3. 逐个运行策略 + results: list[dict[str, Any]] = [] + backtest_results: dict[str, Any] = {} + strategy_classes: dict[str, type] = {} + + for sf in strategy_files: + strategy_name = sf.stem + click.echo(f"\n>> 运行策略: {strategy_name} ...", nl=False) + + strategy_cls = _load_strategy_class(sf) + if strategy_cls is None: + click.echo(" [加载失败/无 Strategy 子类]") + continue + + strategy_classes[strategy_name] = strategy_cls + t0 = time.perf_counter() + try: + engine = BacktestEngine( + strategy=strategy_cls, + cash=cash, + commission=commission, + ) + result = engine.run(df) + elapsed = time.perf_counter() - t0 + perf = result.performance + click.echo(f" 完成 ({elapsed:.1f}s)") + + results.append( + { + "strategy": strategy_name, + "total_return": perf.get("total_return", 0), + "annual_return": perf.get("annual_return", 0), + "max_drawdown": perf.get("max_drawdown", 0), + "sharpe": perf.get("sharpe", 0), + "sortino": perf.get("sortino", 0), + "calmar": perf.get("calmar", 0), + "win_rate": perf.get("win_rate", 0), + "total_trades": perf.get("total_trades", 0), + "profit_factor": perf.get("profit_factor", 0), + "volatility": perf.get("volatility", 0), + } + ) + backtest_results[strategy_name] = result + except Exception as e: + elapsed = time.perf_counter() - t0 + click.echo(f" 错误 ({elapsed:.1f}s): {e}") + results.append({"strategy": strategy_name, "error": str(e)}) + + # 4. 输出排名 + has_valid = _print_ranking(results, backtest_results) + if not has_valid: + raise SystemExit(1) + + # 5. 多因子组合回测 + if combo_sizes: + _run_combo_screen( + strategy_classes=strategy_classes, + df=df, + cash=cash, + commission=commission, + combo_sizes=combo_sizes, + combo_mode=combo_mode, + ) + + # 6. 展示最佳策略曲线图 + if show_chart: + valid = [r for r in results if "error" not in r] + if valid: + valid.sort(key=lambda x: x["total_return"], reverse=True) + 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, + )