Coverage for adrs/alpha.py: 29%
41 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-29 04:01 +0800
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-29 04:01 +0800
1import polars as pl
2from abc import abstractmethod
3from datetime import datetime, timedelta
4from typing import Any, TypedDict, Unpack, NotRequired, cast
6from adrs.types import Performance
7from adrs.performance import Evaluator
8from adrs.data import Datamap, DataInfo, DataProcessor
9from adrs.performance.metric import Ratio, Drawdown, Trade
12class AlphaBacktestArgs(TypedDict):
13 evaluator: Evaluator
14 base_asset: str
15 datamap: Datamap
16 start_time: datetime
17 end_time: datetime
18 fees: float
19 data_df: NotRequired[pl.DataFrame]
20 price_shift: NotRequired[int]
21 output_columns: NotRequired[list[pl.Expr]]
24class Alpha:
25 def __init__(
26 self,
27 id: str,
28 data_infos: list[DataInfo],
29 data_processor: DataProcessor,
30 ):
31 self.id = id
32 self.data_infos = data_infos
33 self.data_processor = data_processor
34 self.data_processor.data_infos = data_infos
36 @abstractmethod
37 def next(self, data_df: pl.DataFrame) -> pl.DataFrame:
38 raise NotImplementedError("Every alpha must implement its `next` method")
40 def backtest(
41 self, **kwargs: Unpack[AlphaBacktestArgs]
42 ) -> tuple[Performance, pl.DataFrame]:
43 evaluator = kwargs["evaluator"]
44 base_asset = kwargs["base_asset"]
45 datamap = kwargs["datamap"]
46 start_time = kwargs["start_time"]
47 end_time = kwargs["end_time"]
48 fees = kwargs["fees"]
49 data_df = kwargs.get("data_df", self.data_processor.process(datamap))
50 price_shift = kwargs.get("price_shift", 0)
51 output_columns = kwargs.get("output_columns", [pl.all()])
53 if data_df is None:
54 raise ValueError("data_df received is None")
56 df = self.next(data_df)
57 interval = cast(timedelta, df["start_time"].diff().last())
59 # Verify that the signal is valid
60 if "signal" not in df.schema.keys():
61 raise ValueError(
62 "`next()` method must return a DataFrame with the column 'signal'"
63 )
64 if not df.schema["signal"].is_numeric():
65 raise ValueError(
66 "DataFrame returned from `next()` must have a 'signal' column that is numeric"
67 )
68 if (
69 df.select("signal").min().to_numpy().ravel()[0] < -1
70 or df.select("signal").max().to_numpy().ravel()[0] > 1
71 ):
72 raise ValueError(
73 "DataFrame returned from `next()` must have a 'signal' column that is between [-1, 1]"
74 )
76 pdf = evaluator.eval(
77 signal_lf=df.lazy(),
78 base_asset=base_asset,
79 datamap=datamap,
80 start_time=start_time,
81 end_time=end_time,
82 fees=fees,
83 interval=interval,
84 price_shift=price_shift,
85 output_columns=output_columns,
86 ).collect(engine="in-memory")
88 # Compute the metrics
89 performance: dict[str, Any] = {
90 "start_time": start_time,
91 "end_time": end_time,
92 "resolution_detected": interval,
93 "offset_detected": evaluator.last_grid_phase,
94 "metadata": {},
95 }
96 for metric in [Ratio(interval=interval), Trade(), Drawdown(interval=interval)]:
97 result = metric.compute(pdf)
98 performance = {**performance, **result}
100 return Performance.model_validate(performance), pdf