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:
@@ -16,6 +16,7 @@ import {
|
||||
formatNumber,
|
||||
formatPct,
|
||||
type EquityPoint,
|
||||
type LiveBook,
|
||||
type LiveStrategyState,
|
||||
type StrategyCurrency,
|
||||
type StrategyMarket,
|
||||
@@ -40,8 +41,29 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
|
||||
type SortKey = "currentValue" | "unrealizedPnlPct" | "name" | "lastUpdated";
|
||||
|
||||
const BOOK_STORAGE_KEY = "live.book";
|
||||
|
||||
function storedBook(): string | null {
|
||||
try {
|
||||
return window.localStorage.getItem(BOOK_STORAGE_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function storeBook(book: string) {
|
||||
try {
|
||||
window.localStorage.setItem(BOOK_STORAGE_KEY, book);
|
||||
} catch {
|
||||
// storage unavailable: the choice just is not remembered
|
||||
}
|
||||
}
|
||||
|
||||
export function LivePage() {
|
||||
const [market, setMarket] = useState<StrategyMarket>("us");
|
||||
const [books, setBooks] = useState<LiveBook[]>([]);
|
||||
// null until the book list is known; "" is the original book
|
||||
const [book, setBook] = useState<string | null>(null);
|
||||
const [strategies, setStrategies] = useState<LiveStrategyState[]>([]);
|
||||
const [selectedName, setSelectedName] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -49,10 +71,29 @@ export function LivePage() {
|
||||
const [sortKey, setSortKey] = useState<SortKey>("unrealizedPnlPct");
|
||||
const [filterText, setFilterText] = useState("");
|
||||
|
||||
const refresh = (mk: StrategyMarket = market) => {
|
||||
useEffect(() => {
|
||||
api<LiveBook[]>("/api/strategies/live/books")
|
||||
.then((res) => {
|
||||
setBooks(res);
|
||||
const saved = storedBook();
|
||||
// Default to the newest named book (listed last).
|
||||
setBook(saved !== null && res.some((b) => b.id === saved) ? saved : (res[res.length - 1]?.id ?? ""));
|
||||
})
|
||||
.catch(() => setBook(""));
|
||||
}, []);
|
||||
|
||||
const selectBook = (id: string) => {
|
||||
storeBook(id);
|
||||
setBook(id);
|
||||
};
|
||||
|
||||
const currentBook = books.find((b) => b.id === book);
|
||||
|
||||
const refresh = (mk: StrategyMarket = market, bk: string | null = book) => {
|
||||
if (bk === null) return;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
api<LiveStrategyState[]>(`/api/strategies/live?market=${mk}`)
|
||||
api<LiveStrategyState[]>(`/api/strategies/live?market=${mk}&book=${encodeURIComponent(bk)}`)
|
||||
.then((res) => {
|
||||
setStrategies(res);
|
||||
if (res.length > 0) {
|
||||
@@ -68,9 +109,9 @@ export function LivePage() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refresh(market);
|
||||
refresh(market, book);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [market]);
|
||||
}, [market, book]);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const list = [...strategies];
|
||||
@@ -111,6 +152,20 @@ export function LivePage() {
|
||||
<button className={market === "us" ? "active" : ""} onClick={() => setMarket("us")}>美股</button>
|
||||
<button className={market === "cn" ? "active" : ""} onClick={() => setMarket("cn")}>A股</button>
|
||||
</div>
|
||||
{books.length > 1 && (
|
||||
<label className="ghostSelectLabel">
|
||||
<span>组合</span>
|
||||
<select
|
||||
className="ghostSelect"
|
||||
value={book ?? ""}
|
||||
onChange={(e) => selectBook(e.target.value)}
|
||||
>
|
||||
{books.map((b) => (
|
||||
<option key={b.id} value={b.id}>{b.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
className="ghostInput"
|
||||
placeholder="筛选策略..."
|
||||
@@ -151,7 +206,7 @@ export function LivePage() {
|
||||
{loading && sorted.length === 0 ? (
|
||||
<p className="hint">加载中...</p>
|
||||
) : sorted.length === 0 ? (
|
||||
<p className="hint">暂无在线策略。运行 quant 的 trader.py auto 后再刷新。</p>
|
||||
<p className="hint">{emptyHint(currentBook, market)}</p>
|
||||
) : (
|
||||
<div className="liveList">
|
||||
{sorted.map((s) => (
|
||||
@@ -203,6 +258,14 @@ export function LivePage() {
|
||||
);
|
||||
}
|
||||
|
||||
function emptyHint(book: LiveBook | undefined, market: StrategyMarket): string {
|
||||
if (!book?.startDate) return "暂无在线策略。运行 quant 的 trader.py auto 后再刷新。";
|
||||
const currency = currencyForMarket(market);
|
||||
const fee = book.feePerOrder ? ` · 每笔 ${formatMoney(book.feePerOrder[market], currency)}` : "";
|
||||
return `${book.label}:${book.strategies[market].length} 个策略,各 ${formatMoney(book.capital[market], currency)}${fee},`
|
||||
+ `${book.startDate[market]} 收盘后出现首日记录。`;
|
||||
}
|
||||
|
||||
function LiveStrategyDetail({ strategy }: { strategy: LiveStrategyState }) {
|
||||
const m = strategy;
|
||||
const currency = currencyOf(m);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { WatchlistStore } from "../agent/watchlistStore";
|
||||
import { loadIpadsConfig } from "./ipadsConfig";
|
||||
import type { ChartPeriod, WatchStock } from "../shared/types";
|
||||
import type { BacktestConfig } from "../shared/strategyTypes";
|
||||
import { listStrategies, runBacktest, getCachedResult, getLiveStrategies } from "./strategyService";
|
||||
import { listStrategies, runBacktest, getCachedResult, getLiveStrategies, getLiveBooks } from "./strategyService";
|
||||
|
||||
const port = Number(process.env.PORT ?? 8787);
|
||||
const host = process.env.HOST ?? "127.0.0.1";
|
||||
@@ -284,7 +284,16 @@ app.get("/api/strategies/live", async (request, response, next) => {
|
||||
try {
|
||||
const market = String(request.query.market ?? "us");
|
||||
const strategy = request.query.strategy ? String(request.query.strategy) : undefined;
|
||||
response.json(await getLiveStrategies(market, strategy));
|
||||
const book = request.query.book ? String(request.query.book) : undefined;
|
||||
response.json(await getLiveStrategies(market, strategy, book));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/strategies/live/books", async (_request, response, next) => {
|
||||
try {
|
||||
response.json(await getLiveBooks());
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { REPO_ROOT, WEB_DATA_DIR } from "./paths";
|
||||
import type { BacktestConfig, BacktestResult, LiveStrategyState, StrategyInfo } from "../shared/strategyTypes.js";
|
||||
import type { BacktestConfig, BacktestResult, LiveBook, LiveStrategyState, StrategyInfo } from "../shared/strategyTypes.js";
|
||||
|
||||
const CACHE_DIR = join(WEB_DATA_DIR, "strategy_cache");
|
||||
const TIMEOUT_MS = Number(process.env.QUANT_BACKTEST_TIMEOUT_MS) || 180_000;
|
||||
@@ -157,12 +157,17 @@ export async function getCachedResult(id: string): Promise<BacktestResult | null
|
||||
return normalizeBacktestResult(JSON.parse(readFileSync(cachePath, "utf-8")) as BacktestResult);
|
||||
}
|
||||
|
||||
export async function getLiveStrategies(market: string, strategy?: string): Promise<LiveStrategyState[]> {
|
||||
export async function getLiveStrategies(market: string, strategy?: string, book?: string): Promise<LiveStrategyState[]> {
|
||||
const args = ["live", "--market", market];
|
||||
if (strategy) args.push("--strategy", strategy);
|
||||
if (book) args.push("--book", book);
|
||||
return (await runBridge(args)) as LiveStrategyState[];
|
||||
}
|
||||
|
||||
export async function getLiveBooks(): Promise<LiveBook[]> {
|
||||
return (await runBridge(["books"])) as LiveBook[];
|
||||
}
|
||||
|
||||
function normalizeBacktestResult(result: BacktestResult): BacktestResult {
|
||||
return {
|
||||
...result,
|
||||
|
||||
@@ -160,6 +160,18 @@ export interface TradeLogEntry {
|
||||
commission: number;
|
||||
}
|
||||
|
||||
// A live book: a cohort of strategies, each with its own account, started on
|
||||
// one date with one capital and fee schedule. id "" is the original book.
|
||||
export interface LiveBook {
|
||||
id: string;
|
||||
label: string;
|
||||
startDate: Record<StrategyMarket, string> | null;
|
||||
capital: Record<StrategyMarket, number>;
|
||||
// Flat fee per order; null for the original book's tiered schedule.
|
||||
feePerOrder: Record<StrategyMarket, number> | null;
|
||||
strategies: Record<StrategyMarket, string[]>;
|
||||
}
|
||||
|
||||
export interface LiveStrategyState {
|
||||
name: string;
|
||||
label: string;
|
||||
|
||||
Reference in New Issue
Block a user