Files
2026-07-13 12:07:23 +08:00

286 lines
9.4 KiB
Python

from datetime import datetime
from enum import Enum
from numbers import Real
from typing import Union, cast
from collections.abc import Callable
import polars as pl
EXPRESSION_FUNCTIONS: dict[str, Callable] = {}
def register_functions(functions: list[Callable]) -> None:
"""Register custom expression functions by function name."""
for func in functions:
EXPRESSION_FUNCTIONS[func.__name__] = func
class DataProxy:
"""Feature data proxy"""
def __init__(self, df: pl.DataFrame) -> None:
"""Constructor"""
self.name: str = df.columns[-1]
self.df: pl.DataFrame = df.rename({self.name: "data"})
# Note that for numerical expressions, variables should be placed before numbers. e.g. a * 2
@staticmethod
def _as_series(value: object) -> pl.Series:
"""Normalize an operator result to a Polars series."""
if isinstance(value, pl.Series):
return value
return cast(pl.Series, value)
def _comparison_series(self, value: object) -> pl.Series:
"""Normalize comparison results to an Int32 series."""
if isinstance(value, pl.Series):
return value.cast(pl.Int32)
if isinstance(value, bool):
return pl.Series(name="data", values=[int(value)] * len(self.df))
if isinstance(value, Real):
return pl.Series(name="data", values=[int(bool(value))] * len(self.df))
raise TypeError(f"Unsupported comparison result type: {type(value)!r}")
def result(self, s: pl.Series) -> "DataProxy":
"""Convert series data to feature object"""
result: pl.DataFrame = self.df[["datetime", "vt_symbol"]]
result = result.with_columns(other=s)
return DataProxy(result)
def __add__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Addition operation"""
if isinstance(other, DataProxy):
s = self._as_series(self.df["data"] + other.df["data"])
else:
s = self._as_series(self.df["data"] + other)
return self.result(s)
def __radd__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Right addition operation"""
if isinstance(other, DataProxy):
s = self._as_series(other.df["data"] + self.df["data"])
else:
s = self._as_series(other + self.df["data"])
return self.result(s)
def __sub__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Subtraction operation"""
if isinstance(other, DataProxy):
s = self._as_series(self.df["data"] - other.df["data"])
else:
s = self._as_series(self.df["data"] - other)
return self.result(s)
def __rsub__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Right subtraction operation"""
if isinstance(other, DataProxy):
s = self._as_series(other.df["data"] - self.df["data"])
else:
s = self._as_series(other - self.df["data"])
return self.result(s)
def __mul__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Multiplication operation"""
if isinstance(other, DataProxy):
s = self._as_series(self.df["data"] * other.df["data"])
else:
s = self._as_series(self.df["data"] * other)
return self.result(s)
def __rmul__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Right multiplication operation"""
if isinstance(other, DataProxy):
s = self._as_series(self.df["data"] * other.df["data"])
else:
s = self._as_series(self.df["data"] * other)
return self.result(s)
def __truediv__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Division operation"""
if isinstance(other, DataProxy):
s = self._as_series(self.df["data"] / other.df["data"])
else:
s = self._as_series(self.df["data"] / other)
return self.result(s)
def __rtruediv__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Right division operation"""
if isinstance(other, DataProxy):
s = self._as_series(other.df["data"] / self.df["data"])
else:
s = self._as_series(other / self.df["data"])
return self.result(s)
def __floordiv__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Floor division operation"""
if isinstance(other, DataProxy):
s = self._as_series(self.df["data"] // other.df["data"])
else:
s = self._as_series(self.df["data"] // other)
return self.result(s)
def __mod__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Modulo operation"""
if isinstance(other, DataProxy):
s = self._as_series(self.df["data"] % other.df["data"])
else:
s = self._as_series(self.df["data"] % other)
return self.result(s)
def __pow__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Power operation"""
if isinstance(other, DataProxy):
s = self._as_series(self.df["data"].pow(other.df["data"]))
else:
s = self._as_series(self.df["data"].pow(cast(int | float, other)))
return self.result(s)
def __abs__(self) -> "DataProxy":
"""Get absolute value"""
s: pl.Series = self.df["data"].abs()
return self.result(s)
def __neg__(self) -> "DataProxy":
"""Negation operation"""
s: pl.Series = -self.df["data"]
return self.result(s)
def __gt__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Greater than comparison"""
if isinstance(other, DataProxy):
s: object = self.df["data"] > other.df["data"]
else:
s = self.df["data"] > other
return self.result(self._comparison_series(s))
def __ge__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Greater than or equal comparison"""
if isinstance(other, DataProxy):
s: object = self.df["data"] >= other.df["data"]
else:
s = self.df["data"] >= other
return self.result(self._comparison_series(s))
def __lt__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Less than comparison"""
if isinstance(other, DataProxy):
s: object = self.df["data"] < other.df["data"]
else:
s = self.df["data"] < other
return self.result(self._comparison_series(s))
def __le__(self, other: Union["DataProxy", Real]) -> "DataProxy":
"""Less than or equal comparison"""
if isinstance(other, DataProxy):
s: object = self.df["data"] <= other.df["data"]
else:
s = self.df["data"] <= other
return self.result(self._comparison_series(s))
def __eq__(self, other: Union["DataProxy", Real]) -> "DataProxy": # type: ignore[override]
"""Equal comparison"""
if isinstance(other, DataProxy):
s: object = self.df["data"] == other.df["data"]
else:
s = self.df["data"] == other
return self.result(self._comparison_series(s))
def __ne__(self, other: Union["DataProxy", Real]) -> "DataProxy": # type: ignore[override]
"""Not equal comparison"""
if isinstance(other, DataProxy):
s: object = self.df["data"] != other.df["data"]
else:
s = self.df["data"] != other
return self.result(self._comparison_series(s))
def calculate_by_expression(df: pl.DataFrame, expression: str) -> pl.DataFrame:
"""Execute calculation based on expression"""
# Import operators locally to avoid polluting global namespace
from .ts_function import ( # noqa
ts_delay,
ts_min, ts_max,
ts_argmax, ts_argmin,
ts_rank, ts_sum,
ts_mean, ts_std,
ts_slope, ts_quantile,
ts_rsquare, ts_resi,
ts_corr,
ts_less, ts_greater,
ts_log, ts_abs,
ts_delta, ts_cov,
ts_decay_linear,
ts_product
)
from .cs_function import ( # noqa
cs_rank,
cs_mean,
cs_std,
cs_sum,
cs_scale
)
from .ta_function import ( # noqa
ta_rsi,
ta_atr
)
from .math_function import ( # noqa
less, greater, log, abs,
sign, pow1, pow2,
quesval, quesval2
)
# Extract feature objects to local space
d: dict = locals()
d.update(EXPRESSION_FUNCTIONS)
for column in df.columns:
# Filter index columns
if column in {"datetime", "vt_symbol"}:
continue
# Cache feature df
column_df = df[["datetime", "vt_symbol", column]]
d[column] = DataProxy(column_df)
# Use eval to execute calculation
other: DataProxy = eval(expression, {}, d)
# Return result DataFrame
return other.df
def calculate_by_polars(df: pl.DataFrame, expression: pl.expr.expr.Expr) -> pl.DataFrame:
"""Execute calculation based on Polars expression"""
return df.select([
"datetime",
"vt_symbol",
expression.alias("data")
])
def to_datetime(arg: datetime | str) -> datetime:
"""Convert time data type"""
if isinstance(arg, str):
if "-" in arg:
fmt: str = "%Y-%m-%d"
else:
fmt = "%Y%m%d"
return datetime.strptime(arg, fmt)
else:
return arg
class Segment(Enum):
"""Data segment enumeration values"""
TRAIN = 1
VALID = 2
TEST = 3