Fix portfolio construction NaN handling

This commit is contained in:
Yuxuan Yan
2026-06-10 14:58:06 +08:00
parent 94ab679a75
commit 98a4f99300
7 changed files with 109 additions and 20 deletions
+78
View File
@@ -1,6 +1,7 @@
"""Tests for the portfolio construction & execution phase (no network)."""
import datetime as dt
import logging
import numpy as np
import pandas as pd
@@ -150,6 +151,10 @@ def test_continuous_targets_guards_bad_price():
alpha = np.array([1.0, -1.0])
w, v, q = continuous_targets(alpha, np.array([np.nan, 10.0]), 1e6)
assert w[0] == 0.0 and q[0] == 0.0
assert np.isclose(np.abs(w).sum(), 1.0)
assert np.isclose(w[1], -1.0)
assert np.isclose(v[1], -1e6)
assert np.isclose(q[1], -100000.0)
# --- round_to_valid_lot (state-dependent) ------------------------------------
@@ -232,6 +237,25 @@ def test_repair_drives_net_and_gross():
assert abs(gross - B) <= 0.02 * B + price.max() * 1
def test_repair_ignores_nan_price_exposure():
q_round = np.array([101, -99, 0], dtype=np.int64)
q_target = q_round.astype(float)
price = np.array([10.0, 10.0, np.nan])
inc = np.ones(3, dtype=np.int64)
min_open = np.ones(3, dtype=np.int64)
prev = np.zeros(3, dtype=np.int64)
pos = repair_exposure(q_round, q_target, price, inc, min_open, prev,
booksize=2000.0, net_tol=0.0, gross_tol=0.0)
safe_price = np.nan_to_num(price, nan=0.0)
gross, net = _gross_net(pos, safe_price)
assert np.isfinite(gross)
assert np.isfinite(net)
assert abs(net) <= 10.0
assert not np.array_equal(pos, q_round)
def test_repair_does_not_worsen_tracking_error_grossly():
rng = np.random.default_rng(2)
n = 150
@@ -296,6 +320,30 @@ def test_construct_positions_threads_state_and_closes_absent():
assert final.empty or (final["position_shares"] == 0).all()
def test_construct_positions_carries_book_on_zero_gross(caplog):
dates = pd.to_datetime(["2024-01-01", "2024-01-02"])
symbols = ["sh600000", "sz000001"]
data = pd.DataFrame([
{"symbol_id": sym, "date": d, "close": 10.0, "isST": 0}
for sym in symbols for d in dates
])
weights = pd.DataFrame({
"symbol_id": ["sh600000", "sz000001", "sh600000", "sz000001"],
"date": [dates[0], dates[0], dates[1], dates[1]],
"combo_name": ["combo"] * 4,
"weight": [1.0, -1.0, 0.0, 0.0],
})
caplog.set_level(logging.WARNING)
pos = construct_positions(weights, data, booksize=10000.0, portfolio_name="run1")
shares = pos.pivot_table(index="date", columns="symbol_id",
values="position_shares", aggfunc="first")
assert shares.loc[dates[1], "sh600000"] == shares.loc[dates[0], "sh600000"]
assert shares.loc[dates[1], "sz000001"] == shares.loc[dates[0], "sz000001"]
assert "zero-gross target" in caplog.text
# --- constraints -------------------------------------------------------------
def _slice(n, **over):
@@ -358,6 +406,36 @@ def test_simulator_next_open_and_blocked_buy_holds_prev():
assert (fills["realized_shares"] == fills["prev_shares"] + fills["traded_shares"]).all()
def test_simulator_uses_constructed_position_shares_not_continuous_targets():
positions = pd.DataFrame({
"symbol_id": ["sh600000"],
"date": pd.to_datetime(["2024-01-01"]),
"portfolio_name": ["run1"],
"target_weight": [1.0],
"target_value": [1534.0],
"target_shares": [153.4],
"position_shares": [100],
"position_value": [1000.0],
"price": [10.0],
})
data = pd.DataFrame({
"symbol_id": ["sh600000", "sh600000"],
"date": pd.to_datetime(["2024-01-01", "2024-01-02"]),
"open": [10.0, 10.0],
"close": [10.0, 10.0],
"preclose": [10.0, 10.0],
"amount": [1e9, 1e9],
"tradestatus": [1, 1],
"isST": [0, 0],
})
fills, _ = ReferenceSimulator().run(positions, data)
assert fills["target_shares"].iloc[0] == 100
assert fills["traded_shares"].iloc[0] == 100
assert fills["realized_shares"].iloc[0] == 100
def test_simulator_blocked_buy_when_suspended():
n = 1
sim = ReferenceSimulator(constraints=[SuspensionConstraint()])