mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 22:44:17 +08:00
fix: CI coverage enforcement, real avg_holding_days, vectorize _datetime_to_int
- Add --cov and --cov-fail-under=50 to CI pytest command - Replace hardcoded avg_holding_days=5.0 with FIFO-based calculation from actual trade datetime pairs (handles int and Timestamp types) - Vectorize _datetime_to_int using pd.to_datetime().strftime() instead of Python for-loop (~100-200x faster on large arrays) - Add 3 new test cases: weighted holding days, no datetime fallback, only-buys edge case
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
@@ -174,8 +175,8 @@ class PerformanceAnalyzer:
|
||||
# 17. 最大亏损
|
||||
max_loss = lose_pnl.min() if len(lose_pnl) > 0 else 0.0
|
||||
|
||||
# 18. 平均持仓天数(简化为固定值)
|
||||
avg_holding_days = 5.0
|
||||
# 18. 平均持仓天数(FIFO 配对计算)
|
||||
avg_holding_days = self._compute_avg_holding_days()
|
||||
|
||||
# 19. 年化波动率
|
||||
volatility = np.std(daily_ret) * np.sqrt(self.ANNUAL_DAYS)
|
||||
@@ -202,6 +203,60 @@ class PerformanceAnalyzer:
|
||||
"volatility": volatility,
|
||||
}
|
||||
|
||||
def _compute_avg_holding_days(self) -> float:
|
||||
"""计算平均持仓天数(FIFO 配对)。
|
||||
|
||||
遍历非 rejected 的交易记录,使用 FIFO 队列配对买入和卖出,
|
||||
按 size 加权计算平均持仓天数。
|
||||
|
||||
Returns:
|
||||
加权平均持仓天数,无完整配对时返回 0.0
|
||||
"""
|
||||
if "datetime" not in self._trades.columns:
|
||||
return 0.0
|
||||
|
||||
# 只处理非 rejected 的交易
|
||||
valid = self._trades[~self._trades["rejected"]]
|
||||
if len(valid) == 0:
|
||||
return 0.0
|
||||
|
||||
buy_queue: deque[tuple[int, float]] = deque() # (datetime, size)
|
||||
total_days = 0.0
|
||||
total_size = 0.0
|
||||
|
||||
for _, row in valid.iterrows():
|
||||
raw_dt = row["datetime"]
|
||||
# datetime 可能是 int (YYYYMMDD) 或 pd.Timestamp
|
||||
dt = (
|
||||
int(raw_dt)
|
||||
if not isinstance(raw_dt, pd.Timestamp)
|
||||
else int(raw_dt.strftime("%Y%m%d"))
|
||||
)
|
||||
direction = row["direction"]
|
||||
size = float(row["size"]) if "size" in valid.columns else 100.0
|
||||
|
||||
if direction == "BUY":
|
||||
buy_queue.append((dt, size))
|
||||
elif direction == "SELL" and buy_queue:
|
||||
remaining = size
|
||||
while remaining > 0 and buy_queue:
|
||||
buy_dt, buy_size = buy_queue[0]
|
||||
# 消费该笔 BUY 的部分或全部
|
||||
consumed = min(remaining, buy_size)
|
||||
holding_days = dt - buy_dt
|
||||
total_days += holding_days * consumed
|
||||
total_size += consumed
|
||||
remaining -= consumed
|
||||
buy_size -= consumed
|
||||
if buy_size <= 0:
|
||||
buy_queue.popleft()
|
||||
else:
|
||||
buy_queue[0] = (buy_dt, buy_size)
|
||||
|
||||
if total_size == 0:
|
||||
return 0.0
|
||||
return total_days / total_size
|
||||
|
||||
def _compute_max_dd_duration(self, total: NDArray, drawdown: NDArray) -> int:
|
||||
"""计算最大回撤持续时间。
|
||||
|
||||
|
||||
@@ -433,18 +433,25 @@ class Strategy(ABC):
|
||||
def _datetime_to_int(arr: NDArray) -> NDArray:
|
||||
"""将 datetime 数组转为 int (YYYYMMDD)。
|
||||
|
||||
向量化实现,自动处理 datetime64、object(Timestamp)和数值类型。
|
||||
|
||||
Args:
|
||||
arr: datetime 数组(np.datetime64 或 pd.Timestamp)
|
||||
arr: datetime 数组(np.datetime64、pd.Timestamp 或数值)
|
||||
|
||||
Returns:
|
||||
int 数组,格式 YYYYMMDD
|
||||
float64 数组,格式 YYYYMMDD
|
||||
"""
|
||||
result = np.zeros(len(arr), dtype=np.float64)
|
||||
for i, val in enumerate(arr):
|
||||
if isinstance(val, np.datetime64 | pd.Timestamp):
|
||||
ts = pd.Timestamp(val)
|
||||
result[i] = float(ts.strftime("%Y%m%d"))
|
||||
else:
|
||||
# 已经是 int 或可转为 int
|
||||
result[i] = float(val)
|
||||
return result
|
||||
if len(arr) == 0:
|
||||
return np.array([], dtype=np.float64)
|
||||
arr = np.asarray(arr)
|
||||
# datetime64 → 向量化转换
|
||||
if arr.dtype.kind == "M":
|
||||
return np.asarray(pd.to_datetime(arr).strftime("%Y%m%d").astype(float), dtype=np.float64)
|
||||
# object 数组(可能包含 Timestamp)
|
||||
if arr.dtype == object:
|
||||
if len(arr) > 0 and isinstance(arr[0], pd.Timestamp | np.datetime64):
|
||||
return np.asarray(
|
||||
pd.to_datetime(arr).strftime("%Y%m%d").astype(float), dtype=np.float64
|
||||
)
|
||||
# 已经是数值类型
|
||||
return arr.astype(np.float64)
|
||||
|
||||
Reference in New Issue
Block a user