跳转至

期货逐日盯市处理

本任务核心为期货逐日盯市全链路处理,主程序为main.py,输入为trades_cf.csv(逐笔开平仓记录,含开仓时间/价格、平仓时间/价格、方向、手数、手续费参数等)与cf2309.csv(按交易日聚合的行情数据,含trade_date、datetime、close),核心处理逻辑为逐日盯市计算,配套后处理脚本完成风险绩效统计与字段标准化,最终产出可直接用于报告的统一格式结果。

主程序(main.py)完整处理流程:读取并清洗CSV文件(兼容utf-8、utf-8-sig、gb18030、gbk四种编码,可自动处理带说明行的表头)→ 将自然时间映射到交易日(21:00及之后归属下一交易日)→ 生成逐笔交易表output/trades.csv、逐日持仓表output/position.csv、逐日权益表output/balance.csv,核心计算遵循逐日盯市口径。

核心计算口径:当日开平的trade_pnl = (平仓价 - 开仓价) × 方向 × 手数 × 合约乘数;跨日持仓按结算价变动拆分hold_pnl(持仓浮盈)与平仓日trade_pnl;手续费为开平总成本,均分到开仓日与平仓日;权益(equity)、收益率(daily_ret)、净值(nav)由每日总盈亏(daily_pnl)逐日累积计算得出。

from __future__ import annotations

import argparse
from dataclasses import dataclass
from pathlib import Path

import pandas as pd


@dataclass(frozen=True)
class ContractSpec:
    tick_size: float
    size: float


def _read_trades(path: Path) -> pd.DataFrame:
    df = _read_csv_any_encoding(path)
    df["open_time"] = pd.to_datetime(df["open_time"])
    df["cover_time"] = pd.to_datetime(df["cover_time"])
    df["cover_date"] = df["cover_date"].astype(int)
    df["ls"] = df["ls"].astype(int)
    df["volume"] = df["volume"].astype(float)
    return df


def _read_daily(path: Path) -> pd.DataFrame:
    df = _read_csv_any_encoding(path)
    df["trade_date"] = df["trade_date"].astype(int)
    df["datetime"] = pd.to_datetime(df["datetime"])
    df = df.sort_values("trade_date").reset_index(drop=True)
    return df


def _read_csv_any_encoding(path: Path) -> pd.DataFrame:
    """
    The provided CSVs are often GBK/GB18030 on Windows.
    Try a small set of common encodings deterministically.
    """
    last_err: Exception | None = None
    for enc in ("utf-8", "utf-8-sig", "gb18030", "gbk"):
        try:
            df = pd.read_csv(path, encoding=enc)
            # Many provided CSVs contain a human-readable first row (e.g. "表头注释" or "交易日")
            # and the real header starts from row 2.
            col0 = str(df.columns[0]) if len(df.columns) else ""
            if ("表头注释" in col0) or ("交易日" in col0):
                df = pd.read_csv(path, encoding=enc, header=1)
            return df
        except UnicodeDecodeError as e:
            last_err = e
            continue
    raise UnicodeDecodeError(
        "unknown",
        b"",
        0,
        1,
        f"Failed to decode {path} with utf-8/utf-8-sig/gb18030/gbk. Last error: {last_err}",
    )


def _build_trade_calendar(daily: pd.DataFrame) -> pd.Series:
    # Sorted unique trade dates available for settlement/close.
    cal = pd.Series(daily["trade_date"].unique()).sort_values().reset_index(drop=True)
    return cal


def _next_trade_date(calendar: pd.Series, yyyymmdd: int) -> int:
    # First trade date >= yyyymmdd.
    i = int(calendar.searchsorted(yyyymmdd, side="left"))
    if i >= len(calendar):
        raise ValueError(f"Timestamp maps beyond available daily data: {yyyymmdd}")
    return int(calendar.iloc[i])


