from __future__ import annotations
import numpy as np
import pandas as pd
from wbt import WeightBacktest
def _uf_find(parent: list[int], x: int) -> int:
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def _uf_union(parent: list[int], a: int, b: int) -> None:
ra, rb = _uf_find(parent, a), _uf_find(parent, b)
if ra != rb:
parent[ra] = rb
def _ref_merge(agg: pd.DataFrame) -> list[tuple]:
segs: list[tuple] = []
for (sym, direction), grp in agg.groupby(["symbol", "交易方向"]):
g = grp.reset_index(drop=True)
n = len(g)
parent = list(range(n))
o_seen: dict = {}
c_seen: dict = {}
for i in range(n):
o, c = g.loc[i, "开仓时间"], g.loc[i, "平仓时间"]
if o in o_seen:
_uf_union(parent, i, o_seen[o])
else:
o_seen[o] = i
if c in c_seen:
_uf_union(parent, i, c_seen[c])
else:
c_seen[c] = i
comp: dict[int, list[int]] = {}
for i in range(n):
comp.setdefault(_uf_find(parent, i), []).append(i)
for members in comp.values():
rep = min(members, key=lambda i: (-int(g.loc[i, "持仓K线数"]), g.loc[i, "开仓时间"]))
cnt = sum(int(g.loc[i, "count"]) for i in members)
segs.append(
(
sym,
direction,
g.loc[rep, "开仓时间"],
g.loc[rep, "平仓时间"],
round(float(g.loc[rep, "盈亏比例"]), 4),
int(g.loc[rep, "持仓K线数"]),
cnt,
)
)
return sorted(segs)
def _kt_segs(kt: pd.DataFrame) -> list[tuple]:
segs = {
(
r["symbol"],
r["交易方向"],
r["开仓时间"],
r["平仓时间"],
round(float(r["盈亏比例"]), 4),
int(r["持仓K线数"]),
int(r["count"]),
)
for _, r in kt.iterrows()
}
return sorted(segs)
def _ref_board(segs: list[tuple], top: int) -> dict:
by_year: dict[int, list[tuple]] = {}
for s in segs:
by_year.setdefault(pd.Timestamp(s[3]).year, []).append(s)
out: dict = {}
for y, items in by_year.items():
best = sorted(items, key=lambda s: (-s[4], s[2]))[:top]
best_set = set(best)
worst = [s for s in sorted(items, key=lambda s: (s[4], s[2])) if s not in best_set][:top]
if best:
out[(y, "best")] = sorted(round(s[4], 4) for s in best)
if worst:
out[(y, "worst")] = sorted(round(s[4], 4) for s in worst)
return out
def _kt_board(kt: pd.DataFrame) -> dict:
out: dict = {}
for (y, kind), g in kt.groupby(["year", "kind"]):
out[(int(y), kind)] = sorted(round(float(x), 4) for x in g["盈亏比例"])
return out
def _make_bt(rng: np.random.Generator) -> WeightBacktest:
rows = []
for sym in ["AAA", "BBB", "CCC"][: rng.integers(1, 4)]:
year = int(rng.choice([2022, 2023, 2024]))
for dd in range(int(rng.integers(6, 40))):
w = round(float(rng.choice([-0.5, -0.25, 0.0, 0.0, 0.25, 0.5])), 2)
rows.append(
{
"dt": f"{year}-{1 + dd // 28:02d}-{1 + dd % 28:02d} 09:30:00",
"symbol": sym,
"weight": w,
"price": round(100.0 + float(rng.normal(0, 3)), 4),
}
)
return WeightBacktest(pd.DataFrame(rows), digits=2, n_jobs=1)
def test_key_trades_merge_matches_reference() -> None:
rng = np.random.default_rng(20240618)
checked = 0
for _ in range(60):
bt = _make_bt(rng)
agg = bt.aggregated_pairs
if agg.empty:
continue
checked += 1
top = int(rng.integers(1, 5))
ref_segs = _ref_merge(agg)
assert ref_segs == _kt_segs(bt.key_trades(top=10**9)), "合并段集合与参照不一致"
assert _ref_board(ref_segs, top) == _kt_board(bt.key_trades(top=top)), "年度选榜与参照不一致"
assert checked >= 15, f"有效随机场景过少({checked})"
def test_key_trades_dedups_lifo_split() -> None:
rows = []
for i, w in enumerate([0.5, 0.5, 0.25, 0.0]):
rows.append({"dt": f"2024-03-{i + 1:02d} 09:30:00", "symbol": "AAA", "weight": w, "price": 100.0 + i})
bt = WeightBacktest(pd.DataFrame(rows), digits=2, n_jobs=1)
agg = bt.aggregated_pairs
kt = bt.key_trades(top=10)
seg = kt.drop(columns=["year", "kind"]).drop_duplicates()
assert len(seg) <= len(agg)
for _, gp in seg.groupby(["symbol", "交易方向"]):
assert gp["开仓时间"].is_unique
assert gp["平仓时间"].is_unique