import yfinance as yf import pandas as pd import numpy as np from sklearn.metrics import accuracy_score import numpy as np import pandas as pd import yfinance as yf from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, mean_absolute_error from catboost import CatBoostClassifier TEST_SIZE = 0.2 SEQ_LENGTH = 180 SEQ_INTERVAL = 10 import datetime def get_forecast_data( ticker, q, feature_cols, best_seq_len, selected_model, start="2017-01-01" ): df = yf.download(ticker, start=start, auto_adjust=True, progress=False) d_ = yf.download( ticker, start=datetime.datetime.now().strftime("%Y-%m-%d"), auto_adjust=True, progress=False, ) df.update(d_) if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.get_level_values(0) df["ret"] = np.log(df["Close"] / df["Close"].shift(1)) df["volatility"] = df["ret"].rolling(20).std() df["hl_ratio"] = np.log(df["High"] / df["Low"]) df["oc_ratio"] = np.log(df["Close"] / df["Open"]) df["volume_change"] = np.log(df["Volume"] / df["Volume"].shift(1)) sign = np.sign(df["ret"]) streak = [] s = 0 for x in sign: if pd.isna(x): streak.append(np.nan) continue if x > 0: s = s + 1 if s > 0 else 1 elif x < 0: s = s - 1 if s < 0 else -1 else: s = 0 streak.append(s) df["streak"] = streak df["target_ret"] = df["ret"].shift(-1) # Re-apply quantile binning using previously fitted bin_edges quantile_features = [ "ret", "volatility", "hl_ratio", "oc_ratio", "volume_change", ] bin_edges = {} for col in quantile_features: _, bins = pd.qcut(df[col], q=q, labels=False, retbins=True, duplicates="drop") bins[0] = -np.inf bins[-1] = np.inf bin_edges[col] = bins df[col + "_q"] = pd.cut(df[col], bins=bins, labels=False) # .astype(int) for col in quantile_features: df[col + "_q"] = pd.cut( df[col], bins=bin_edges[col], labels=False, duplicates="drop" ) _, target_bins = pd.qcut( df["target_ret"], q=q, labels=False, retbins=True, duplicates="drop" ) target_bins[0] = -np.inf target_bins[-1] = np.inf df["target"] = pd.cut(df["target_ret"], bins=target_bins, labels=False) # Drop rows with NaN values introduced by feature engineering df_processed = df.dropna(subset=feature_cols + ["target"]) # df_for_tomorrow_prediction = df.dropna()#.tail(best_seq_len_multiclass) # Create the sequence. The make_sequences function expects a target column, but for actual prediction, it won't be used. # We will manually extract X_tomorrow. X_tomorrow_raw, _ = make_sequences( df_processed, feature_cols, "target", # Placeholder, not actually used for a single prediction point best_seq_len, ) # The make_sequences returns sequences ending at the last day of the input df. # We need the *last* sequence from X_tomorrow_raw for the actual prediction. X_tomorrow = X_tomorrow_raw[-1].reshape(1, -1) pred_tomorrow = selected_model["model"].predict(X_tomorrow)[0] # First, generate predictions for all historical data using the best model X_full, y_full = make_sequences(df_processed, feature_cols, "target", best_seq_len) X_full_flat = X_full.reshape(X_full.shape[0], -1) pred_full = selected_model["model"].predict(X_full_flat) pred_full = pred_full.astype(int).ravel() # Create tmp_current dataframe using predicted bins and actual historical returns tmp_current = pd.DataFrame( { "pred": pred_full, "ret": df_processed["target_ret"].iloc[best_seq_len:].values, } ) # Calculate rolling metrics on tmp_current TREND_WINDOW = 15 for val in range(q): conditional_ret_current = tmp_current["ret"].where(tmp_current["pred"] == val) tmp_current[ f"rolling_ret_{TREND_WINDOW}_mean_pred_{val}" ] = conditional_ret_current.rolling(window=TREND_WINDOW, min_periods=1).mean() # Calculate the trend signal for each day in tmp_current trend_signal_current = [] for idx, row in tmp_current.fillna(0).iterrows(): res = 0 for n in range(q): res += row[f"rolling_ret_{TREND_WINDOW}_mean_pred_{n}"] trend_signal_current.append(np.sign(res)) # The trend signal for tomorrow is the last calculated signal trend_signal_for_tomorrow = trend_signal_current[-1] TOP_BINS = 2 signal_tomorrow = ( (pred_tomorrow >= (q - TOP_BINS)) and (trend_signal_for_tomorrow >= 0) ).astype(int) return ( pred_tomorrow, trend_signal_for_tomorrow, signal_tomorrow, df_processed.index[-1], ) def get_dataframe(ticker, q=3, start="2017-01-01", end="2026-02-01"): df = yf.download(ticker, start=start, end=None, auto_adjust=True) if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.get_level_values(0) # ============================================================ # FEATURES # ============================================================ df["ret"] = np.log(df["Close"] / df["Close"].shift(1)) df["volatility"] = df["ret"].rolling(20).std() df["hl_ratio"] = np.log(df["High"] / df["Low"]) df["oc_ratio"] = np.log(df["Close"] / df["Open"]) df["volume_change"] = np.log(df["Volume"] / df["Volume"].shift(1)) # ============================================================ # STREAK # ============================================================ sign = np.sign(df["ret"]) streak = [] s = 0 for x in sign: if pd.isna(x): streak.append(np.nan) continue if x > 0: s = s + 1 if s > 0 else 1 elif x < 0: s = s - 1 if s < 0 else -1 else: s = 0 streak.append(s) df["streak"] = streak # ============================================================ # TARGET # ============================================================ df["target_ret"] = df["ret"].shift(-1) # ============================================================ # TRAIN/TEST SPLIT FIRST # (important to avoid leakage) # ============================================================ split_idx = int(len(df) * (1 - TEST_SIZE)) train_df = df.iloc[:split_idx].copy() test_df = df.iloc[split_idx:].copy() # ============================================================ # QCUT FEATURES USING TRAIN ONLY # ============================================================ quantile_features = [ "ret", "volatility", "hl_ratio", "oc_ratio", "volume_change", ] bin_edges = {} for col in quantile_features: _, bins = pd.qcut( train_df[col], q=q, labels=False, retbins=True, duplicates="drop" ) bins[0] = -np.inf bins[-1] = np.inf bin_edges[col] = bins train_df[col + "_q"] = pd.cut( train_df[col], bins=bins, labels=False ) # .astype(int) test_df[col + "_q"] = pd.cut( test_df[col], bins=bins, labels=False ) # .astype(int) # ============================================================ # TARGET BINS # ============================================================ _, target_bins = pd.qcut( train_df["target_ret"], q=q, labels=False, retbins=True, duplicates="drop" ) # .astype(int) target_bins[0] = -np.inf target_bins[-1] = np.inf train_df["target"] = pd.cut( train_df["target_ret"], bins=target_bins, labels=False ) # .astype(int) test_df["target"] = pd.cut( test_df["target_ret"], bins=target_bins, labels=False ) # .astype(int) # ============================================================ # KEEP ONLY VALID ROWS # ============================================================ feature_cols = [ "ret_q", "volatility_q", "hl_ratio_q", "oc_ratio_q", "volume_change_q", "streak", ] train_df = train_df.dropna(subset=feature_cols + ["target"]) test_df = test_df.dropna(subset=feature_cols + ["target"]) return train_df, test_df, feature_cols def make_sequences(df, feature_cols, target_col, seq_len): X = [] y = [] values = df[feature_cols].values target = df[target_col].values for i in range(seq_len, len(df)): X.append(values[i - seq_len : i]) y.append(target[i]) return np.array(X), np.array(y) def prepare_data(train_df, test_df, feature_cols): models = {} for seq in range(SEQ_INTERVAL, SEQ_LENGTH + SEQ_INTERVAL, SEQ_INTERVAL): X_train, y_train = make_sequences(train_df, feature_cols, "target", seq) X_test, y_test = make_sequences(test_df, feature_cols, "target", seq) print(X_train.shape) print(X_test.shape) # ============================================================ # FLATTEN FOR CATBOOST # ============================================================ X_train_flat = X_train.reshape(X_train.shape[0], -1) X_test_flat = X_test.reshape(X_test.shape[0], -1) # ============================================================ # CATBOOST # ============================================================ model = CatBoostClassifier( loss_function="MultiClass", iterations=5000, learning_rate=0.01, depth=7, l2_leaf_reg=50, od_type="Iter", od_wait=100, use_best_model=True, verbose=False, ) # iterations=5000, # depth=6, # learning_rate=0.03, # random_seed=42, # verbose=100 # ) model.fit(X_train_flat, y_train, eval_set=(X_test_flat, y_test)) pred = model.predict(X_test_flat) pred = pred.astype(int).ravel() # ============================================================ # METRICS # ============================================================ acc = accuracy_score(y_test, pred) mae_bins = mean_absolute_error(y_test, pred) # print() # print("Accuracy :", acc) # print("MAE bins :", mae_bins) # exact ±1 bin accuracy adj_acc = np.mean(np.abs(pred - y_test) <= 1) models[seq] = { "acc": acc, "mae_bins": mae_bins, "adj_acc": adj_acc, "model": model, "pred": pred, "y_test": y_test, "val_loss": model.best_score_["validation"]["MultiClass"], } # for x in range(1,q-1): # adj_acc = np.mean( # np.abs(pred - y_test) <= x # ) # models[seq].update({ # f"adj_acc_{x}": adj_acc # }) # print(f"Within {x} bin :", adj_acc) return models