def _map_natural_time_to_trade_date(calendar: pd.Series, ts: pd.Timestamp) -> int:
    # Per题干:21:00-次日15:00 归属“次日”交易日。简化映射:
    # - 若自然时间 >= 21:00,归属下一个交易日(>= 次日日期)
    # - 否则归属当日交易日(>= 当日日期)
    base_date = ts.date()
    if ts.time() >= pd.Timestamp("21:00").time():
        base_date = (ts + pd.Timedelta(days=1)).date()
    yyyymmdd = int(pd.Timestamp(base_date).strftime("%Y%m%d"))
    return _next_trade_date(calendar, yyyymmdd)


def build_trades_table(trades_raw: pd.DataFrame, daily: pd.DataFrame) -> pd.DataFrame:
    """
    Match the demo Excel schema: one row per completed round-trip trade.
    """
    df = trades_raw.copy().reset_index(drop=True)
    df["contract"] = df["symbol"].astype(str)
    df["open_ts"] = pd.to_datetime(df["open_time"])
    df["open_px"] = df["open_price"].astype(float)
    df["open_qty"] = (df["ls"].astype(int) * df["volume"].astype(float)).astype(float)
    df["multiplier"] = df["size"].astype(float)

    fee_cash = (
        df["fee"].astype(float)
        * df["size"].astype(float)
        * df["tick_size"].astype(float)
    )
    df["fee_open"] = (fee_cash / 2.0).astype(float)

    df["close_ts"] = pd.to_datetime(df["cover_time"])
    df["close_px"] = df["cover_price"].astype(float)
    df["close_qty"] = (-df["open_qty"]).astype(float)
    df["fee_close"] = (fee_cash / 2.0).astype(float)
    df["pnl_ticks"] = (
        (df["close_px"] - df["open_px"])
        / df["tick_size"].astype(float)
        * df["ls"].astype(int)
    ).round(3)

    out = df[
        [
            "contract",
            "open_ts",
            "open_px",
            "open_qty",
            "multiplier",
            "fee_open",
            "close_ts",
            "close_px",
            "close_qty",
            "fee_close",
            "pnl_ticks",
        ]
    ].copy()
    out.insert(0, "trade_id", range(len(out)))
    return out


def build_daily_pnls(trades_raw: pd.DataFrame, daily: pd.DataFrame) -> pd.DataFrame:
    """
    Compute daily trade_pnl / hold_pnl / trade_cost under mark-to-market:
    - intraday close: (cover - open) * dir * vol * size
    - multi-day: split into
      open_day hold: (close(open_day) - open) * dir * vol * size
      between days hold: (close(d) - close(prev)) * dir * vol * size
      cover_day trade: (cover - close(prev_trade_day)) * dir * vol * size
    Fees: fee is round-trip total (ticks), split equally across open/cover legs,
          and charged on their respective trade_date.
    """
    cal = _build_trade_calendar(daily)
    close_map = daily.set_index("trade_date")["close"].astype(float)

    # For prev close lookup
    cal_list = cal.tolist()
    prev_map: dict[int, int] = {
        int(cal_list[i]): int(cal_list[i - 1]) for i in range(1, len(cal_list))
    }

    def prev_trade_date(d: int) -> int:
        if d not in prev_map:
            raise ValueError(f"No previous trade day for {d} in daily data")
        return prev_map[d]

    # accumulate per day
    acc = pd.DataFrame(index=cal.astype(int))
    acc["trade_pnl"] = 0.0
    acc["hold_pnl"] = 0.0
    acc["trade_cost"] = 0.0

    # fee cost on open/cover days
    fee_cash = (
        trades_raw["fee"].astype(float)
        * trades_raw["size"].astype(float)
        * trades_raw["tick_size"].astype(float)
    )
    half_fee_cash = fee_cash / 2.0

    for idx, r in trades_raw.reset_index(drop=True).iterrows():
        dir_ = 1.0 if int(r["ls"]) == 1 else -1.0
        vol = float(r["volume"])
        size = float(r["size"])
        mult = vol * size

        open_day = _map_natural_time_to_trade_date(cal, pd.to_datetime(r["open_time"]))
        cover_day = int(r["cover_date"])

        # trade cost split by legs
        acc.loc[open_day, "trade_cost"] += float(half_fee_cash.iloc[idx])
        acc.loc[cover_day, "trade_cost"] += float(half_fee_cash.iloc[idx])

        open_price = float(r["open_price"])
        cover_price = float(r["cover_price"])

        if open_day == cover_day:
            acc.loc[cover_day, "trade_pnl"] += (cover_price - open_price) * dir_ * mult
            continue

        # Open day holding pnl (today-opened position marked to close)
        acc.loc[open_day, "hold_pnl"] += (
            (float(close_map.loc[open_day]) - open_price) * dir_ * mult
        )

        # Intermediate days holding pnl
        open_pos = cal.searchsorted(open_day)
        cover_pos = cal.searchsorted(cover_day)
        for i in range(int(open_pos) + 1, int(cover_pos)):
            d = int(cal.iloc[i])
            d_prev = prev_trade_date(d)
            acc.loc[d, "hold_pnl"] += (
                (float(close_map.loc[d]) - float(close_map.loc[d_prev])) * dir_ * mult
            )

        # Cover day trade pnl for historical position (vs yesterday settlement)
        d_prev = prev_trade_date(cover_day)
        acc.loc[cover_day, "trade_pnl"] += (
            (cover_price - float(close_map.loc[d_prev])) * dir_ * mult
        )

    acc = acc.reset_index(names=["trade_date"])
    return acc


