feat: named live books; start selected_2026_09 (26 strategies)

A live book is a cohort of strategies, each with its own account, started
on one date with one capital and a flat fee per order (trader.LIVE_BOOKS).
The original book keeps its state in data/; a named book in
data/books/<book>/. The monitor runs every book; auto/status/log/compare/
replay take --book. A fresh account opens on the book's first session.

selected_2026_09: US $10,000 and $1 per order from 2026-09-25, CN ¥50,000
and ¥5 per order from 2026-09-28 (the first CN session after the choice).
Strategies beat their benchmark in the Apr-Sep 2026 point-in-time replay
and passed a 10-year PIT backtest (US Sharpe >= 0.6, MaxDD > -50%; CN
Sharpe >= 0.2, CAGR > 0), no daily variants, at most two frequencies per
factor signal, plus the three 2026-09 research strategies.

bridge.py live --book and bridge.py books feed a book selector on the
live page (/api/strategies/live?book=, /api/strategies/live/books).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 20:46:09 +08:00
parent 905ca4fe05
commit 5dbabc3e08
8 changed files with 309 additions and 47 deletions

View File

@@ -23,9 +23,13 @@ import metrics
from backtest_runner import benchmark_curve, run_market
from research.permanent_yearly import equity_with_cashflows
from trader import (
LIVE_BOOKS,
LIVE_EXCLUDED_STRATEGIES,
MARKET_FEE_TIERED,
MONITOR_CAPITAL,
STRATEGY_REGISTRY,
book_dir,
book_strategies,
convert_quote_currency,
strategy_data_market,
strategy_supports_market,
@@ -863,8 +867,10 @@ def cmd_live(args: argparse.Namespace) -> None:
"""
market = args.market
prefix = f"trader_{market}_"
state_files = sorted(glob(os.path.join("data", f"{prefix}*.json")))
book = args.book or None
state_files = sorted(glob(os.path.join(book_dir(book), f"{prefix}*.json")))
wanted = args.strategy
in_book = set(book_strategies(book, market)) if book else None
panels = _load_panels({market, "etfs"})
bench_ticker, bench_label = LIVE_BENCHMARK[market]
@@ -885,6 +891,9 @@ def cmd_live(args: argparse.Namespace) -> None:
if strategy_name in LIVE_EXCLUDED_STRATEGIES or not strategy_supports_market(strategy_name, market):
_log(f" Skipping {strategy_name}: not a live strategy in the {market.upper()} book")
continue
if in_book is not None and strategy_name not in in_book:
_log(f" Skipping {strategy_name}: no longer in book {book}")
continue
try:
with open(path) as f:
@@ -1018,6 +1027,18 @@ def cmd_live(args: argparse.Namespace) -> None:
json.dump(_sanitize_for_json(results), sys.stdout, default=str)
def cmd_books() -> None:
"""Live books: the original book first, then the named ones (newest last)."""
books = [{"id": "", "label": "全部策略(2026-04 起回放)", "startDate": None,
"capital": MONITOR_CAPITAL, "feePerOrder": None,
"strategies": {m: book_strategies(None, m) for m in ("us", "cn")}}]
for book_id, cfg in LIVE_BOOKS.items():
books.append({"id": book_id, "label": cfg["label"], "startDate": cfg["start"],
"capital": cfg["capital"], "feePerOrder": cfg["fee_per_order"],
"strategies": {m: book_strategies(book_id, m) for m in ("us", "cn")}})
json.dump(books, sys.stdout)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@@ -1043,6 +1064,11 @@ def main() -> None:
live_parser = subparsers.add_parser("live", help="Read live trader states")
live_parser.add_argument("--market", default="us", choices=["us", "cn"])
live_parser.add_argument("--strategy", default=None, help="Filter to specific strategy")
live_parser.add_argument("--book", default="", choices=["", *LIVE_BOOKS],
help="Named live book (default: the original book)")
# books
subparsers.add_parser("books", help="List live books")
args = parser.parse_args()
@@ -1052,6 +1078,8 @@ def main() -> None:
cmd_backtest(args)
elif args.command == "live":
cmd_live(args)
elif args.command == "books":
cmd_books()
else:
parser.print_help(sys.stderr)
sys.exit(1)