306 lines
9.9 KiB
Python
306 lines
9.9 KiB
Python
import time
|
|
from datetime import datetime
|
|
from typing import cast
|
|
from collections.abc import Callable
|
|
from multiprocessing import get_context
|
|
from multiprocessing.context import BaseContext
|
|
|
|
import polars as pl
|
|
import pandas as pd
|
|
from tqdm import tqdm
|
|
from alphalens.utils import get_clean_factor_and_forward_returns # type: ignore
|
|
from alphalens.tears import create_full_tear_sheet # type: ignore
|
|
|
|
from ..logger import logger
|
|
from .utility import (
|
|
to_datetime,
|
|
Segment,
|
|
calculate_by_expression,
|
|
calculate_by_polars
|
|
)
|
|
|
|
|
|
class AlphaDataset:
|
|
"""Alpha dataset template class"""
|
|
|
|
def __init__(
|
|
self,
|
|
df: pl.DataFrame,
|
|
train_period: tuple[str, str],
|
|
valid_period: tuple[str, str],
|
|
test_period: tuple[str, str],
|
|
process_type: str = "append"
|
|
) -> None:
|
|
"""Constructor"""
|
|
self.df: pl.DataFrame = df
|
|
|
|
# DataFrames for processed data
|
|
self.result_df: pl.DataFrame
|
|
self.raw_df: pl.DataFrame
|
|
self.infer_df: pl.DataFrame
|
|
self.learn_df: pl.DataFrame
|
|
|
|
# New version
|
|
self.data_periods: dict[Segment, tuple[str, str]] = {
|
|
Segment.TRAIN: train_period,
|
|
Segment.VALID: valid_period,
|
|
Segment.TEST: test_period
|
|
}
|
|
|
|
self.feature_expressions: dict[str, str | pl.expr.expr.Expr] = {}
|
|
self.feature_results: dict[str, pl.DataFrame] = {}
|
|
self.label_expression: str = ""
|
|
|
|
self.process_type: str = process_type
|
|
self.infer_processors: list = []
|
|
self.learn_processors: list = []
|
|
|
|
def add_feature(
|
|
self,
|
|
name: str,
|
|
expression: str | pl.expr.expr.Expr | None = None,
|
|
result: pl.DataFrame | None = None
|
|
) -> None:
|
|
"""
|
|
Add a feature expression
|
|
"""
|
|
if expression is not None and result is not None:
|
|
raise ValueError("Only one of 'expression' or 'result' can be provided")
|
|
|
|
if expression is not None:
|
|
self.feature_expressions[name] = expression
|
|
elif result is not None:
|
|
self.feature_results[name] = result
|
|
|
|
def set_label(self, expression: str) -> None:
|
|
"""
|
|
Set the label expression
|
|
"""
|
|
self.label_expression = expression
|
|
|
|
def add_processor(self, task: str, processor: Callable[[pl.DataFrame], None]) -> None:
|
|
"""
|
|
Add a feature preprocessor
|
|
"""
|
|
if task == "infer":
|
|
self.infer_processors.append(processor)
|
|
else:
|
|
self.learn_processors.append(processor)
|
|
|
|
def prepare_data(self, filters: dict | None = None, max_workers: int | None = None) -> None:
|
|
"""
|
|
Generate required data
|
|
"""
|
|
# List for feature data results
|
|
results: list = []
|
|
|
|
# Iterate through expressions for calculation
|
|
expressions: list[tuple[str, str | pl.expr.expr.Expr]] = list(self.feature_expressions.items())
|
|
|
|
if self.label_expression:
|
|
expressions.append(("label", self.label_expression))
|
|
|
|
# Create process pool
|
|
logger.info("开始计算表达式因子特征")
|
|
|
|
args: list[tuple] = [(self.df, name, expression) for name, expression in expressions]
|
|
|
|
context: BaseContext = get_context("spawn")
|
|
|
|
with context.Pool(processes=max_workers) as pool:
|
|
# Calculate all expressions in parallel
|
|
it = pool.imap(calculate_feature, args)
|
|
|
|
# Collect results
|
|
for result in tqdm(it, total=len(args)):
|
|
results.append(result)
|
|
|
|
self.result_df = self.df.with_columns(results)
|
|
|
|
# Merge result data factor features
|
|
logger.info("开始合并结果数据因子特征")
|
|
|
|
label_exist: bool = "label" in self.result_df
|
|
for name, feature_result in tqdm(self.feature_results.items()):
|
|
feature_result = feature_result.rename({"data": name})
|
|
self.result_df = self.result_df.join(feature_result, on=["datetime", "vt_symbol"], how="left")
|
|
|
|
if label_exist:
|
|
# Put label at the last column
|
|
cols: list = [col for col in self.result_df.columns if col != "label"] + ["label"]
|
|
self.result_df = self.result_df.select(cols).sort(["datetime", "vt_symbol"])
|
|
|
|
# Generate raw data
|
|
raw_df = self.result_df.fill_null(float("nan"))
|
|
|
|
if filters:
|
|
logger.info("开始筛选成分股数据")
|
|
|
|
dfs: list[pl.DataFrame] = []
|
|
|
|
for vt_symbol, ranges in tqdm(filters.items(), total=len(filters)):
|
|
for start, end in ranges:
|
|
temp_df = raw_df.filter(
|
|
(pl.col("vt_symbol") == vt_symbol)
|
|
& (pl.col("datetime") >= pl.lit(start))
|
|
& (pl.col("datetime") <= pl.lit(end))
|
|
)
|
|
dfs.append(temp_df)
|
|
|
|
raw_df = pl.concat(dfs)
|
|
|
|
# Only keep feature columns
|
|
select_columns: list[str] = ["datetime", "vt_symbol"] + raw_df.columns[self.df.width:]
|
|
self.raw_df = raw_df.select(select_columns).sort(["datetime", "vt_symbol"])
|
|
|
|
self.infer_df = self.raw_df
|
|
self.learn_df = self.raw_df
|
|
|
|
def process_data(self) -> None:
|
|
"""
|
|
Process data
|
|
"""
|
|
# Generate inference data
|
|
for processor in self.infer_processors:
|
|
self.infer_df = processor(df=self.infer_df)
|
|
|
|
# Generate learning data
|
|
if self.process_type == "append":
|
|
self.learn_df = self.infer_df
|
|
|
|
for processor in self.learn_processors:
|
|
self.learn_df = processor(df=self.learn_df)
|
|
|
|
def fetch_raw(self, segment: Segment) -> pl.DataFrame:
|
|
"""
|
|
Get raw data for a specific segment
|
|
"""
|
|
start, end = self.data_periods[segment]
|
|
return query_by_time(self.raw_df, start, end)
|
|
|
|
def fetch_infer(self, segment: Segment) -> pl.DataFrame:
|
|
"""
|
|
Get inference data for a specific segment
|
|
"""
|
|
start, end = self.data_periods[segment]
|
|
return query_by_time(self.infer_df, start, end)
|
|
|
|
def fetch_learn(self, segment: Segment) -> pl.DataFrame:
|
|
"""
|
|
Get learning data for a specific segment
|
|
"""
|
|
start, end = self.data_periods[segment]
|
|
return query_by_time(self.learn_df, start, end)
|
|
|
|
def show_feature_performance(self, name: str) -> None:
|
|
"""
|
|
Perform performance analysis for a feature
|
|
"""
|
|
starts: list[datetime] = []
|
|
ends: list[datetime] = []
|
|
|
|
for period in self.data_periods.values():
|
|
starts.append(to_datetime(period[0]))
|
|
ends.append(to_datetime(period[1]))
|
|
|
|
start: datetime = min(starts)
|
|
end: datetime = max(ends)
|
|
|
|
# Select range
|
|
result_df: pl.DataFrame = query_by_time(self.result_df, start, end)
|
|
learn_df: pl.DataFrame = query_by_time(self.learn_df, start, end)
|
|
|
|
merged_df = (
|
|
result_df
|
|
.select(["datetime", "vt_symbol", "close"])
|
|
.join(
|
|
learn_df.select(["datetime", "vt_symbol", name]),
|
|
on=["datetime", "vt_symbol"],
|
|
how="inner"
|
|
)
|
|
)
|
|
|
|
# Fill NaN and drop nulls
|
|
merged_df = merged_df.fill_nan(None).drop_nulls()
|
|
|
|
# Extract feature
|
|
feature_df: pd.DataFrame = merged_df.select(["datetime", "vt_symbol", name]).to_pandas()
|
|
feature_df.set_index(["datetime", "vt_symbol"], inplace=True)
|
|
|
|
feature_s: pd.Series = feature_df[name]
|
|
|
|
# Extract price
|
|
price_df: pd.DataFrame = merged_df.select(["datetime", "vt_symbol", "close"]).to_pandas()
|
|
price_df = price_df.pivot(index="datetime", columns="vt_symbol", values="close")
|
|
|
|
# Merge data
|
|
clean_data: pd.DataFrame = get_clean_factor_and_forward_returns(feature_s, price_df, quantiles=10)
|
|
|
|
# Perform analysis
|
|
create_full_tear_sheet(clean_data)
|
|
|
|
def show_signal_performance(self, signal: pl.DataFrame) -> None:
|
|
"""
|
|
Perform performance analysis for prediction signals
|
|
"""
|
|
# Get signal start and end times
|
|
start: datetime = cast(datetime, signal["datetime"].min())
|
|
end: datetime = cast(datetime, signal["datetime"].max())
|
|
|
|
# Select range
|
|
df: pl.DataFrame = query_by_time(self.result_df, start, end)
|
|
|
|
# Extract feature
|
|
signal_df: pd.DataFrame = signal.to_pandas()
|
|
signal_df.set_index(["datetime", "vt_symbol"], inplace=True)
|
|
signal_s: pd.Series = signal_df["signal"]
|
|
|
|
# Extract price
|
|
price_df: pd.DataFrame = df.select(["datetime", "vt_symbol", "close"]).to_pandas()
|
|
price_df = price_df.pivot(index="datetime", columns="vt_symbol", values="close")
|
|
|
|
# Merge data
|
|
clean_data: pd.DataFrame = get_clean_factor_and_forward_returns(
|
|
signal_s,
|
|
price_df,
|
|
max_loss=1.0,
|
|
quantiles=10
|
|
)
|
|
|
|
# Perform analysis
|
|
create_full_tear_sheet(clean_data)
|
|
|
|
|
|
def query_by_time(df: pl.DataFrame, start: datetime | str = "", end: datetime | str = "") -> pl.DataFrame:
|
|
"""
|
|
Filter DataFrame based on time range
|
|
"""
|
|
if start:
|
|
start = to_datetime(start)
|
|
df = df.filter(pl.col("datetime") >= start)
|
|
|
|
if end:
|
|
end = to_datetime(end)
|
|
df = df.filter(pl.col("datetime") <= end)
|
|
|
|
return df.sort(["datetime", "vt_symbol"])
|
|
|
|
|
|
def calculate_feature(args: tuple[pl.DataFrame, str, str | pl.expr.expr.Expr]) -> pl.Series:
|
|
"""
|
|
Calculate feature by expression
|
|
"""
|
|
start = time.time()
|
|
|
|
df, name, expression = args
|
|
|
|
if isinstance(expression, pl.expr.expr.Expr):
|
|
result = calculate_by_polars(df, expression)["data"].alias(name)
|
|
else:
|
|
result = calculate_by_expression(df, expression)["data"].alias(name)
|
|
|
|
end = time.time()
|
|
print(f"Feature calculation {name} took: {end - start} seconds | {expression}")
|
|
|
|
return result
|