def build_position_table(trades_raw: pd.DataFrame, daily: pd.DataFrame) -> pd.DataFrame:
    cal = _build_trade_calendar(daily)
    close_map = daily.set_index("trade_date")["close"].astype(float)

    cal_list = cal.tolist()
    prev_map: dict[int, int] = {
        int(cal_list[i]): int(cal_list[i - 1]) for i in range(1, len(cal_list))
    }

    def prev_trade_date(d: int) -> int | None:
        return prev_map.get(d)

    # Map open_day for each trade
    tmp = trades_raw.copy()
    tmp["open_day"] = (
        tmp["open_time"]
        .apply(lambda x: _map_natural_time_to_trade_date(cal, pd.to_datetime(x)))
        .astype(int)
    )
    tmp["cover_day"] = tmp["cover_date"].astype(int)
    tmp["dir"] = tmp["ls"].map({1: 1, -1: -1}).astype(int)
    tmp["vol"] = tmp["volume"].astype(float)

    out_rows: list[dict] = []
    for d in cal.astype(int).tolist():
        d_prev = prev_trade_date(d)
        prev_close = float(close_map.loc[d_prev]) if d_prev is not None else None

        # Active trades at end of day: open_day <= d < cover_day
        active = tmp[(tmp["open_day"] <= d) & (d < tmp["cover_day"])]
        if active.empty:
            continue

        for ls in (1, -1):
            sub = active[active["ls"] == ls]
            if sub.empty:
                continue

            total_vol = float(sub["vol"].sum())
            signed_total = total_vol if ls == 1 else -total_vol

            today_open = sub[sub["open_day"] == d]
            overnight = sub[sub["open_day"] < d]

            num = 0.0
            den = 0.0
            if not today_open.empty:
                num += float(
                    (today_open["vol"] * today_open["open_price"].astype(float)).sum()
                )
                den += float(today_open["vol"].sum())
            if not overnight.empty:
                if prev_close is None:
                    raise ValueError(
                        f"Need prev close to price overnight position on {d}"
                    )
                num += float(overnight["vol"].sum()) * float(prev_close)
                den += float(overnight["vol"].sum())

            cost_price = float(num / den) if den != 0 else float("nan")

            out_rows.append(
                {
                    "trade_date": int(d),
                    "symbol": str(sub["symbol"].iloc[0]),
                    "ls": int(ls),
                    "number": float(signed_total),
                    "cost_price": cost_price,
                    "close": float(close_map.loc[d]),
                }
            )

    return (
        pd.DataFrame(out_rows).sort_values(["trade_date", "ls"]).reset_index(drop=True)
    )


