From 2253d8450da2eaed8f5a79b4297193095fa2464a Mon Sep 17 00:00:00 2001 From: CYX22222003 Date: Sun, 29 Mar 2026 22:13:40 +0800 Subject: [PATCH 1/9] Add new alpha and alpha evaluation methods --- alpha/AutoRegression/ar_model.pth | Bin 0 -> 2285 bytes alpha/AutoRegression/autoregression.py | 115 ++++++++++++++++++++++++ alpha/AutoRegression/data_processing.py | 16 ++++ alpha/AutoregressionAlpha.py | 32 +++++++ alpha/interface.py | 18 +++- freqtrade | 2 +- tests/test_alpha.py | 59 +++++++++++- 7 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 alpha/AutoRegression/ar_model.pth create mode 100644 alpha/AutoRegression/autoregression.py create mode 100644 alpha/AutoRegression/data_processing.py create mode 100644 alpha/AutoregressionAlpha.py diff --git a/alpha/AutoRegression/ar_model.pth b/alpha/AutoRegression/ar_model.pth new file mode 100644 index 0000000000000000000000000000000000000000..8efc6fae08b944bb0efe3738055f4b6869f4fee3 GIT binary patch literal 2285 zcmbtWeM}o=9KQ0|6&Muu!RP=p%`8#6w)A7|-9XE7GoDJvM7t)YIoex$;b?i=YzlX;BgQzFxjE;E?hj*Pi1A}n;~(hU#CdNE?Om}N=bQX; z&Gmhr-}AoD@BO{!rm}M|Os&RN3@xU`=%}wf5@5J30h*@`9c>)xD#E<-aUjd@k8m90 z=h;X&rfG@>m?#t2&iZ)`z-pR-O7AI>s5v&w&{0D-!v;e<$nx`%s6S-zb@D71)A*uH zYbVPEd^{75MWVhgvj$}A^`3kQ7?WnwK?dX`AR3a3(EP5%OAW~NsH8bYkmt#kp8k$b zkWYDepx#Mgl#22WfmM{3QVjvk^C2XrfCq)mpvY5@Qq;=QF|ax$r3>YR5_Bd|9LlFu zJSd6N3<52s8U&>YiZULoX$EUMd%-%-Dv8F|&hRwa9k4#0qFnEh-eO=wFQ|wwpo@vd zq^$uuN)_|uB@=^4Q?u5eQM%pDJd-ng*&4w<(MVqieXPA+D`Mnr5vQo zs5D1zREFOsl=|JDzkCjDB#O+bb(l?-ddV0c&9T97I}_$fm-#B%q-ie4iyH zN0C`TDOR5n(E2RTL{PPnF6Uk3UEU6k<#~=l8V0w`Z$SQ_$gH6l+m_OReBWXuU5>jK zaAKqykM5d+pX6lYEh9T%?Zr9guVbaK^4u4KVX*=}#vKz+RlOz7_MgRx&Aah_K_|Ft z8m2!lDuJtq?}6nFBe-hll4JBht)sAIH+-eS0>|10#Gal;;?l<(iKiy|@fmHm^Mbtt zR)k+9j%<1w{y8%)`hUP-|DA(y|3IB^YyO1zYOojv8%A-Ge-l3=>?MMYPGa`-cA`nR zgZG`Ca!zDj#P^lm4`06hm3Z^_%fiXAH^c?k8u*=dNG$BFgKphsqOEUSJnXQ*`PnMs z%yELy-+NV@e5nDx{o0~%-e1Y zxcJePJ@~bqRRlRuKn&GBAkIzK5FcD`B#zz|#Mhts)Op_@C&j+^7iOk@`WBy@e#8-e z=bTty|IIY>&?ac}wam2bKMmJjJ`YD{lj(E&qOgsxeb)Wrle1 z#+WE8o#4GRYTrIKK)Px(t#tXsD4#~F&15oewVLZ}CWp;#w3;0jqt$A$TkK||-DckE zFxt%yyBVo0={s~-FYONsH>E;?lQ)mN`)Sc1im~DzN{7okSh)gvL^3-GPivUzEb=y_ zQ?O_yu_=}ny)T_e-b+dqM2F-=$sU%Dmse`B0>4hW=#oMb{U;qPFGVHzrc|9uu+;vQ zKFBMnM3MQ)Tp4(&cGJ;vhpQFn&*bR8uRS-Fm-pZjs4BZQ319v;S$>wq3kSn`P^XrL ZghO8n*znSX#4VN^|5uHvF*H9h_b*`eUaJ5A literal 0 HcmV?d00001 diff --git a/alpha/AutoRegression/autoregression.py b/alpha/AutoRegression/autoregression.py new file mode 100644 index 0000000..1d7fbad --- /dev/null +++ b/alpha/AutoRegression/autoregression.py @@ -0,0 +1,115 @@ +import torch +from torch import nn +from torch.utils.data import Dataset, DataLoader +import numpy as np +from pathlib import Path + +class AR(nn.Module): + def __init__(self, lag): + super().__init__() + self.lag = lag + self.linear = nn.Linear(lag, 1, bias=True) + + def forward(self, x): + return self.linear(x) + +def train_one_epoch(loader: DataLoader, model: nn.Module, loss_fn, optimizer): + model.train() + size = len(loader.dataset) + running_loss = 0.0 + for _, (xb, yb) in enumerate(loader): + xb = xb.float() + yb = yb.float() + optimizer.zero_grad() + pred = model(xb) + loss = loss_fn(pred, yb) + loss.backward() + optimizer.step() + running_loss += loss.item() * xb.size(0) + epoch_loss = running_loss / size + print(f"Average traning loss: {epoch_loss: .10e}") + return epoch_loss + +def train(train_dataset: Dataset, epoches: int, ar_model: nn.Module): + loss_fn = nn.L1Loss() + optimizer = torch.optim.Adam(ar_model.parameters(), lr=1e-3) + loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=False) + for _ in range(epoches): + train_one_epoch(loader, ar_model, loss_fn, optimizer) + +def build_train(data, T): + X = [] + y = [] + n = len(data) + for i in range(n - T): + window = data[i : i + T] + target = data[i + T] + X.append(window) + y.append(target) + + return np.array(X), np.array(y) + +class ArDataset(Dataset): + def __init__(self, X, y): + X = torch.tensor(X, dtype=torch.float32) + y = torch.tensor(y, dtype=torch.float32) + self.X = X.float() + self.y = y.float() + self.y = self.y.unsqueeze(1) + + def __len__(self): + return self.X.shape[0] + + def __getitem__(self, idx): + return self.X[idx], self.y[idx] + +def infer_mu(model, X): + model.eval() + with torch.no_grad(): + X_tensor = torch.tensor(X, dtype=torch.float32) + mu = model(X_tensor).squeeze(1).cpu().numpy() + return mu + +def load_ar_model(model_path="./ar_model.pth", lag=90): + model = AR(lag=lag) + model.load_state_dict(torch.load(model_path, weights_only=True)) + model.eval() + return model + +base_dir = Path(__file__).resolve().parent +model_path = base_dir / "ar_model.pth" +trained_ar_model = load_ar_model(model_path) + +# if __name__ == "__main__": +# print("1. Define model") +# ar_model = AR(lag=90) + +# print("2. Load data") +# data = load_data() +# log_ret = data["log_return"].to_numpy() +# log_ret = log_ret[np.isfinite(log_ret)] +# X, y = build_train(log_ret, 90) +# print(X[0], y[0]) + +# print("3. Prepare training") +# train_idx = int(len(X) * 0.85) +# X_train = X[:train_idx] +# y_train = y[:train_idx] +# dataset = ArDataset(X_train, y_train) + +# print("4. Train") +# train(dataset, 10, ar_model) + +# print("5. Test inference") +# ar_model.eval() +# X_test = X[train_idx:] +# y_test = y[train_idx:] + +# mu_test = infer_mu(ar_model, X_test) +# residuals = y_test - mu_test + +# mae = np.mean(np.abs(residuals)) +# print("Test MAE:", mae) + +# print("6. Save model") +# torch.save(ar_model.state_dict(), "./ar_model.pth") \ No newline at end of file diff --git a/alpha/AutoRegression/data_processing.py b/alpha/AutoRegression/data_processing.py new file mode 100644 index 0000000..5fcbe45 --- /dev/null +++ b/alpha/AutoRegression/data_processing.py @@ -0,0 +1,16 @@ +import numpy as np +import pandas as pd + +def load_data(): + data_path = "./binance/BTC_USDT-5m.feather" # Configure to the correct data paths + df = pd.read_feather(data_path) + df = df[df["date"] > "2021-01-01"] + df["log_return"] = np.log(df['close'] / df['close'].shift(1)) + print(df.head()) + return df + +def get_log_return_series(): + df = load_data() + log_ret = df["log_return"].to_numpy() + log_ret = log_ret[np.isfinite(log_ret)] + return log_ret \ No newline at end of file diff --git a/alpha/AutoregressionAlpha.py b/alpha/AutoregressionAlpha.py new file mode 100644 index 0000000..30fd58e --- /dev/null +++ b/alpha/AutoregressionAlpha.py @@ -0,0 +1,32 @@ +import numpy as np +import torch +from pandas import DataFrame +from alpha.interface import IAlpha +from alpha.AutoRegression.autoregression import trained_ar_model, build_train + +AR_MODEL_PATH = "./ar_model.pth" +AR_LAG = 90 + +class AutoregressionAlpha(IAlpha): + def process(self) -> DataFrame: + df = self.dataframe.copy() + + df["log_return"] = np.log(df["close"] / df["close"].shift(1)) + log_ret = df["log_return"].to_numpy() + log_ret = log_ret[np.isfinite(log_ret)] + + if len(log_ret) <= AR_LAG: + df["ar_pred"] = np.nan + return df + X, _ = build_train(log_ret, AR_LAG) + # Load AR model + ar_model = trained_ar_model + ar_model.eval() + with torch.no_grad(): + X_tensor = torch.tensor(X, dtype=torch.float32) + preds = ar_model(X_tensor).squeeze(1).cpu().numpy() + # Align predictions with dataframe index + ar_pred = np.full(df.shape[0], np.nan) + ar_pred[AR_LAG+1:] = preds + df["ar_pred"] = ar_pred + return df \ No newline at end of file diff --git a/alpha/interface.py b/alpha/interface.py index d97a052..6ff9671 100644 --- a/alpha/interface.py +++ b/alpha/interface.py @@ -12,4 +12,20 @@ def process(self) -> DataFrame: This is to decouple the pipulation of indicator from IStrategy """ pass - \ No newline at end of file + +fwd_ret_timeframe = [1, 5, 10, 20, 90] +class AlphaEvaluator: + def __init__(self, dataframe: DataFrame, alpha: IAlpha): + self.df = dataframe + self.alpha = alpha(dataframe) + + def evaluate_information_coefficient(self, alpha_names): + self.df = self.alpha.process() + out = {} + for a in alpha_names: + for t in fwd_ret_timeframe: + self.df['fwd_ret'] = self.df['close'].pct_change().shift(-t) + self.df = self.df.dropna(subset=[a, 'fwd_ret']) + ic = self.df['alpha'].corr(self.df['fwd_ret'], method='spearman') + out[(a, t)] = ic + return out \ No newline at end of file diff --git a/freqtrade b/freqtrade index d57e6c5..8c7385d 160000 --- a/freqtrade +++ b/freqtrade @@ -1 +1 @@ -Subproject commit d57e6c5e5bca53cfc63ee10063710bf7e46d8df0 +Subproject commit 8c7385dc98ecab1359edd878afcff91fffdec683 diff --git a/tests/test_alpha.py b/tests/test_alpha.py index 8069abd..2067115 100644 --- a/tests/test_alpha.py +++ b/tests/test_alpha.py @@ -6,11 +6,12 @@ talib = pytest.importorskip("talib", reason="TA-Lib C library not installed") -from alpha.interface import IAlpha +from alpha.interface import IAlpha, AlphaEvaluator from alpha.SimpleEmaFactors import EmaAlpha from alpha.RsiAlpha import RsiAlpha from alpha.MacdAlpha import MacdAlpha from alpha.BollingerAlpha import BollingerAlpha +from alpha.AutoregressionAlpha import AutoregressionAlpha def _make_ohlcv(n=100): @@ -159,3 +160,59 @@ def test_pctb_near_0_5_for_mean(self): valid = result["bb_pctb"].dropna() # Mean %B across a random walk should be roughly centered around 0.5 assert 0.2 < valid.mean() < 0.8 + +class TestAutoregressionAlpha: + def test_process_adds_ar_pred_column(self): + df = _make_ohlcv(200) + df = df[["date", "close"]].copy() + result = AutoregressionAlpha(df).process() + assert "ar_pred" in result.columns, "Missing ar_pred column" + + def test_ar_pred_nan_for_short_series(self): + df = _make_ohlcv(50) + df = df[["date", "close"]].copy() + result = AutoregressionAlpha(df).process() + assert result["ar_pred"].isna().all(), "ar_pred should be NaN for short series" + + def test_ar_pred_has_valid_values(self): + df = _make_ohlcv(200) + df = df[["date", "close"]].copy() + result = AutoregressionAlpha(df).process() + ar_pred = result["ar_pred"].iloc[91:] + assert ar_pred.notna().any(), "ar_pred should have valid predictions after lag" + +class AlphaStub(IAlpha): + def process(self): + df = self.dataframe.copy() + df["alpha"] = df["close"].pct_change() + return df + +class TestAlphaInformationEvaluation: + def test_evaluate_information_coefficient_returns_dict(self): + df = _make_ohlcv(120) + evaluator = AlphaEvaluator(df, AlphaStub) + result = evaluator.evaluate_information_coefficient(["alpha"]) + assert isinstance(result, dict) + expected_keys = [("alpha", t) for t in [1, 5, 10, 20, 90]] + for key in expected_keys: + assert key in result + assert isinstance(result[key], (float, type(None))) + + def test_ic_values_are_finite_or_nan(self): + df = _make_ohlcv(120) + evaluator = AlphaEvaluator(df, AlphaStub) + result = evaluator.evaluate_information_coefficient(["alpha"]) + for ic in result.values(): + assert (ic is None) or (isinstance(ic, float)) + + def test_ic_on_constant_alpha_is_nan(self): + class ConstAlpha(IAlpha): + def process(self): + df = self.dataframe.copy() + df["alpha"] = 1.0 + return df + df = _make_ohlcv(120) + evaluator = AlphaEvaluator(df, ConstAlpha) + result = evaluator.evaluate_information_coefficient(["alpha"]) + for ic in result.values(): + assert ic != ic \ No newline at end of file From b7273a30e28c2d3cdeefdefb9880228b73b97939 Mon Sep 17 00:00:00 2001 From: CYX22222003 Date: Sun, 29 Mar 2026 22:26:46 +0800 Subject: [PATCH 2/9] Update Unit test scripts to install ml related dependencies --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0eaa4a..81e9c96 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,6 +109,7 @@ jobs: run: | pip install --upgrade pip "setuptools<75" wheel pip install -r requirements.txt + pip install -r requirements-ml.txt pip install scipy pip install -e . From d8f609f8cf18be00adc46cafce25510ad074fb6c Mon Sep 17 00:00:00 2001 From: CHEN YIXUN <138369841+CYX22222003@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:09:06 +0800 Subject: [PATCH 3/9] Update alpha/interface.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- alpha/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alpha/interface.py b/alpha/interface.py index 6ff9671..1d0094f 100644 --- a/alpha/interface.py +++ b/alpha/interface.py @@ -24,7 +24,7 @@ def evaluate_information_coefficient(self, alpha_names): out = {} for a in alpha_names: for t in fwd_ret_timeframe: - self.df['fwd_ret'] = self.df['close'].pct_change().shift(-t) + self.df['fwd_ret'] = self.df['close'].pct_change(periods=t).shift(-t) self.df = self.df.dropna(subset=[a, 'fwd_ret']) ic = self.df['alpha'].corr(self.df['fwd_ret'], method='spearman') out[(a, t)] = ic From 416b2f3d21926268791ec70e91a0a832eb295382 Mon Sep 17 00:00:00 2001 From: CHEN YIXUN <138369841+CYX22222003@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:09:38 +0800 Subject: [PATCH 4/9] Update alpha/AutoRegression/data_processing.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- alpha/AutoRegression/data_processing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/alpha/AutoRegression/data_processing.py b/alpha/AutoRegression/data_processing.py index 5fcbe45..51144f9 100644 --- a/alpha/AutoRegression/data_processing.py +++ b/alpha/AutoRegression/data_processing.py @@ -1,10 +1,10 @@ import numpy as np import pandas as pd -def load_data(): - data_path = "./binance/BTC_USDT-5m.feather" # Configure to the correct data paths +def load_data(data_path: str = "./binance/BTC_USDT-5m.feather", + start_date: str = "2021-01-01"): df = pd.read_feather(data_path) - df = df[df["date"] > "2021-01-01"] + df = df[df["date"] > start_date] df["log_return"] = np.log(df['close'] / df['close'].shift(1)) print(df.head()) return df From 4ec2a30fe6f5d05bc162f78de810cf450fd186d8 Mon Sep 17 00:00:00 2001 From: CHEN YIXUN <138369841+CYX22222003@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:10:19 +0800 Subject: [PATCH 5/9] Update alpha/AutoRegression/autoregression.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- alpha/AutoRegression/autoregression.py | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/alpha/AutoRegression/autoregression.py b/alpha/AutoRegression/autoregression.py index 1d7fbad..da13219 100644 --- a/alpha/AutoRegression/autoregression.py +++ b/alpha/AutoRegression/autoregression.py @@ -78,8 +78,34 @@ def load_ar_model(model_path="./ar_model.pth", lag=90): base_dir = Path(__file__).resolve().parent model_path = base_dir / "ar_model.pth" -trained_ar_model = load_ar_model(model_path) + +class _LazyARModel: + """ + Lazily loads and caches the AR model on first use. + + This avoids loading the model at import time, so that import errors + due to missing/corrupt weights or missing dependencies do not + prevent the rest of the package from being used. + """ + + def __init__(self, path): + self._path = path + self._model = None + + def _load(self): + if self._model is None: + # Delegate to the existing loader; any exceptions will be + # raised at first use rather than at import time. + self._model = load_ar_model(self._path) + return self._model + + def __getattr__(self, name): + # Proxy attribute access to the underlying model instance. + return getattr(self._load(), name) + + +trained_ar_model = _LazyARModel(model_path) # if __name__ == "__main__": # print("1. Define model") # ar_model = AR(lag=90) From bedb761a12073eccd4630cf7cc1eeb74b6ceb21b Mon Sep 17 00:00:00 2001 From: CHEN YIXUN <138369841+CYX22222003@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:11:21 +0800 Subject: [PATCH 6/9] Update alpha/interface.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- alpha/interface.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/alpha/interface.py b/alpha/interface.py index 1d0094f..06d01bf 100644 --- a/alpha/interface.py +++ b/alpha/interface.py @@ -20,12 +20,13 @@ def __init__(self, dataframe: DataFrame, alpha: IAlpha): self.alpha = alpha(dataframe) def evaluate_information_coefficient(self, alpha_names): - self.df = self.alpha.process() + df_processed = self.alpha.process() out = {} for a in alpha_names: for t in fwd_ret_timeframe: - self.df['fwd_ret'] = self.df['close'].pct_change(periods=t).shift(-t) - self.df = self.df.dropna(subset=[a, 'fwd_ret']) - ic = self.df['alpha'].corr(self.df['fwd_ret'], method='spearman') + fwd_ret = df_processed['close'].pct_change().shift(-t) + temp_df = DataFrame({'alpha': df_processed[a], 'fwd_ret': fwd_ret}) + temp_df = temp_df.dropna(subset=['alpha', 'fwd_ret']) + ic = temp_df['alpha'].corr(temp_df['fwd_ret'], method='spearman') out[(a, t)] = ic return out \ No newline at end of file From e01c0793e260505c73c68ab746733fb23f202fc7 Mon Sep 17 00:00:00 2001 From: CHEN YIXUN <138369841+CYX22222003@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:11:39 +0800 Subject: [PATCH 7/9] Update alpha/AutoRegression/autoregression.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- alpha/AutoRegression/autoregression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alpha/AutoRegression/autoregression.py b/alpha/AutoRegression/autoregression.py index da13219..795ccb6 100644 --- a/alpha/AutoRegression/autoregression.py +++ b/alpha/AutoRegression/autoregression.py @@ -27,7 +27,7 @@ def train_one_epoch(loader: DataLoader, model: nn.Module, loss_fn, optimizer): optimizer.step() running_loss += loss.item() * xb.size(0) epoch_loss = running_loss / size - print(f"Average traning loss: {epoch_loss: .10e}") + print(f"Average training loss: {epoch_loss: .10e}") return epoch_loss def train(train_dataset: Dataset, epoches: int, ar_model: nn.Module): From 7514cb30289d87abd0e1e37bf7a57e8b937a7435 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:14:14 +0000 Subject: [PATCH 8/9] Fix AlphaEvaluator type annotation: use type[IAlpha] instead of IAlpha instance Agent-Logs-Url: https://github.com/mlsys-io/PortfolioBench/sessions/36ad2240-1e58-424c-a71e-18338280393a Co-authored-by: CYX22222003 <138369841+CYX22222003@users.noreply.github.com> --- alpha/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alpha/interface.py b/alpha/interface.py index 06d01bf..8652fe3 100644 --- a/alpha/interface.py +++ b/alpha/interface.py @@ -15,9 +15,9 @@ def process(self) -> DataFrame: fwd_ret_timeframe = [1, 5, 10, 20, 90] class AlphaEvaluator: - def __init__(self, dataframe: DataFrame, alpha: IAlpha): + def __init__(self, dataframe: DataFrame, alpha: type[IAlpha], metadata: dict = None): self.df = dataframe - self.alpha = alpha(dataframe) + self.alpha = alpha(dataframe, metadata if metadata is not None else {}) def evaluate_information_coefficient(self, alpha_names): df_processed = self.alpha.process() From 5f94d401817eb647ee865702f07609c50d90e282 Mon Sep 17 00:00:00 2001 From: CYX22222003 Date: Wed, 8 Apr 2026 17:15:18 +0800 Subject: [PATCH 9/9] Fix copilot bug --- alpha/AutoRegression/autoregression.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alpha/AutoRegression/autoregression.py b/alpha/AutoRegression/autoregression.py index 795ccb6..2b5b5e9 100644 --- a/alpha/AutoRegression/autoregression.py +++ b/alpha/AutoRegression/autoregression.py @@ -103,6 +103,10 @@ def _load(self): def __getattr__(self, name): # Proxy attribute access to the underlying model instance. return getattr(self._load(), name) + + def __call__(self, *args, **kwargs): + # Forward calls to the underlying model + return self._load()(*args, **kwargs) trained_ar_model = _LazyARModel(model_path)