Coverage for adrs/report/alpha.py: 0%
62 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 io
2import inspect
3import polars as pl
4from datetime import datetime, timedelta
5from pydantic import BaseModel, TypeAdapter
6from typing import Self, cast, Unpack, NotRequired, TypedDict
8from adrs import json_utils as json
9from adrs.alpha import Alpha
10from adrs.data import Datamap
11from adrs.types import Performance
12from adrs.performance import Evaluator
13from adrs.tests import Sensitivity, SensitivityParameter
15type Params = dict[str, float | int | timedelta]
18class SensitivitySharpeRatioSummary(BaseModel):
19 best_param: float
20 mean: float
21 median: float
22 std: float
23 min: float
24 max: float
25 p25: float
26 p75: float
27 num_negative: int
28 num_positive: int
29 total_permutations: int
30 score: float
32 @classmethod
33 def compute(
34 cls, performance: Performance, results: list[tuple[Params, Performance]]
35 ) -> Self:
36 df = pl.DataFrame(map(lambda t: t[1], results))
38 best_param = performance.sharpe_ratio
39 mean = cast(float, df["sharpe_ratio"].mean() or 0.0)
40 std = cast(float, df["sharpe_ratio"].std() or 0.0)
41 num_positive = df.filter(pl.col("sharpe_ratio") > 0).shape[0]
42 total_permutations = df.shape[0]
43 # fmt: off
44 score = (
45 ((1 / std) if std != 0 else 0) * 0.4
46 + ((mean / best_param) if best_param != 0 else 0) * 0.3
47 + ((num_positive / total_permutations) if total_permutations != 0 else 0) * 0.3
48 )
49 # fmt: on
51 return cls(
52 best_param=best_param,
53 mean=mean,
54 median=cast(float, df["sharpe_ratio"].median() or 0.0),
55 std=std,
56 min=cast(float, df["sharpe_ratio"].min() or 0.0),
57 max=cast(float, df["sharpe_ratio"].max() or 0.0),
58 p25=df["sharpe_ratio"].quantile(0.25) or 0.0,
59 p75=df["sharpe_ratio"].quantile(0.75) or 0.0,
60 num_negative=df.filter(pl.col("sharpe_ratio") < 0).shape[0],
61 num_positive=num_positive,
62 total_permutations=total_permutations,
63 score=score,
64 )
67class AlphaReportV1Performance(BaseModel):
68 performance: Performance
69 performance_df: pl.DataFrame
70 sensitivity: list[tuple[Params, Performance]] | None
71 sensitivity_sr_summary: SensitivitySharpeRatioSummary | None
73 class Config:
74 arbitrary_types_allowed = True
77class AlphaBacktestArgsWithoutDates(TypedDict):
78 evaluator: Evaluator
79 base_asset: str
80 datamap: Datamap
81 fees: float
82 data_df: NotRequired[pl.DataFrame]
83 price_shift: NotRequired[int]
84 output_columns: NotRequired[list[pl.Expr]]
87class AlphaReportV1(BaseModel):
88 alpha_id: str
89 params: Params
90 back: AlphaReportV1Performance
91 forward: AlphaReportV1Performance
92 sensitivity_params: dict[str, SensitivityParameter] | None
94 @staticmethod
95 def version() -> str:
96 return "alpha-report-v1"
98 @classmethod
99 def compute(
100 cls,
101 alpha: Alpha,
102 B_start: datetime,
103 B_end: datetime,
104 F_start: datetime,
105 F_end: datetime,
106 sensitivity: Sensitivity | None = None,
107 **kwargs: Unpack[AlphaBacktestArgsWithoutDates],
108 ) -> Self:
109 # get alpha params
110 params = {
111 name: getattr(alpha, name)
112 for name in inspect.signature(alpha.__init__).parameters.keys()
113 }
115 # run backtest
116 back, back_df = alpha.backtest(start_time=B_start, end_time=B_end, **kwargs)
117 back_sensitivity = (
118 [
119 (x[0], x[1])
120 for x in sensitivity.test(start_time=B_start, end_time=B_end, **kwargs)
121 ]
122 if sensitivity is not None
123 else None
124 )
126 # run forward test
127 forward, forward_df = alpha.backtest(
128 start_time=F_start, end_time=F_end, **kwargs
129 )
130 forward_sensitivity = (
131 [
132 (x[0], x[1])
133 for x in sensitivity.test(start_time=F_start, end_time=F_end, **kwargs)
134 ]
135 if sensitivity is not None
136 else None
137 )
139 return cls(
140 alpha_id=alpha.id,
141 params=params,
142 sensitivity_params=sensitivity.sensitivity_parameters
143 if sensitivity is not None
144 else None,
145 back=AlphaReportV1Performance(
146 performance=back,
147 performance_df=back_df,
148 sensitivity=back_sensitivity,
149 sensitivity_sr_summary=SensitivitySharpeRatioSummary.compute(
150 back, back_sensitivity
151 )
152 if back_sensitivity is not None
153 else None,
154 ),
155 forward=AlphaReportV1Performance(
156 performance=forward,
157 performance_df=forward_df,
158 sensitivity=forward_sensitivity,
159 sensitivity_sr_summary=SensitivitySharpeRatioSummary.compute(
160 forward, forward_sensitivity
161 )
162 if forward_sensitivity is not None
163 else None,
164 ),
165 )
167 def write_parquet(self, path: str):
168 """Write the report to a parquet file."""
169 with open(path, "wb+") as f:
170 f.write(self.serialize())
172 def serialize(self) -> bytes:
173 back_pdf_buf = io.BytesIO()
174 self.back.performance_df.write_parquet(back_pdf_buf)
175 forward_pdf_buf = io.BytesIO()
176 self.forward.performance_df.write_parquet(forward_pdf_buf)
178 buf = io.BytesIO()
179 df = pl.DataFrame(
180 {
181 "alpha_id": [self.alpha_id],
182 "params": [json.dumps(self.params)],
183 "sensitivity_params": [
184 TypeAdapter(dict[str, SensitivityParameter] | None).dump_json(
185 self.sensitivity_params
186 )
187 ],
188 "back": [self.back.model_dump_json(exclude={"performance_df"})],
189 "back_pdf_buf": [back_pdf_buf.getvalue()],
190 "forward": [self.forward.model_dump_json(exclude={"performance_df"})],
191 "forward_pdf_buf": [forward_pdf_buf.getvalue()],
192 }
193 )
194 df.write_parquet(buf)
196 return buf.getvalue()
198 @classmethod
199 def deserialize(cls, buf: bytes) -> Self:
200 df = pl.read_parquet(buf)
202 back_pdf = pl.read_parquet(df["back_pdf_buf"][0])
203 forward_pdf = pl.read_parquet(df["forward_pdf_buf"][0])
205 return cls(
206 alpha_id=df["alpha_id"][0],
207 params=json.loads(df["params"][0]),
208 sensitivity_params=TypeAdapter(
209 dict[str, SensitivityParameter] | None
210 ).validate_json(df["sensitivity_params"][0]),
211 back=AlphaReportV1Performance(
212 **json.loads(df["back"][0]),
213 performance_df=back_pdf,
214 ),
215 forward=AlphaReportV1Performance(
216 **json.loads(df["forward"][0]),
217 performance_df=forward_pdf,
218 ),
219 )
221 def __eq__(self, other: object) -> bool:
222 if not isinstance(other, AlphaReportV1):
223 return False
224 return (
225 self.alpha_id == other.alpha_id
226 and self.params == other.params
227 and self.sensitivity_params == other.sensitivity_params
228 and self.back.performance == other.back.performance
229 and self.back.sensitivity == other.back.sensitivity
230 and self.back.sensitivity_sr_summary == other.back.sensitivity_sr_summary
231 and self.back.performance_df.equals(other.back.performance_df)
232 and self.forward.performance == other.forward.performance
233 and self.forward.sensitivity == other.forward.sensitivity
234 and self.forward.sensitivity_sr_summary
235 == other.forward.sensitivity_sr_summary
236 and self.forward.performance_df.equals(other.forward.performance_df)
237 )