def format_position_table(position_internal: pd.DataFrame) -> pd.DataFrame:
    if position_internal.empty:
        return position_internal.copy()
    pos = position_internal.copy()
    pos = pos.rename(
        columns={
            "symbol": "contract",
            "close": "settle_px",
            "number": "position_qty",
            "cost_price": "cost_px",
        }
    )
    pos["direction"] = pos["ls"].map({1: "LONG", -1: "SHORT"})
    pos["biz_date"] = pd.to_datetime(
        pos["trade_date"].astype(str), format="%Y%m%d"
    ).dt.strftime("%Y-%m-%d")
    return pos[
        ["biz_date", "contract", "direction", "cost_px", "settle_px", "position_qty"]
    ].reset_index(drop=True)


def build_balance_table(
    daily: pd.DataFrame,
    daily_pnls: pd.DataFrame,
    position: pd.DataFrame,
    spec: ContractSpec,
    init_balance: float = 1_000_000.0,
) -> pd.DataFrame:
    cal = _build_trade_calendar(daily).astype(int)
    close_map = daily.set_index("trade_date")["close"].astype(float)

    pnl = daily_pnls.set_index("trade_date")[
        ["trade_pnl", "hold_pnl", "trade_cost"]
    ].reindex(cal, fill_value=0.0)

    # Net exposure for market_value: abs(long + short) * size * close
    net_pos = (
        position.groupby("trade_date")["number"].sum().reindex(cal, fill_value=0.0)
        if not position.empty
        else pd.Series(0.0, index=cal)
    )
    market_value = (net_pos.abs() * spec.size * close_map.reindex(cal)).astype(float)

    df = pd.DataFrame(index=cal)
    df["trade_pnl"] = pnl["trade_pnl"].astype(float)
    df["hold_pnl"] = pnl["hold_pnl"].astype(float)
    df["trade_cost"] = pnl["trade_cost"].astype(float)
    df["total_pnl"] = df["trade_pnl"] + df["hold_pnl"] - df["trade_cost"]

    df["cum_pnl"] = df["total_pnl"].cumsum()
    df["balance"] = init_balance + df["cum_pnl"]
    df["market_value"] = market_value

    prev_balance = df["balance"].shift(1)
    df["return"] = (df["total_pnl"] / prev_balance).fillna(0.0)
    df["net"] = (1.0 + df["return"]).cumprod()

    out = df.reset_index(names=["trade_date"])
    out["biz_date"] = pd.to_datetime(
        out["trade_date"].astype(str), format="%Y%m%d"
    ).dt.strftime("%Y-%m-%d")
    out = out.rename(
        columns={
            "hold_pnl": "carry_pnl",
            "trade_pnl": "realized_pnl",
            "trade_cost": "txn_cost",
            "total_pnl": "daily_pnl",
            "market_value": "exposure",
            "cum_pnl": "cum_pnl_amt",
            "balance": "equity",
            "return": "daily_ret",
            "net": "nav",
        }
    )[
        [
            "biz_date",
            "carry_pnl",
            "realized_pnl",
            "txn_cost",
            "daily_pnl",
            "exposure",
            "cum_pnl_amt",
            "equity",
            "daily_ret",
            "nav",
        ]
    ].copy()
    return out


