feat: add minute/tick level support for long format backtest#5
Conversation
- Migrate all date types from i32 to i64 millisecond timestamps - Add DateMode enum to preserve input type (Date/Datetime) in output - Add ResampleFreq::Interval variant for sub-daily resampling (H, nT, nS) - Support Datetime input with automatic type preservation - Support String date columns with auto-cast to Date - Add interval validation in Python API (_validate_resample) - Update crossed_interval_boundary for sub-daily boundary detection Breaking change: Internal date representation changed from i32 days to i64 ms Backward compatible: Date input still produces Date output
…resample validation - Update tracker.rs documentation: Date type is now i64 milliseconds, not i32 - Fix Python _INTERVAL_PATTERN regex to allow formats without number prefix (e.g., "H", "T", "S") to match Rust parse_interval behavior
PR #5 深度 Review: feat: add minute/tick level support for long format backtestPR 概要目標: 將 polars_backtest 的 Long Format API 從只支援日級 (daily) 擴展到支援任意時間粒度 (hourly/minute/tick) 變更統計: +979/-371 行, 6 個檔案 架構設計評估核心策略: 統一用 i64 毫秒時間戳優點:
設計亮點:
檔案層級 Review1.
|
🔍 深度可靠性 Review (Senior Engineer Perspective)本 review 從資深軟體工程師角度,以高可靠性標準檢視程式碼。 🚨 Critical IssuesIssue #7:
|
| 位置 | 程式碼 | 不變量 |
|---|---|---|
| long.rs:387 | current_timestamp.unwrap() |
在 is_some() 檢查後 |
| long.rs:657 | delayed_rebalances.pop_front().unwrap() |
在 front() 成功後 |
| long.rs:838 | ohlc_accessors.unwrap() |
在 is_some() 檢查後 |
| wide.rs:667, long.rs:2085 | snapshot.unwrap() |
is_continuing 保證存在 |
建議: 將 unwrap() 改為 expect("invariant: ...") 以便在失敗時提供更好的錯誤訊息。
Issue #11: 時區資訊遺失 (已在初版 review 提及)
DataType::Datetime(time_unit, _tz) => { ... }輸入的 timezone 被忽略,輸出 DataFrame 會丟失時區資訊。
影響: 對於帶時區的 Datetime 輸入,輸出會變成 naive datetime。
建議: 在 DateMode enum 中儲存時區:
enum DateMode {
Date,
DatetimeMicroseconds(Option<Arc<str>>), // Store timezone
DatetimeMilliseconds(Option<Arc<str>>),
DatetimeNanoseconds(Option<Arc<str>>),
}📊 Edge Case 測試建議
目前測試覆蓋良好,但建議新增以下 edge cases:
-
極端 interval 值
resample="1S"(每秒 rebalance)- 非常大的 interval (如
"86400H"= 10 年)
-
邊界時間戳
- Unix epoch (1970-01-01)
- Year 2038 問題邊界 (i32 overflow)
- 負時間戳 (1970 年之前)
-
空資料處理
- 有 rows 但全部 weight 都是 NaN
- 單一 timestamp 多 symbols 全無有效價格
-
Datetime 精度測試
- Nanosecond 精度輸入/輸出往返
- Microsecond vs Millisecond 精度混合
✅ 正面評價
防禦性程式設計亮點
- 價格驗證:
is_valid_price(price)檢查 NaN/Inf - 權重容差:
weight.abs() > FLOAT_EPSILON避免浮點精度問題 - Factor 驗證:
factor.is_finite() && factor > 0.0 - 空結果處理:
if n_rows == 0 { return (vec![], vec![]); }
程式碼品質
- 良好的註解解釋複雜邏輯 (如 delayed rebalance)
- 適當使用
#[inline]優化熱點函數 - 清晰的模組邊界和職責分離
📝 總結
| 類別 | 數量 | 狀態 |
|---|---|---|
| Critical | 3 | 建議修復 |
| Medium | 2 | 建議改進 |
| 文檔 | 2 | ✅ 已修復 |
整體評估: 程式碼品質良好,主要風險來自防禦性程式設計的缺失。Critical issues 在正常使用下不會觸發,但為了達到高可靠性標準,建議處理。
建議優先順序:
- Issue #7 (Division by zero) - 加入防護
- Issue #9 (empty timestamps) - 加入 early return
- Issue #10 (unwrap → expect) - 改善錯誤訊息
- Issue #8, #11 - 低優先,可作為 follow-up
- Issue #7: Add division-by-zero guard in crossed_interval_boundary() Returns false if interval_secs == 0 instead of panicking - Issue #8: Use saturating_mul in DateMode::from_ms() for overflow safety Prevents panic on extreme timestamps when converting to us/ns - Issue #9: Replace unwrap() with expect() for better error messages Clarifies invariant when accessing timestamps.last()
✅ Critical Issues 已修復Commit Issue #7: Division by Zero Guardfn crossed_interval_boundary(prev_ms: i64, curr_ms: i64, interval_secs: u32) -> bool {
if interval_secs == 0 {
return false; // 新增防護
}
let interval_ms = interval_secs as i64 * 1000;
prev_ms / interval_ms != curr_ms / interval_ms
}Issue #8: Overflow Safetyfn from_ms(&self, ms: i64) -> i64 {
match self {
DateMode::Date => ms / 86_400_000,
DateMode::DatetimeMicroseconds => ms.saturating_mul(1_000), // 改用 saturating_mul
DateMode::DatetimeMilliseconds => ms,
DateMode::DatetimeNanoseconds => ms.saturating_mul(1_000_000), // 改用 saturating_mul
}
}Issue #9: Better Error Messagelet last_price_timestamp = *timestamps.last()
.expect("invariant: timestamps is non-empty after is_empty() check");測試結果:
|
Summary
i32(days) toi64(milliseconds) for unified timestamp handlingDateModeenum to track input type (Date/Datetime) and preserve it in outputResampleFreq::Interval(u32)for sub-daily resampling supportH(hourly),nT(n minutes),nS(n seconds)Key Changes
btcore/src/tracker.rsbtcore/src/simulation/long.rspolars_backtest/src/lib.rspolars_backtest/src/ffi_convert.rspolars_backtest/python/.../namespace.pyBackward Compatibility
New Features
Test plan
just test-rust)just test)