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
+18 -13
View File
@@ -47,18 +47,16 @@ def continuous_targets(
"""
alpha = np.asarray(alpha, dtype=np.float64)
price = np.asarray(price, dtype=np.float64)
a = np.where(np.isfinite(alpha), alpha, 0.0)
tradable = np.isfinite(price) & (price > 0)
a = np.where(np.isfinite(alpha) & tradable, alpha, 0.0)
gross = np.abs(a).sum()
if gross <= 0:
zeros = np.zeros_like(a)
return zeros, zeros.copy(), zeros.copy()
w = a / gross
v_target = booksize * w
tradable = np.isfinite(price) & (price > 0)
q_target = np.where(tradable, v_target / np.where(tradable, price, 1.0), 0.0)
# Names without a tradable price get no target exposure.
w = np.where(tradable, w, 0.0)
v_target = np.where(tradable, v_target, 0.0)
q_target = np.zeros_like(v_target)
np.divide(v_target, price, out=q_target, where=tradable)
return w, v_target, q_target
@@ -86,7 +84,7 @@ def construct_positions(
vector. Each date: continuous targets → state-dependent lot rounding →
two-stage exposure repair. Names absent on a date get weight 0 (which closes
any stale holding). An empty / zero-gross cross-section carries the book
unchanged.
unchanged in ``position_shares`` while leaving the target fields at 0.
Args:
weights_df: Long frame with ``symbol_id, date, weight`` (ALPHA/COMBO).
@@ -136,13 +134,20 @@ def construct_positions(
)
w, v_target, q_target = continuous_targets(alpha, price, booksize)
q_round = round_to_valid_lot(q_target, prev_shares, min_open, increment, odd_full)
pos = repair_exposure(
q_round, q_target, price, increment, min_open, prev_shares, odd_full,
booksize=booksize, net_tol=net_tol, gross_tol=gross_tol,
)
if np.abs(w).sum() <= 0:
logger.warning(
"Portfolio '%s': zero-gross target on %s; carrying previous positions.",
portfolio_name, d,
)
pos = prev_shares.copy()
else:
q_round = round_to_valid_lot(q_target, prev_shares, min_open, increment, odd_full)
pos = repair_exposure(
q_round, q_target, price, increment, min_open, prev_shares, odd_full,
booksize=booksize, net_tol=net_tol, gross_tol=gross_tol,
)
safe_price = np.where(np.isfinite(price), price, 0.0)
safe_price = np.where(np.isfinite(price) & (price > 0), price, 0.0)
blocks.append(pd.DataFrame({
"symbol_id": symbols,
"date": d,
+7 -4
View File
@@ -82,7 +82,8 @@ def round_to_valid_lot(
def _exposures(q: np.ndarray, price: np.ndarray) -> tuple[np.ndarray, float, float]:
v = q.astype(np.float64) * price
safe_price = np.where(np.isfinite(price) & (price > 0), price, 0.0)
v = q.astype(np.float64) * safe_price
return v, float(v.sum()), float(np.abs(v).sum())
@@ -129,7 +130,7 @@ def repair_exposure(
``int64`` repaired positions, length N.
"""
q = np.asarray(q_round, dtype=np.int64).copy()
price = np.asarray(price, dtype=np.float64)
raw_price = np.asarray(price, dtype=np.float64)
increment = np.asarray(increment, dtype=np.int64).astype(np.float64)
min_open = np.asarray(min_open, dtype=np.int64).astype(np.float64)
qt = np.asarray(q_target, dtype=np.float64)
@@ -137,8 +138,10 @@ def repair_exposure(
if n == 0:
return q
vt = np.where(np.isfinite(qt), qt, 0.0) * price # v_target, NaN-safe
tradable = np.isfinite(price) & (price > 0)
tradable = np.isfinite(raw_price) & (raw_price > 0)
price = np.where(tradable, raw_price, 0.0)
qt_safe = np.where(np.isfinite(qt), qt, 0.0)
vt = np.where(tradable, qt_safe * price, 0.0) # v_target, NaN-safe
step = np.where(tradable, increment * price, np.inf) # dollar per increment
if max_iters is None:
+2 -1
View File
@@ -24,7 +24,8 @@ def evaluate_portfolio(positions_df: pd.DataFrame, data_df: pd.DataFrame) -> dic
"""Evaluate target weights as a continuous research portfolio.
Args:
positions_df: POSITION_COLUMNS (uses ``target_weight``).
positions_df: POSITION_COLUMNS (uses ``target_weight``; zero-gross
construction carry dates remain flat in this research view).
data_df: DATA_COLUMNS (uses ``close`` for returns).
Returns:
+2 -2
View File
@@ -122,7 +122,7 @@ class ReferenceSimulator(ExecutionSimulator):
FILL_COLUMNS / PNL_COLUMNS.
Args:
positions_df: POSITION_COLUMNS (uses ``target_shares``).
positions_df: POSITION_COLUMNS (uses constructed ``position_shares``).
data_df: DATA_COLUMNS (open/close/preclose/amount/tradestatus/isST).
rule_engine: For per-name price-limit bands; default built if None.
@@ -145,7 +145,7 @@ class ReferenceSimulator(ExecutionSimulator):
return df.pivot_table(index="date", columns="symbol_id",
values=col, aggfunc="first").sort_index()
tgt = wide(positions_df, "target_shares")
tgt = wide(positions_df, "position_shares")
opn = wide(data_df, "open")
close = wide(data_df, "close")
preclose = wide(data_df, "preclose") if "preclose" in data_df.columns else close.shift(1)