def main() -> int:
    ap = argparse.ArgumentParser(
        description="Compute futures MTM trades/position/balance tables."
    )
    ap.add_argument("--trades", type=Path, default=Path("trades_cf.csv"))
    ap.add_argument("--daily", type=Path, default=Path("cf2309.csv"))
    ap.add_argument("--out", type=Path, default=Path("output"))
    args = ap.parse_args()

    trades_raw = _read_trades(args.trades)
    daily = _read_daily(args.daily)

    cal = _build_trade_calendar(daily)
    open_days = (
        trades_raw["open_time"]
        .apply(lambda x: _map_natural_time_to_trade_date(cal, pd.to_datetime(x)))
        .astype(int)
    )
    start_day = int(min(open_days.min(), trades_raw["cover_date"].min()))
    end_day = int(max(open_days.max(), trades_raw["cover_date"].max()))

    spec = ContractSpec(
        tick_size=float(trades_raw["tick_size"].iloc[0]),
        size=float(trades_raw["size"].iloc[0]),
    )

    trades = build_trades_table(trades_raw, daily)
    daily_pnls = build_daily_pnls(trades_raw, daily)
    position_internal = build_position_table(trades_raw, daily)
    balance = build_balance_table(daily, daily_pnls, position_internal, spec=spec)
    position = format_position_table(position_internal)

    # Slice to relevant date range to match demo style output
    start_date = pd.to_datetime(str(start_day), format="%Y%m%d").strftime("%Y-%m-%d")
    end_date = pd.to_datetime(str(end_day), format="%Y%m%d").strftime("%Y-%m-%d")
    balance = balance[
        (balance["biz_date"] >= start_date) & (balance["biz_date"] <= end_date)
    ].reset_index(drop=True)

    if not position.empty:
        position = position[
            (position["biz_date"] >= start_date) & (position["biz_date"] <= end_date)
        ].reset_index(drop=True)

    # Formatting to better match the provided demo Excel
    for c in ("fee_open", "fee_close"):
        if c in trades.columns:
            trades[c] = trades[c].round(1)

    if not position.empty:
        for c in ("cost_px", "settle_px", "position_qty"):
            if c in position.columns:
                position[c] = position[c].round(6)

    for c in (
        "carry_pnl",
        "realized_pnl",
        "txn_cost",
        "daily_pnl",
        "exposure",
        "cum_pnl_amt",
        "equity",
    ):
        if c in balance.columns:
            balance[c] = balance[c].round(1)
    if "daily_ret" in balance.columns:
        balance["daily_ret"] = balance["daily_ret"].round(5)
    if "nav" in balance.columns:
        balance["nav"] = balance["nav"].round(6)

    args.out.mkdir(parents=True, exist_ok=True)
    trades.to_csv(args.out / "trades.csv", index=False, encoding="utf-8-sig")
    position.to_csv(args.out / "position.csv", index=False, encoding="utf-8-sig")
    balance.to_csv(args.out / "balance.csv", index=False, encoding="utf-8-sig")

    print(f"Wrote: {args.out / 'trades.csv'}")
    print(f"Wrote: {args.out / 'position.csv'}")
    print(f"Wrote: {args.out / 'balance.csv'}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

主程序运行后生成3张核心表,字段已做统一标准化,具体如下:

  1. output/trades.csv(逐笔交易表):trade_id, contract, open_ts, open_px, open_qty, multiplier, fee_open, close_ts, close_px, close_qty, fee_close, pnl_ticks

样例数据:

trade_id,contract,open_ts,open_px,open_qty,multiplier,fee_open,close_ts,close_px,close_qty,fee_close,pnl_ticks
0,cf2309,2023-04-06 21:00:00,14685.0,1.0,5.0,4.3,2023-04-06 21:25:00,14770.0,-1.0,4.3,17.0
1,cf2309,2023-04-06 21:00:00,14695.0,1.0,5.0,4.3,2023-04-06 21:25:00,14775.0,-1.0,4.3,16.0
  1. output/position.csv(逐日持仓表):biz_date, contract, direction, cost_px, settle_px, position_qty
  2. output/balance.csv(逐日权益表):biz_date, carry_pnl, realized_pnl, txn_cost, daily_pnl, exposure, cum_pnl_amt, equity, daily_ret, nav

样例数据:

biz_date,carry_pnl,realized_pnl,txn_cost,daily_pnl,exposure,cum_pnl_amt,equity,daily_ret,nav
2023-04-07,62.5,1275.0,124.7,1212.8,74512.5,1212.8,1001212.8,0.00121,1.001213
2023-04-10,-475.0,1250.0,60.2,714.8,74037.5,1927.6,1001927.6,0.00071,1.001928

新增后处理脚本,在不改动main.py原有计算口径的前提下,读取主程序输出的trades.csv与balance.csv,完成字段标准化映射与风险绩效汇总,计时仅覆盖指标计算过程,不含CSV读写,可直接在项目目录运行。

from __future__ import annotations

import time
from pathlib import Path

import pandas as pd


def _max_drawdown(net: pd.Series) -> tuple[float, str, str]:
    running_peak = net.cummax()
    drawdown = net / running_peak - 1.0
    end_idx = int(drawdown.idxmin())
    start_idx = int(net.loc[:end_idx].idxmax())
    return float(drawdown.iloc[end_idx]), str(start_idx), str(end_idx)


def main() -> None:
    root = Path(__file__).resolve().parents[1]
    out_dir = root / "output"
    balance_path = out_dir / "balance.csv"
    trades_path = out_dir / "trades.csv"

    balance = pd.read_csv(balance_path, encoding="utf-8-sig")
    trades = pd.read_csv(trades_path, encoding="utf-8-sig")

    t0 = time.perf_counter_ns()

    bal = balance.copy()
    bal["date"] = pd.to_datetime(bal["date"])
    bal["ret"] = bal["return"].astype(float)
    bal["net"] = bal["net"].astype(float)

    total_days = len(bal)
    positive_days = int((bal["total_pnl"] > 0).sum())
    win_day_ratio = positive_days / total_days if total_days else 0.0

    mdd, start_idx, end_idx = _max_drawdown(bal["net"])
    mdd_start = bal.iloc[int(start_idx)]["date"].strftime("%Y-%m-%d")
    mdd_end = bal.iloc[int(end_idx)]["date"].strftime("%Y-%m-%d")

    ann_factor = 244.0
    daily_ret_std = float(bal["ret"].std(ddof=0))
    annual_vol = daily_ret_std * (ann_factor**0.5)
    annual_ret = (float(bal["net"].iloc[-1]) ** (ann_factor / max(total_days, 1))) - 1.0
    sharpe = annual_ret / annual_vol if annual_vol > 0 else 0.0

    tr = trades.copy()
    tr["pnl"] = (
        (tr["Close_trade_price"].astype(float) - tr["Open_trade_price"].astype(float))
        * tr["Open_trade_num"].astype(float)
        * tr["size"].astype(float)
    )
    tr["cost"] = tr["open_commission"].astype(float) + tr["close_commission"].astype(
        float
    )
    tr["net_pnl"] = tr["pnl"] - tr["cost"]

    gross_profit = float(tr.loc[tr["net_pnl"] > 0, "net_pnl"].sum())
    gross_loss = float(-tr.loc[tr["net_pnl"] < 0, "net_pnl"].sum())
    profit_factor = gross_profit / gross_loss if gross_loss > 0 else 0.0

    risk_report = pd.DataFrame(
        [
            {"metric": "start_date", "value": bal["date"].min().strftime("%Y-%m-%d")},
            {"metric": "end_date", "value": bal["date"].max().strftime("%Y-%m-%d")},
            {"metric": "trading_days", "value": total_days},
            {"metric": "win_day_ratio", "value": round(win_day_ratio, 6)},
            {"metric": "max_drawdown", "value": round(mdd, 6)},
            {"metric": "max_drawdown_start", "value": mdd_start},
            {"metric": "max_drawdown_end", "value": mdd_end},
            {"metric": "annual_return", "value": round(annual_ret, 6)},
            {"metric": "annual_volatility", "value": round(annual_vol, 6)},
            {"metric": "sharpe", "value": round(sharpe, 6)},
            {"metric": "profit_factor", "value": round(profit_factor, 6)},
            {"metric": "final_net", "value": round(float(bal["net"].iloc[-1]), 6)},
        ]
    )

    by_symbol = tr.groupby("Ticker", as_index=False).agg(
        trades=("Ticker", "count"),
        net_pnl=("net_pnl", "sum"),
        avg_net_pnl=("net_pnl", "mean"),
        win_ratio=("net_pnl", lambda s: float((s > 0).mean())),
    )
    by_symbol = by_symbol.sort_values("net_pnl", ascending=False).reset_index(drop=True)

    t1 = time.perf_counter_ns()

    risk_report.to_csv(out_dir / "risk_report.csv", index=False, encoding="utf-8-sig")
    by_symbol.to_csv(out_dir / "symbol_stats.csv", index=False, encoding="utf-8-sig")

    elapsed_us = (t1 - t0) / 1000.0
    print(f"process_us={elapsed_us:.2f}")
    print(f"risk_metrics={len(risk_report)}")
    print(f"symbol_rows={len(by_symbol)}")


if __name__ == "__main__":
    main()

脚本运行过程中,将主程序输出映射为标准化输入表,便于统一统计:

(1)output/equity_curve.csv(由output/balance.csv映射):biz_date, carry_pnl, realized_pnl, txn_cost, daily_pnl, exposure, cum_pnl_amt, equity, daily_ret, nav

样例数据:

biz_date,carry_pnl,realized_pnl,txn_cost,daily_pnl,exposure,cum_pnl_amt,equity,daily_ret,nav
2023-04-07,62.5,1275.0,124.7,1212.8,74512.5,1212.8,1001212.8,0.00121,1.001213
2023-04-10,-475.0,1250.0,60.2,714.8,74037.5,1927.6,1001927.6,0.00071,1.001928

(2)output/deal_journal.csv(由output/trades.csv映射):contract, open_ts, open_px, open_qty, size, fee_open, close_ts, close_px, close_qty, fee_close

样例数据:

contract,open_ts,open_px,open_qty,size,fee_open,close_ts,close_px,close_qty,fee_close
cf2309,2023-04-06 21:00:00,14685.0,1.0,5.0,4.3,2023-04-06 21:25:00,14770.0,-1.0,4.3
cf2309,2023-04-06 21:00:00,14695.0,1.0,5.0,4.3,2023-04-06 21:25:00,14775.0,-1.0,4.3

脚本最终生成2张可直接用于报告的风险绩效统计表:

(1)output/risk_report.csv(组合层指标):包含start_date、end_date、trading_days、win_day_ratio、max_drawdown、max_drawdown_start、max_drawdown_end、annual_return、annual_volatility、sharpe、profit_factor、final_net共12项核心风险绩效指标。

实际结果:

metric,value
start_date,2023-04-07
end_date,2023-07-21
trading_days,71
win_day_ratio,0.577465
max_drawdown,-0.01914
max_drawdown_start,2023-05-31
max_drawdown_end,2023-06-08
annual_return,0.076585
annual_volatility,0.045569
sharpe,1.68065
profit_factor,1.448351
final_net,1.021705

(2)output/symbol_stats.csv(合约层指标):包含Ticker、trades、net_pnl、avg_net_pnl、win_ratio共5项合约维度统计指标。

实际结果:

Ticker,trades,net_pnl,avg_net_pnl,win_ratio
cf2309,886,21705.4,24.49819413092551,0.5981941309255079

运行说明与实际结果

  1. 运行命令

先运行主程序,再运行后处理脚本,命令如下:

python main.py
python task_assets/build_risk_report.py
  1. 实际运行输出

主程序运行后输出3张核心表的写入提示;后处理脚本运行后输出处理耗时、风险指标数量、合约统计行数,实际输出如下:

process_us=65365.90
risk_metrics=12
symbol_rows=1

基于当前全链路处理结果,可进一步优化完善,提升报告的完整性与解释力:

  1. 按周/月聚合风险指标,形成分层归因报告,清晰呈现不同周期的绩效表现;
  2. 增加Calmar比率、Sortino比率、连续回撤天数等风控指标,丰富风险评估维度;
  3. 将手续费拆分为交易所费、滑点估计等细分项,提升成本分析的精细化程度;
  4. 若开展多合约回测,建议按“策略-合约-方向”三层结构做绩效贡献度分析,明确各维度对整体收益的影响。