Skip to content

Commit a68a53b

Browse files
authored
Add shared history frame normalizer (#38)
1 parent 31dfce9 commit a68a53b

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from __future__ import annotations
2+
3+
import pandas as pd
4+
5+
6+
def normalize_history_frame(history, *, label: str, close_column: str = "close") -> pd.DataFrame:
7+
if isinstance(history, pd.DataFrame):
8+
frame = history.copy()
9+
elif isinstance(history, pd.Series):
10+
frame = history.to_frame(name=close_column)
11+
else:
12+
frame = pd.DataFrame(list(history))
13+
14+
if frame.empty:
15+
raise ValueError(f"{label} must contain close history")
16+
17+
normalized_close = str(close_column or "close").strip() or "close"
18+
lower_columns = {str(column).strip().lower(): column for column in frame.columns}
19+
if normalized_close not in frame.columns:
20+
close_match = lower_columns.get(normalized_close.lower())
21+
if close_match is not None:
22+
frame = frame.rename(columns={close_match: normalized_close})
23+
elif len(frame.columns) == 1:
24+
frame = frame.rename(columns={frame.columns[0]: normalized_close})
25+
else:
26+
columns = ", ".join(str(column) for column in frame.columns)
27+
raise ValueError(f"{label} must include a {normalized_close} column; got columns: {columns or '<none>'}")
28+
29+
frame[normalized_close] = pd.to_numeric(frame[normalized_close], errors="coerce")
30+
frame = frame.dropna(subset=[normalized_close]).reset_index(drop=True)
31+
if frame.empty:
32+
raise ValueError(f"{label} close history is empty after normalization")
33+
return frame

tests/test_common_history.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
import unittest
4+
5+
import pandas as pd
6+
7+
from quant_platform_kit.common.history import normalize_history_frame
8+
9+
10+
class CommonHistoryTests(unittest.TestCase):
11+
def test_normalize_history_frame_accepts_dataframe_with_close_column(self) -> None:
12+
frame = pd.DataFrame([{"close": 1.0}, {"close": 2.0}])
13+
14+
normalized = normalize_history_frame(frame, label="benchmark_history")
15+
16+
self.assertEqual(list(normalized["close"]), [1.0, 2.0])
17+
18+
def test_normalize_history_frame_accepts_dataframe_with_close_case_variant(self) -> None:
19+
frame = pd.DataFrame([{"Close": 1.0}, {"Close": 2.0}])
20+
21+
normalized = normalize_history_frame(frame, label="benchmark_history")
22+
23+
self.assertEqual(list(normalized["close"]), [1.0, 2.0])
24+
25+
def test_normalize_history_frame_accepts_series(self) -> None:
26+
series = pd.Series([1.0, 2.0, 3.0], name="close")
27+
28+
normalized = normalize_history_frame(series, label="benchmark_history")
29+
30+
self.assertEqual(list(normalized["close"]), [1.0, 2.0, 3.0])
31+
32+
def test_normalize_history_frame_accepts_single_column_iterable(self) -> None:
33+
history = [1.0, 2.0, 3.0]
34+
35+
normalized = normalize_history_frame(history, label="benchmark_history")
36+
37+
self.assertEqual(list(normalized["close"]), [1.0, 2.0, 3.0])
38+
39+
def test_normalize_history_frame_rejects_missing_close_data(self) -> None:
40+
with self.assertRaisesRegex(ValueError, "benchmark_history must include a close column"):
41+
normalize_history_frame([{"open": 1.0, "high": 1.0}], label="benchmark_history")
42+
43+
44+
if __name__ == "__main__":
45+
unittest.main()

0 commit comments

Comments
 (0)