Prediction and model training work

This commit is contained in:
jarno committed 2026-08-06 17:22:57 +03:00
1 parent fddcc9190a
commit 465e09fc82
13 files changed
+1689 -403

No files matched your search

+164
View File
@@ -0,0 +1,164 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 11,
"id": "c8a08105",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from dotenv import load_dotenv\n",
"from alpaca.trading.client import TradingClient\n",
"\n",
"load_dotenv()\n",
"\n",
"# Set paper=True for paper trading (sandbox), paper=False for live trading\n",
"trading_client = TradingClient(\n",
" api_key=os.getenv(\"ALPACA_API_KEY\"),\n",
" secret_key=os.getenv(\"ALPACA_SECRET_KEY\"),\n",
" paper=True,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "49b14380",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"No open positions.\n"
]
}
],
"source": [
"positions = trading_client.get_all_positions()\n",
"\n",
"if not positions:\n",
" print(\"No open positions.\")\n",
"else:\n",
" print(\"Current Portfolio Positions:\")\n",
" for pos in positions:\n",
" print(\n",
" f\"Symbol: {pos.symbol:<5} | \"\n",
" f\"Qty: {pos.qty:<5} | \"\n",
" f\"Avg Entry Price: ${float(pos.avg_entry_price):.2f} | \"\n",
" f\"Current Price: ${float(pos.current_price):.2f} | \"\n",
" f\"Unrealized P/L: ${float(pos.unrealized_pl):.2f}\"\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "c032239c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"99997.95 99997.95\n"
]
}
],
"source": [
"account = trading_client.get_account()\n",
"print(\n",
" account.cash,\n",
" account.equity\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "c759e40f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"No open orders found.\n"
]
}
],
"source": [
"from alpaca.trading.requests import GetOrdersRequest\n",
"from alpaca.trading.enums import QueryOrderStatus\n",
"\n",
"# Request only open orders\n",
"request_params = GetOrdersRequest(status=QueryOrderStatus.OPEN)\n",
"open_orders = trading_client.get_orders(filter=request_params)\n",
"\n",
"if not open_orders:\n",
" print(\"No open orders found.\")\n",
"else:\n",
" print(f\"Found {len(open_orders)} open order(s):\")\n",
" for order in open_orders:\n",
" print(\n",
" f\"ID: {order.id} | Symbol: {order.symbol} | \"\n",
" f\"Side: {order.side} | Qty: {order.qty} | Status: {order.status}\"\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "091cf061",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Submitted Order ID: 4838c16d-7a12-4dc3-80cb-7c5d1dbecda3 | Status: OrderStatus.ACCEPTED\n"
]
}
],
"source": [
"from alpaca.trading.requests import MarketOrderRequest\n",
"from alpaca.trading.enums import OrderSide, TimeInForce\n",
"\n",
"# Define a market buy order for 10 shares of SPY\n",
"market_order_data = MarketOrderRequest(\n",
" symbol=\"SPY\",\n",
" notional=100.0,\n",
" side=OrderSide.BUY,\n",
" time_in_force=TimeInForce.DAY,\n",
")\n",
"\n",
"# Submit the order\n",
"order = trading_client.submit_order(order_data=market_order_data)\n",
"\n",
"print(f\"Submitted Order ID: {order.id} | Status: {order.status}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.15"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+308
View File
@@ -0,0 +1,308 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "038bf2ce",
"metadata": {},
"outputs": [],
"source": [
"import warnings\n",
"from pathlib import Path\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# Source of truth for model inputs (must match training order exactly)\n",
"FEATURE_COLUMNS = [\n",
" \"SPY_ret_5\",\n",
" \"SPY_ret_20\",\n",
" \"SPY_dist_sma50\",\n",
" \"VIX_change_5\",\n",
" \"VIX_rank_20\",\n",
" \"TLT_ret_10\",\n",
" \"USO_ret_5\",\n",
" \"SPY_TLT_ratio_ret\",\n",
"]\n",
"\n",
"DEFAULT_SYMBOLS = {\n",
" \"SPY\": \"SPY\",\n",
" \"VIXY\": \"VIX\", # Change to \"VIX\": \"VIX\" if using raw VIX Parquet\n",
" \"TLT\": \"TLT\",\n",
" \"USO\": \"USO\",\n",
"}\n",
"\n",
"\n",
"def load_raw_close_prices(\n",
" raw_data_dir: Path, symbols: dict[str, str]\n",
") -> pd.DataFrame:\n",
" \"\"\"Reads raw Parquet files and merges close prices into a single inner-joined DataFrame.\"\"\"\n",
" frames = []\n",
" for symbol, alias in symbols.items():\n",
" path = raw_data_dir / f\"{symbol}.parquet\"\n",
" if not path.exists():\n",
" raise FileNotFoundError(f\"Missing raw data file: {path}\")\n",
"\n",
" frame = pd.read_parquet(path)\n",
" if \"date\" in frame.columns:\n",
" frame = frame.set_index(\"date\")\n",
"\n",
" frame.index = pd.to_datetime(frame.index)\n",
" frame = frame.sort_index()\n",
" frames.append(frame[[\"close\"]].rename(columns={\"close\": f\"{alias}_close\"}))\n",
"\n",
" return pd.concat(frames, axis=1, join=\"inner\")\n",
"\n",
"\n",
"def compute_features(prices: pd.DataFrame) -> pd.DataFrame:\n",
" \"\"\"Computes engineered features from raw merged price history.\"\"\"\n",
" df = prices.copy()\n",
"\n",
" # Target asset features\n",
" df[\"SPY_ret_5\"] = df[\"SPY_close\"].pct_change(5)\n",
" df[\"SPY_ret_20\"] = df[\"SPY_close\"].pct_change(20)\n",
"\n",
" sma_50 = df[\"SPY_close\"].rolling(50).mean()\n",
" df[\"SPY_dist_sma50\"] = (df[\"SPY_close\"] - sma_50) / sma_50\n",
"\n",
" # Volatility / Market Stress\n",
" df[\"VIX_change_5\"] = df[\"VIX_close\"].pct_change(5)\n",
" df[\"VIX_rank_20\"] = df[\"VIX_close\"].rolling(20).rank(pct=True)\n",
"\n",
" # Macro & Relative ratios\n",
" df[\"TLT_ret_10\"] = df[\"TLT_close\"].pct_change(10)\n",
" df[\"USO_ret_5\"] = df[\"USO_close\"].pct_change(5)\n",
" df[\"SPY_TLT_ratio_ret\"] = (df[\"SPY_close\"] / df[\"TLT_close\"]).pct_change(5)\n",
"\n",
" return df[FEATURE_COLUMNS]\n",
"\n",
"\n",
"def get_latest_inference_features(\n",
" raw_data_dir: Path,\n",
" symbols: dict[str, str] | None = None,\n",
" max_age_days: int = 1,\n",
") -> pd.DataFrame:\n",
" \"\"\"Loads raw prices, computes features, verifies date freshness,\n",
"\n",
" and returns the latest single row for model prediction.\n",
" \"\"\"\n",
" symbols = symbols or DEFAULT_SYMBOLS\n",
"\n",
" # 1. Load prices & compute rolling features\n",
" prices = load_raw_close_prices(raw_data_dir, symbols)\n",
" features = compute_features(prices).dropna()\n",
"\n",
" if features.empty:\n",
" raise ValueError(\n",
" \"Not enough historical rows to compute 50-day rolling window features.\"\n",
" )\n",
"\n",
" # 2. Extract latest available row as a 1-row DataFrame\n",
" latest_row = features.iloc[[-1]]\n",
" latest_date = latest_row.index[0]\n",
"\n",
" # 3. Check data freshness and raise a warning if stale\n",
" now = pd.Timestamp.now()\n",
" latest_date_naive = (\n",
" latest_date.tz_localize(None)\n",
" if latest_date.tz is not None\n",
" else latest_date\n",
" )\n",
" days_old = (now.floor(\"D\") - latest_date_naive.floor(\"D\")).days\n",
"\n",
" if days_old > max_age_days:\n",
" warnings.warn(\n",
" f\"STALE DATA WARNING: Latest feature row is from {latest_date.strftime('%Y-%m-%d')} \"\n",
" f\"({days_old} day(s) old). Update raw Parquet files before executing trades.\",\n",
" UserWarning,\n",
" stacklevel=2,\n",
" )\n",
"\n",
" return latest_row\n",
"\n",
"def get_target_exposure(\n",
" p_pred: float, p_base: float, sensitivity: float = 5.0\n",
") -> float:\n",
" \"\"\"Maps predicted probability to a target portfolio equity allocation (0.0 to 1.0).\n",
"\n",
" - p_pred == p_base --> 50% Target Exposure (Neutral)\n",
" - p_pred > p_base --> Scale up toward 100% (Bullish)\n",
" - p_pred < p_base --> Scale down toward 0% (Bearish / Cash)\n",
" \"\"\"\n",
" # Calculate deviation from the historical average\n",
" delta = p_pred - p_base\n",
"\n",
" # Base target allocation is 50% equity / 50% cash\n",
" base_allocation = 0.50\n",
"\n",
" # Sensitivity controls how aggressively probability changes alter allocation\n",
" # e.g., a +0.08 delta * 5.0 = +0.40 -> 90% Equity Allocation\n",
" target_allocation = base_allocation + (delta * sensitivity)\n",
"\n",
" # Clamp bounds strictly between 0% (full cash) and 100% (full SPY)\n",
" return float(np.clip(target_allocation, 0.0, 1.0))"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "0b3d2c25",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Date: 2026-07-27 | Prob: 0.5843 | Base: 0.5830\n"
]
},
{
"data": {
"text/plain": [
"0.5064256139268726"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import json\n",
"import xgboost as xgb\n",
"\n",
"# 1. Load model and metadata\n",
"model = xgb.XGBClassifier()\n",
"model.load_model(\"models/spy_xgb_v1.json\")\n",
"\n",
"with open(\"models/spy_xgb_v1_meta.json\", \"r\") as f:\n",
" meta = json.load(f)\n",
"\n",
"# 2. Fetch latest features from raw parquet files\n",
"RAW_DATA_DIR = Path(\"../data/ibkr/daily\")\n",
"X_latest = get_latest_inference_features(RAW_DATA_DIR, max_age_days=1)\n",
"\n",
"# 3. Predict probability\n",
"p_pred = float(model.predict_proba(X_latest[meta[\"feature_cols\"]])[0, 1])\n",
"p_base = meta[\"p_base\"]\n",
"\n",
"print(\n",
" f\"Date: {X_latest.index[0].date()} | Prob: {p_pred:.4f} | Base: {p_base:.4f}\"\n",
")\n",
"\n",
"get_target_exposure(p_pred, p_base, sensitivity=5.0)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "3f823fa6",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>SPY_ret_5</th>\n",
" <th>SPY_ret_20</th>\n",
" <th>SPY_dist_sma50</th>\n",
" <th>VIX_change_5</th>\n",
" <th>VIX_rank_20</th>\n",
" <th>TLT_ret_10</th>\n",
" <th>USO_ret_5</th>\n",
" <th>SPY_TLT_ratio_ret</th>\n",
" </tr>\n",
" <tr>\n",
" <th>date</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2026-07-27</th>\n",
" <td>-0.004043</td>\n",
" <td>0.013855</td>\n",
" <td>-0.007938</td>\n",
" <td>0.007065</td>\n",
" <td>0.75</td>\n",
" <td>-0.00262</td>\n",
" <td>-0.005976</td>\n",
" <td>-0.002378</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" SPY_ret_5 SPY_ret_20 SPY_dist_sma50 VIX_change_5 VIX_rank_20 \\\n",
"date \n",
"2026-07-27 -0.004043 0.013855 -0.007938 0.007065 0.75 \n",
"\n",
" TLT_ret_10 USO_ret_5 SPY_TLT_ratio_ret \n",
"date \n",
"2026-07-27 -0.00262 -0.005976 -0.002378 "
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"X_latest"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "476fd7fd",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.15"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+141 -20
View File
@@ -17,7 +17,26 @@
"execution_count": null,
"id": "543426a2",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Connecting to IBKR Gateway at 127.0.0.1:4002 with client id 101...\n",
"Failed to connect to IBKR Gateway: This event loop is already running\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"Error 200, reqId 9: No security definition has been found for the request, contract: Stock(symbol='VUAA', exchange='SMART', currency='USD')\n",
"Error 200, reqId 10: No security definition has been found for the request\n",
"Canceled order: Trade(contract=Stock(symbol='VUAA', exchange='SMART', currency='USD'), order=MarketOrder(orderId=10, clientId=101, action='BUY', totalQuantity=0.7), orderStatus=OrderStatus(orderId=10, status='Cancelled', filled=0.0, remaining=0.0, avgFillPrice=0.0, permId=0, parentId=0, lastFillPrice=0.0, clientId=0, whyHeld='', mktCapPrice=0.0), fills=[], log=[TradeLogEntry(time=datetime.datetime(2026, 7, 30, 19, 36, 57, 539380, tzinfo=datetime.timezone.utc), status='PendingSubmit', message='', errorCode=0), TradeLogEntry(time=datetime.datetime(2026, 7, 30, 19, 36, 57, 743208, tzinfo=datetime.timezone.utc), status='Cancelled', message='Error 200, reqId 10: No security definition has been found for the request', errorCode=200)], advancedError='')\n",
"Peer closed connection.\n"
]
}
],
"source": [
"import sys\n",
"from pathlib import Path\n",
@@ -35,36 +54,55 @@
"if str(src_path) not in sys.path:\n",
" sys.path.insert(0, str(src_path))\n",
"\n",
"from trading_bot.data.fetch_ibkr_daily import (\n",
" IBKR_CLIENT_ID,\n",
" IBKR_CONNECT_TIMEOUT_SECONDS,\n",
" IBKR_HOST,\n",
" IBKR_PORT,\n",
")\n",
"IBKR_HOST = \"127.0.0.1\"\n",
"IBKR_PORT = 4002\n",
"IBKR_CLIENT_ID = 101\n",
"IBKR_CONNECT_TIMEOUT_SECONDS = 10\n",
"\n",
"ib = IB()\n",
"print(f\"Connecting to IBKR Gateway at {IBKR_HOST}:{IBKR_PORT} with client id {IBKR_CLIENT_ID}...\")\n",
"ib.connect(IBKR_HOST, IBKR_PORT, clientId=IBKR_CLIENT_ID, timeout=IBKR_CONNECT_TIMEOUT_SECONDS)\n",
"print(\"Connection established.\")\n"
"try:\n",
" ib.connect(IBKR_HOST, IBKR_PORT, clientId=IBKR_CLIENT_ID, timeout=IBKR_CONNECT_TIMEOUT_SECONDS)\n",
" print(\"Connection established.\")\n",
"except Exception as e:\n",
" print(f\"Failed to connect to IBKR Gateway: {e}\")\n",
" ib.disconnect()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 7,
"id": "ecdc721f",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Connected: True\n",
"Client ID: 101\n"
]
}
],
"source": [
"print(f\"Connected: {ib.isConnected()}\")\n",
"print(f\"Client ID: {ib.clientId}\")\n"
"print(f\"Client ID: {ib.client.clientId}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 8,
"id": "10ebc240",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"No open portfolio positions were returned.\n"
]
}
],
"source": [
"portfolio = ib.portfolio()\n",
"if not portfolio:\n",
@@ -79,27 +117,110 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 23,
"id": "fed654ec",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Unknown contract: Stock(symbol='VUAA', exchange='SMART', currency='USD')\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Submitted order for VUAA: Trade(contract=Stock(symbol='VUAA', exchange='SMART', currency='USD'), order=MarketOrder(orderId=10, clientId=101, action='BUY', totalQuantity=0.7), orderStatus=OrderStatus(orderId=10, status='PendingSubmit', filled=0.0, remaining=0.0, avgFillPrice=0.0, permId=0, parentId=0, lastFillPrice=0.0, clientId=0, whyHeld='', mktCapPrice=0.0), fills=[], log=[TradeLogEntry(time=datetime.datetime(2026, 7, 30, 19, 36, 57, 539380, tzinfo=datetime.timezone.utc), status='PendingSubmit', message='', errorCode=0)], advancedError='')\n"
]
}
],
"source": [
"from ib_insync import MarketOrder\n",
"\n",
"contract = Stock(\"SPY\", \"SMART\", \"USD\")\n",
"ib.qualifyContracts(contract)\n",
"contract = Stock(\"VUAA\", \"SMART\", \"USD\")\n",
"await ib.qualifyContractsAsync(contract)\n",
"\n",
"# Adjust the quantity as needed before running this cell.\n",
"order = MarketOrder(\"BUY\", 1)\n",
"order = MarketOrder(\"BUY\", 0.70)\n",
"trade = ib.placeOrder(contract, order)\n",
"\n",
"print(f\"Submitted order for {contract.symbol}: {trade}\")\n"
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "06881101",
"metadata": {},
"outputs": [],
"source": [
"trds = ib.trades()"
]
},
{
"cell_type": "code",
"execution_count": 42,
"id": "08663602",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['Cancelled', 'Cancelled', 'Cancelled']"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"list(map(lambda x: x.orderStatus.status, trds))"
]
},
{
"cell_type": "code",
"execution_count": 43,
"id": "2c7abf23",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[Trade(contract=Stock(conId=756733, symbol='SPY', right='?', exchange='SMART', currency='USD', localSymbol='SPY', tradingClass='SPY'), order=Order(permId=1578393268, action='BUY', totalQuantity=1.0, orderType='MKT', lmtPrice=0.0, auxPrice=0.0, tif='DAY', ocaType=3, displaySize=2147483647, rule80A='0', openClose='', volatilityType=0, deltaNeutralOrderType='None', referencePriceType=0, account='DUR281921', clearingIntent='IB', cashQty=0.0, dontUseAutoPriceForHedge=True, filledQuantity=0.0, refFuturesConId=2147483647, shareholder='Not an insider or substantial shareholder'), orderStatus=OrderStatus(orderId=0, status='Cancelled', filled=0.0, remaining=0.0, avgFillPrice=0.0, permId=0, parentId=0, lastFillPrice=0.0, clientId=0, whyHeld='', mktCapPrice=0.0), fills=[], log=[], advancedError=''),\n",
" Trade(contract=Stock(conId=756733, symbol='SPY', right='?', exchange='SMART', currency='USD', localSymbol='SPY', tradingClass='SPY'), order=Order(permId=24474350, action='BUY', totalQuantity=0.1345, orderType='MKT', lmtPrice=0.0, auxPrice=0.0, tif='DAY', ocaType=3, displaySize=2147483647, rule80A='0', openClose='', volatilityType=0, deltaNeutralOrderType='None', referencePriceType=0, account='DUR281921', clearingIntent='IB', cashQty=0.0, dontUseAutoPriceForHedge=True, filledQuantity=0.0, refFuturesConId=2147483647, shareholder='Not an insider or substantial shareholder'), orderStatus=OrderStatus(orderId=0, status='Cancelled', filled=0.0, remaining=0.0, avgFillPrice=0.0, permId=0, parentId=0, lastFillPrice=0.0, clientId=0, whyHeld='', mktCapPrice=0.0), fills=[], log=[], advancedError=''),\n",
" Trade(contract=Stock(symbol='VUAA', exchange='SMART', currency='USD'), order=MarketOrder(orderId=10, clientId=101, action='BUY', totalQuantity=0.7), orderStatus=OrderStatus(orderId=10, status='Cancelled', filled=0.0, remaining=0.0, avgFillPrice=0.0, permId=0, parentId=0, lastFillPrice=0.0, clientId=0, whyHeld='', mktCapPrice=0.0), fills=[], log=[TradeLogEntry(time=datetime.datetime(2026, 7, 30, 19, 36, 57, 539380, tzinfo=datetime.timezone.utc), status='PendingSubmit', message='', errorCode=0), TradeLogEntry(time=datetime.datetime(2026, 7, 30, 19, 36, 57, 743208, tzinfo=datetime.timezone.utc), status='Cancelled', message='Error 200, reqId 10: No security definition has been found for the request', errorCode=200)], advancedError='')]"
]
},
"execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"trds"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.15"
}
},
"nbformat": 4,
+1 -1
View File
@@ -1 +1 @@
{"learner":{"attributes":{"best_iteration":"41","best_score":"0.6593948980172475","scikit_learn":"{\"_estimator_type\": \"classifier\"}"},"feature_names":["VIX_rank_20","TLT_ret_10","USO_ret_5","SPY_TLT_ratio_ret","SPY_ret_5","SPY_ret_20","SPY_dist_sma50"],"feature_types":["float","float","float","float","float","float","float"],"gradient_booster":{"model":{"cats":{"enc":[],"feature_segments":[],"sorted_idx":[]},"gbtree_model_param":{"num_parallel_tree":"1","num_trees":"62"},"iteration_indptr":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62],"tree_info":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"trees":[{"base_weights":[1.1503149E-2,3.602216E-4,-1.4237287E-2],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":0,"left_children":[1,-1,-1],"loss_changes":[5.1499934E0,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[6.892705E-2,3.602216E-4,-1.4237287E-2],"split_indices":[6,0,0],"split_type":[0,0,0],"sum_hessian":[1.4388889E2,1.4243056E2,1.4583333E0],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[2.2381552E-2,2.2380585E-3,-2.9987623E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":1,"left_children":[1,-1,-1],"loss_changes":[9.187705E0,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[1.4205446E-2,2.2380585E-3,-2.9987623E-3],"split_indices":[3,0,0],"split_type":[0,0,0],"sum_hessian":[1.3951015E2,8.603824E1,5.3471912E1],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[9.584328E-2,-1.7863275E-3,6.1051417E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":2,"left_children":[1,-1,-1],"loss_changes":[2.0163263E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[7.100592E-3,-1.7863275E-3,6.1051417E-3],"split_indices":[1,0,0],"split_type":[0,0,0],"sum_hessian":[1.4096178E2,9.2359184E1,4.86026E1],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[4.7446564E-2,7.0196665E-3,-1.0298543E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":3,"left_children":[1,-1,-1],"loss_changes":[1.4477959E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[-1.7963655E-2,7.0196665E-3,-1.0298543E-3],"split_indices":[3,0,0],"split_type":[0,0,0],"sum_hessian":[1.4530872E2,2.6470573E1,1.1883814E2],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[6.2338286E-3,3.326677E-3,-1.7981239E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":4,"left_children":[1,-1,-1],"loss_changes":[8.945065E0,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[-1.1166078E-2,3.326677E-3,-1.7981239E-3],"split_indices":[2,0,0],"split_type":[0,0,0],"sum_hessian":[1.4530305E2,5.2463432E1,9.283963E1],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[-3.0314475E-2,6.923221E-3,-1.2672357E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":5,"left_children":[1,-1,-1],"loss_changes":[1.0347677E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[-2.858291E-2,6.923221E-3,-1.2672357E-3],"split_indices":[3,0,0],"split_type":[0,0,0],"sum_hessian":[1.4626752E2,1.648944E1,1.2977808E2],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[2.5649436E-2,-3.922325E-3,4.0787407E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":6,"left_children":[1,-1,-1],"loss_changes":[2.3413443E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[-8.8626295E-3,-3.922325E-3,4.0787407E-3],"split_indices":[1,0,0],"split_type":[0,0,0],"sum_hessian":[1.4458455E2,6.905801E1,7.5526535E1],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[-3.350957E-2,2.346545E-3,-3.7299043E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":7,"left_children":[1,-1,-1],"loss_changes":[1.3207154E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[1.2120422E-2,2.346545E-3,-3.7299043E-3],"split_indices":[2,0,0],"split_type":[0,0,0],"sum_hessian":[1.431Line truncated
{"learner":{"attributes":{"best_iteration":"131","best_score":"0.6530751986636056","scikit_learn":"{\"_estimator_type\": \"classifier\"}"},"feature_names":["VIX_rank_20","TLT_ret_10","USO_ret_5","SPY_TLT_ratio_ret","SPY_ret_5","SPY_ret_20","SPY_dist_sma50"],"feature_types":["float","float","float","float","float","float","float"],"gradient_booster":{"model":{"cats":{"enc":[],"feature_segments":[],"sorted_idx":[]},"gbtree_model_param":{"num_parallel_tree":"1","num_trees":"152"},"iteration_indptr":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152],"tree_info":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"trees":[{"base_weights":[9.09237E-3,3.342369E-4,-1.4195536E-2],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":0,"left_children":[1,-1,-1],"loss_changes":[5.105532E0,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[6.892705E-2,3.342369E-4,-1.4195536E-2],"split_indices":[6,0,0],"split_type":[0,0,0],"sum_hessian":[1.4451103E2,1.4305133E2,1.4597074E0],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[1.9695353E-2,2.3339752E-3,-3.2156436E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":1,"left_children":[1,-1,-1],"loss_changes":[1.0367284E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[1.4354015E-2,2.3339752E-3,-3.2156436E-3],"split_indices":[3,0,0],"split_type":[0,0,0],"sum_hessian":[1.4012877E2,8.636299E1,5.376578E1],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[9.294399E-2,-1.785295E-3,6.0087293E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":2,"left_children":[1,-1,-1],"loss_changes":[1.9767847E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[7.100592E-3,-1.785295E-3,6.0087293E-3],"split_indices":[1,0,0],"split_type":[0,0,0],"sum_hessian":[1.4158235E2,9.269047E1,4.8891888E1],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[4.908758E-2,7.0679425E-3,-1.0177689E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":3,"left_children":[1,-1,-1],"loss_changes":[1.4627343E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[-1.7963655E-2,7.0679425E-3,-1.0177689E-3],"split_indices":[3,0,0],"split_type":[0,0,0],"sum_hessian":[1.4569156E2,2.6495813E1,1.1919575E2],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[3.9804457E-3,3.4415158E-3,-1.9024174E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":4,"left_children":[1,-1,-1],"loss_changes":[9.772864E0,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[-1.0558252E-2,3.4415158E-3,-1.9024174E-3],"split_indices":[2,0,0],"split_type":[0,0,0],"sum_hessian":[1.4592857E2,5.2756344E1,9.317223E1],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[-2.8463421E-2,6.9694594E-3,-1.250735E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":5,"left_children":[1,-1,-1],"loss_changes":[1.0434825E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[-2.858291E-2,6.9694594E-3,-1.250735E-3],"split_indices":[3,0,0],"split_type":[0,0,0],"sum_hessian":[1.4665097E2,1.6505238E1,1.3014574E2],"tree_param":{"num_deleted":"0","num_feature":"7","num_nodes":"3","size_leaf_vector":"1"}},{"base_weights":[2.73039E-2,-3.935056E-3,4.084229E-3],"categories":[],"categories_nodes":[],"categories_segments":[],"categories_sizes":[],"default_left":[0,0,0],"id":6,"left_children":[1,-1,-1],"loss_changes":[2.3570513E1,0E0,0E0],"parents":[2147483647,0,0],"right_children":[2,-1,-1],"split_conditions":[-9.210526E-3,-3.935056E-3,4.084229E-3],"split_indices":[1,0,0],"split_type":[0,0,0],"sum_hessian":[1.4496594E2,6.888007E1,7.608588E1],"tree_paLine truncated
+2 -2
View File
@@ -1,5 +1,5 @@
{
"p_base": 0.5833333333333334,
"p_base": 0.5819477434679335,
"feature_cols": [
"VIX_rank_20",
"TLT_ret_10",
@@ -9,5 +9,5 @@
"SPY_ret_20",
"SPY_dist_sma50"
],
"last_trained_date": "2026-07-24 00:00:00"
"last_trained_date": "2026-07-29 00:00:00"
}
+151 -151
View File
@@ -20,14 +20,14 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(PosixPath('/home/jarno/repos/trading-bot'),\n",
" PosixPath('/home/jarno/repos/trading-bot/data/ibkr/daily'),\n",
" PosixPath('/home/jarno/repos/trading-bot/data/alpaca/daily'),\n",
" PosixPath('/home/jarno/repos/trading-bot/data/training/spy_direction_5d.parquet'))"
]
},
@@ -87,16 +87,16 @@
"output_type": "stream",
"text": [
"<class 'pandas.DataFrame'>\n",
"DatetimeIndex: 1251 entries, 2021-07-30 to 2026-07-24\n",
"DatetimeIndex: 1255 entries, 2021-08-02 to 2026-07-31\n",
"Data columns (total 4 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 SPY_close 1251 non-null float64\n",
" 1 VIX_close 1251 non-null float64\n",
" 2 TLT_close 1251 non-null float64\n",
" 3 USO_close 1251 non-null float64\n",
" 0 SPY_close 1255 non-null float64\n",
" 1 VIX_close 1255 non-null float64\n",
" 2 TLT_close 1255 non-null float64\n",
" 3 USO_close 1255 non-null float64\n",
"dtypes: float64(4)\n",
"memory usage: 48.9 KB\n"
"memory usage: 49.0 KB\n"
]
},
{
@@ -135,40 +135,40 @@
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2021-07-30</th>\n",
" <td>438.51</td>\n",
" <td>495.4</td>\n",
" <td>149.52</td>\n",
" <td>50.66</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2021-08-02</th>\n",
" <td>437.59</td>\n",
" <td>513.6</td>\n",
" <td>25.68</td>\n",
" <td>150.67</td>\n",
" <td>49.18</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2021-08-03</th>\n",
" <td>441.15</td>\n",
" <td>488.0</td>\n",
" <td>24.40</td>\n",
" <td>150.75</td>\n",
" <td>48.85</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2021-08-04</th>\n",
" <td>438.98</td>\n",
" <td>487.6</td>\n",
" <td>24.38</td>\n",
" <td>151.06</td>\n",
" <td>47.20</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2021-08-05</th>\n",
" <td>441.76</td>\n",
" <td>474.8</td>\n",
" <td>23.74</td>\n",
" <td>150.29</td>\n",
" <td>48.10</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2021-08-06</th>\n",
" <td>442.49</td>\n",
" <td>23.14</td>\n",
" <td>147.78</td>\n",
" <td>47.57</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
@@ -176,11 +176,11 @@
"text/plain": [
" SPY_close VIX_close TLT_close USO_close\n",
"date \n",
"2021-07-30 438.51 495.4 149.52 50.66\n",
"2021-08-02 437.59 513.6 150.67 49.18\n",
"2021-08-03 441.15 488.0 150.75 48.85\n",
"2021-08-04 438.98 487.6 151.06 47.20\n",
"2021-08-05 441.76 474.8 150.29 48.10"
"2021-08-02 437.59 25.68 150.67 49.18\n",
"2021-08-03 441.15 24.40 150.75 48.85\n",
"2021-08-04 438.98 24.38 151.06 47.20\n",
"2021-08-05 441.76 23.74 150.29 48.10\n",
"2021-08-06 442.49 23.14 147.78 47.57"
]
},
"execution_count": 2,
@@ -234,7 +234,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 4,
"metadata": {},
"outputs": [
{
@@ -283,24 +283,12 @@
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2021-10-08</th>\n",
" <td>0.008336</td>\n",
" <td>-0.017017</td>\n",
" <td>-0.011155</td>\n",
" <td>-0.075239</td>\n",
" <td>0.125</td>\n",
" <td>-0.034239</td>\n",
" <td>0.041495</td>\n",
" <td>0.032998</td>\n",
" <td>1.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2021-10-11</th>\n",
" <td>0.014114</td>\n",
" <td>-0.026625</td>\n",
" <td>-0.018145</td>\n",
" <td>-0.090869</td>\n",
" <td>0.200</td>\n",
" <td>0.20</td>\n",
" <td>-0.033135</td>\n",
" <td>0.031015</td>\n",
" <td>0.038908</td>\n",
@@ -312,7 +300,7 @@
" <td>-0.023752</td>\n",
" <td>-0.020386</td>\n",
" <td>-0.074091</td>\n",
" <td>0.100</td>\n",
" <td>0.10</td>\n",
" <td>-0.001041</td>\n",
" <td>0.008628</td>\n",
" <td>-0.001303</td>\n",
@@ -324,7 +312,7 @@
" <td>-0.028356</td>\n",
" <td>-0.016597</td>\n",
" <td>-0.081006</td>\n",
" <td>0.050</td>\n",
" <td>0.05</td>\n",
" <td>0.006928</td>\n",
" <td>0.036928</td>\n",
" <td>-0.005897</td>\n",
@@ -336,12 +324,24 @@
" <td>-0.010443</td>\n",
" <td>-0.000214</td>\n",
" <td>-0.096759</td>\n",
" <td>0.050</td>\n",
" <td>0.05</td>\n",
" <td>0.010809</td>\n",
" <td>0.026192</td>\n",
" <td>-0.011991</td>\n",
" <td>1.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2021-10-15</th>\n",
" <td>0.018294</td>\n",
" <td>0.010127</td>\n",
" <td>0.007213</td>\n",
" <td>-0.079882</td>\n",
" <td>0.05</td>\n",
" <td>-0.002202</td>\n",
" <td>0.030287</td>\n",
" <td>-0.003823</td>\n",
" <td>1.0</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
@@ -349,22 +349,22 @@
"text/plain": [
" SPY_ret_5 SPY_ret_20 SPY_dist_sma50 VIX_change_5 VIX_rank_20 \\\n",
"date \n",
"2021-10-08 0.008336 -0.017017 -0.011155 -0.075239 0.125 \n",
"2021-10-11 0.014114 -0.026625 -0.018145 -0.090869 0.200 \n",
"2021-10-12 0.001201 -0.023752 -0.020386 -0.074091 0.100 \n",
"2021-10-13 0.000644 -0.028356 -0.016597 -0.081006 0.050 \n",
"2021-10-14 0.008754 -0.010443 -0.000214 -0.096759 0.050 \n",
"2021-10-11 0.014114 -0.026625 -0.018145 -0.090869 0.20 \n",
"2021-10-12 0.001201 -0.023752 -0.020386 -0.074091 0.10 \n",
"2021-10-13 0.000644 -0.028356 -0.016597 -0.081006 0.05 \n",
"2021-10-14 0.008754 -0.010443 -0.000214 -0.096759 0.05 \n",
"2021-10-15 0.018294 0.010127 0.007213 -0.079882 0.05 \n",
"\n",
" TLT_ret_10 USO_ret_5 SPY_TLT_ratio_ret spy_up_5d \n",
"date \n",
"2021-10-08 -0.034239 0.041495 0.032998 1.0 \n",
"2021-10-11 -0.033135 0.031015 0.038908 1.0 \n",
"2021-10-12 -0.001041 0.008628 -0.001303 1.0 \n",
"2021-10-13 0.006928 0.036928 -0.005897 1.0 \n",
"2021-10-14 0.010809 0.026192 -0.011991 1.0 "
"2021-10-14 0.010809 0.026192 -0.011991 1.0 \n",
"2021-10-15 -0.002202 0.030287 -0.003823 1.0 "
]
},
"execution_count": 3,
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
@@ -384,7 +384,7 @@
"df[\"TLT_ret_10\"] = df[\"TLT_close\"].pct_change(10)\n",
"df[\"USO_ret_5\"] = df[\"USO_close\"].pct_change(5)\n",
"df[\"SPY_TLT_ratio_ret\"] = (df[\"SPY_close\"] / df[\"TLT_close\"]).pct_change(5)\n",
"df_model\n",
"\n",
"spy_forward_close = df[\"SPY_close\"].shift(-5)\n",
"df[\"spy_up_5d\"] = np.nan\n",
"df.loc[spy_forward_close > df[\"SPY_close\"], \"spy_up_5d\"] = 1.0\n",
@@ -418,7 +418,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 5,
"metadata": {},
"outputs": [
{
@@ -458,24 +458,24 @@
" <tbody>\n",
" <tr>\n",
" <th>train</th>\n",
" <td>837</td>\n",
" <td>2021-10-08</td>\n",
" <td>2025-02-07</td>\n",
" <td>0.583035</td>\n",
" <td>840</td>\n",
" <td>2021-10-11</td>\n",
" <td>2025-02-13</td>\n",
" <td>0.583333</td>\n",
" </tr>\n",
" <tr>\n",
" <th>validation</th>\n",
" <td>179</td>\n",
" <td>2025-02-10</td>\n",
" <td>2025-10-24</td>\n",
" <td>0.653631</td>\n",
" <td>180</td>\n",
" <td>2025-02-14</td>\n",
" <td>2025-10-31</td>\n",
" <td>0.633333</td>\n",
" </tr>\n",
" <tr>\n",
" <th>test</th>\n",
" <td>181</td>\n",
" <td>2025-10-27</td>\n",
" <td>2026-07-17</td>\n",
" <td>0.558011</td>\n",
" <td>2025-11-03</td>\n",
" <td>2026-07-24</td>\n",
" <td>0.569061</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
@@ -484,12 +484,12 @@
"text/plain": [
" rows start_date end_date target_mean\n",
"split \n",
"train 837 2021-10-08 2025-02-07 0.583035\n",
"validation 179 2025-02-10 2025-10-24 0.653631\n",
"test 181 2025-10-27 2026-07-17 0.558011"
"train 840 2021-10-11 2025-02-13 0.583333\n",
"validation 180 2025-02-14 2025-10-31 0.633333\n",
"test 181 2025-11-03 2026-07-24 0.569061"
]
},
"execution_count": 4,
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
@@ -543,17 +543,17 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Rows: 1,197\n",
"Rows: 1,201\n",
"Feature columns: ['SPY_ret_5', 'SPY_ret_20', 'SPY_dist_sma50', 'VIX_change_5', 'VIX_rank_20', 'TLT_ret_10', 'USO_ret_5', 'SPY_TLT_ratio_ret']\n",
"Target column: spy_up_5d\n",
"Training matrix shape: (837, 8)\n"
"Training matrix shape: (840, 8)\n"
]
},
{
@@ -592,16 +592,16 @@
" <tbody>\n",
" <tr>\n",
" <th>count</th>\n",
" <td>1197.000000</td>\n",
" <td>1197.000000</td>\n",
" <td>1197.000000</td>\n",
" <td>1197.000000</td>\n",
" <td>1197.000000</td>\n",
" <td>1197.000000</td>\n",
" <td>1197.000000</td>\n",
" <td>1197.000000</td>\n",
" <td>1197.000000</td>\n",
" <td>1197</td>\n",
" <td>1201.000000</td>\n",
" <td>1201.000000</td>\n",
" <td>1201.000000</td>\n",
" <td>1201.000000</td>\n",
" <td>1201.000000</td>\n",
" <td>1201.000000</td>\n",
" <td>1201.000000</td>\n",
" <td>1201.000000</td>\n",
" <td>1201.000000</td>\n",
" <td>1201</td>\n",
" </tr>\n",
" <tr>\n",
" <th>unique</th>\n",
@@ -640,32 +640,32 @@
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>837</td>\n",
" <td>840</td>\n",
" </tr>\n",
" <tr>\n",
" <th>mean</th>\n",
" <td>0.002556</td>\n",
" <td>0.009841</td>\n",
" <td>0.011067</td>\n",
" <td>-0.008646</td>\n",
" <td>0.387009</td>\n",
" <td>-0.004083</td>\n",
" <td>0.004662</td>\n",
" <td>0.004927</td>\n",
" <td>0.589808</td>\n",
" <td>0.002502</td>\n",
" <td>0.009848</td>\n",
" <td>0.011028</td>\n",
" <td>0.017763</td>\n",
" <td>0.403476</td>\n",
" <td>-0.004099</td>\n",
" <td>0.005024</td>\n",
" <td>0.004880</td>\n",
" <td>0.588676</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>std</th>\n",
" <td>0.023081</td>\n",
" <td>0.043634</td>\n",
" <td>0.037371</td>\n",
" <td>0.092413</td>\n",
" <td>0.333697</td>\n",
" <td>0.028557</td>\n",
" <td>0.052319</td>\n",
" <td>0.027656</td>\n",
" <td>0.492074</td>\n",
" <td>0.023057</td>\n",
" <td>0.043558</td>\n",
" <td>0.037315</td>\n",
" <td>0.299417</td>\n",
" <td>0.337501</td>\n",
" <td>0.028502</td>\n",
" <td>0.052628</td>\n",
" <td>0.027606</td>\n",
" <td>0.492279</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
@@ -683,40 +683,40 @@
" </tr>\n",
" <tr>\n",
" <th>25%</th>\n",
" <td>-0.009535</td>\n",
" <td>-0.017017</td>\n",
" <td>-0.008847</td>\n",
" <td>-0.058376</td>\n",
" <td>0.075000</td>\n",
" <td>-0.023517</td>\n",
" <td>-0.026200</td>\n",
" <td>-0.009601</td>\n",
" <td>-0.016509</td>\n",
" <td>-0.008656</td>\n",
" <td>-0.056615</td>\n",
" <td>0.100000</td>\n",
" <td>-0.023304</td>\n",
" <td>-0.025866</td>\n",
" <td>-0.010013</td>\n",
" <td>0.000000</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>50%</th>\n",
" <td>0.003869</td>\n",
" <td>0.015797</td>\n",
" <td>0.017906</td>\n",
" <td>-0.020655</td>\n",
" <td>0.250000</td>\n",
" <td>-0.004339</td>\n",
" <td>0.004817</td>\n",
" <td>0.005710</td>\n",
" <td>0.003798</td>\n",
" <td>0.015725</td>\n",
" <td>0.017802</td>\n",
" <td>-0.018570</td>\n",
" <td>0.300000</td>\n",
" <td>-0.004435</td>\n",
" <td>0.004950</td>\n",
" <td>0.005537</td>\n",
" <td>1.000000</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>75%</th>\n",
" <td>0.015957</td>\n",
" <td>0.038385</td>\n",
" <td>0.038332</td>\n",
" <td>0.027778</td>\n",
" <td>0.700000</td>\n",
" <td>0.014272</td>\n",
" <td>0.031883</td>\n",
" <td>0.021406</td>\n",
" <td>0.015955</td>\n",
" <td>0.038179</td>\n",
" <td>0.038282</td>\n",
" <td>0.030814</td>\n",
" <td>0.750000</td>\n",
" <td>0.014227</td>\n",
" <td>0.032486</td>\n",
" <td>0.021390</td>\n",
" <td>1.000000</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
@@ -724,8 +724,8 @@
" <th>max</th>\n",
" <td>0.082843</td>\n",
" <td>0.157566</td>\n",
" <td>0.088103</td>\n",
" <td>0.937828</td>\n",
" <td>0.088105</td>\n",
" <td>3.846154</td>\n",
" <td>1.000000</td>\n",
" <td>0.091322</td>\n",
" <td>0.327273</td>\n",
@@ -739,33 +739,33 @@
],
"text/plain": [
" SPY_ret_5 SPY_ret_20 SPY_dist_sma50 VIX_change_5 VIX_rank_20 \\\n",
"count 1197.000000 1197.000000 1197.000000 1197.000000 1197.000000 \n",
"count 1201.000000 1201.000000 1201.000000 1201.000000 1201.000000 \n",
"unique NaN NaN NaN NaN NaN \n",
"top NaN NaN NaN NaN NaN \n",
"freq NaN NaN NaN NaN NaN \n",
"mean 0.002556 0.009841 0.011067 -0.008646 0.387009 \n",
"std 0.023081 0.043634 0.037371 0.092413 0.333697 \n",
"mean 0.002502 0.009848 0.011028 0.017763 0.403476 \n",
"std 0.023057 0.043558 0.037315 0.299417 0.337501 \n",
"min -0.114962 -0.123975 -0.141995 -0.387709 0.050000 \n",
"25% -0.009535 -0.017017 -0.008847 -0.058376 0.075000 \n",
"50% 0.003869 0.015797 0.017906 -0.020655 0.250000 \n",
"75% 0.015957 0.038385 0.038332 0.027778 0.700000 \n",
"max 0.082843 0.157566 0.088103 0.937828 1.000000 \n",
"25% -0.009601 -0.016509 -0.008656 -0.056615 0.100000 \n",
"50% 0.003798 0.015725 0.017802 -0.018570 0.300000 \n",
"75% 0.015955 0.038179 0.038282 0.030814 0.750000 \n",
"max 0.082843 0.157566 0.088105 3.846154 1.000000 \n",
"\n",
" TLT_ret_10 USO_ret_5 SPY_TLT_ratio_ret spy_up_5d split \n",
"count 1197.000000 1197.000000 1197.000000 1197.000000 1197 \n",
"count 1201.000000 1201.000000 1201.000000 1201.000000 1201 \n",
"unique NaN NaN NaN NaN 3 \n",
"top NaN NaN NaN NaN train \n",
"freq NaN NaN NaN NaN 837 \n",
"mean -0.004083 0.004662 0.004927 0.589808 NaN \n",
"std 0.028557 0.052319 0.027656 0.492074 NaN \n",
"freq NaN NaN NaN NaN 840 \n",
"mean -0.004099 0.005024 0.004880 0.588676 NaN \n",
"std 0.028502 0.052628 0.027606 0.492279 NaN \n",
"min -0.092880 -0.196652 -0.117208 0.000000 NaN \n",
"25% -0.023517 -0.026200 -0.010013 0.000000 NaN \n",
"50% -0.004339 0.004817 0.005710 1.000000 NaN \n",
"75% 0.014272 0.031883 0.021406 1.000000 NaN \n",
"25% -0.023304 -0.025866 -0.010013 0.000000 NaN \n",
"50% -0.004435 0.004950 0.005537 1.000000 NaN \n",
"75% 0.014227 0.032486 0.021390 1.000000 NaN \n",
"max 0.091322 0.327273 0.129204 1.000000 NaN "
]
},
"execution_count": 5,
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
@@ -787,30 +787,30 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(PosixPath('/home/jarno/repos/trading-bot/data/training/spy_direction_5d.parquet'),\n",
" (1197, 11),\n",
" (1201, 11),\n",
" date SPY_ret_5 SPY_ret_20 SPY_dist_sma50 VIX_change_5 \\\n",
" 0 2021-10-08 0.008336 -0.017017 -0.011155 -0.075239 \n",
" 1 2021-10-11 0.014114 -0.026625 -0.018145 -0.090869 \n",
" 2 2021-10-12 0.001201 -0.023752 -0.020386 -0.074091 \n",
" 3 2021-10-13 0.000644 -0.028356 -0.016597 -0.081006 \n",
" 4 2021-10-14 0.008754 -0.010443 -0.000214 -0.096759 \n",
" 0 2021-10-11 0.014114 -0.026625 -0.018145 -0.090869 \n",
" 1 2021-10-12 0.001201 -0.023752 -0.020386 -0.074091 \n",
" 2 2021-10-13 0.000644 -0.028356 -0.016597 -0.081006 \n",
" 3 2021-10-14 0.008754 -0.010443 -0.000214 -0.096759 \n",
" 4 2021-10-15 0.018294 0.010127 0.007213 -0.079882 \n",
" \n",
" VIX_rank_20 TLT_ret_10 USO_ret_5 SPY_TLT_ratio_ret spy_up_5d split \n",
" 0 0.125 -0.034239 0.041495 0.032998 1.0 train \n",
" 1 0.200 -0.033135 0.031015 0.038908 1.0 train \n",
" 2 0.100 -0.001041 0.008628 -0.001303 1.0 train \n",
" 3 0.050 0.006928 0.036928 -0.005897 1.0 train \n",
" 4 0.050 0.010809 0.026192 -0.011991 1.0 train )"
" 0 0.20 -0.033135 0.031015 0.038908 1.0 train \n",
" 1 0.10 -0.001041 0.008628 -0.001303 1.0 train \n",
" 2 0.05 0.006928 0.036928 -0.005897 1.0 train \n",
" 3 0.05 0.010809 0.026192 -0.011991 1.0 train \n",
" 4 0.05 -0.002202 0.030287 -0.003823 1.0 train )"
]
},
"execution_count": 6,
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
+336
View File
@@ -0,0 +1,336 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "882d4907",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import xgboost as xgb\n",
"from sklearn.metrics import classification_report, accuracy_score\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "947722b7",
"metadata": {},
"outputs": [],
"source": [
"\n",
"# 1. Load data\n",
"df = pd.read_parquet(\"../data/training/spy_direction_5d.parquet\")\n",
"\n",
"# 2. Define features & target\n",
"target_col = \"spy_up_5d\"\n",
"feature_cols = [\n",
" \"VIX_rank_20\",\n",
" \"TLT_ret_10\",\n",
" \"USO_ret_5\",\n",
" \"SPY_TLT_ratio_ret\",\n",
" \"SPY_ret_5\",\n",
" \"SPY_ret_20\",\n",
" \"SPY_dist_sma50\"\n",
"]"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "5b02ce5e",
"metadata": {},
"outputs": [],
"source": [
"# 3. Create three distinct splits\n",
"train_df = df[df[\"split\"] == \"train\"]\n",
"val_df = df[df[\"split\"] == \"validation\"] # Adjust string if named \"val\"\n",
"test_df = df[df[\"split\"] == \"test\"]\n",
"\n",
"X_train, y_train = train_df[feature_cols], train_df[target_col]\n",
"X_val, y_val = val_df[feature_cols], val_df[target_col]\n",
"X_test, y_test = test_df[feature_cols], test_df[target_col]"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "93a128a7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train positive %: 0.5819477434679335\n",
"Val positive %: 0.6388888888888888\n",
"Test positive %: 0.5769230769230769\n",
"\n",
"Train Feature Correlations with Target:\n",
"VIX_rank_20 -0.004576\n",
"TLT_ret_10 0.162370\n",
"USO_ret_5 -0.113602\n",
"SPY_TLT_ratio_ret -0.111551\n",
"SPY_ret_5 -0.001073\n",
"SPY_ret_20 0.006523\n",
"SPY_dist_sma50 -0.035773\n",
"dtype: float64\n",
"\n",
"Stump Model Best Iteration: 22\n"
]
}
],
"source": [
"# 1. Check target distribution across splits\n",
"print(\"Train positive %:\", y_train.mean())\n",
"print(\"Val positive %: \", y_val.mean())\n",
"print(\"Test positive %: \", y_test.mean())\n",
"\n",
"# 2. Check simple linear correlation with target\n",
"print(\"\\nTrain Feature Correlations with Target:\")\n",
"print(train_df[feature_cols].apply(lambda col: col.corr(y_train)))\n",
"\n",
"# 3. Test a super simple model (Decision Stump)\n",
"stump_model = xgb.XGBClassifier(\n",
" n_estimators=100,\n",
" max_depth=2, # Decision stumps (1 split per tree, minimal overfitting)\n",
" learning_rate=0.01,\n",
" early_stopping_rounds=15,\n",
" eval_metric=\"logloss\"\n",
")\n",
"stump_model.fit(\n",
" X_train, y_train,\n",
" eval_set=[(X_train, y_train), (X_val, y_val)],\n",
" verbose=False\n",
")\n",
"print(f\"\\nStump Model Best Iteration: {stump_model.best_iteration}\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "2f82ce9d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Optimal number of trees (best iteration): 131\n",
"Starting Validation Loss: 0.6606\n",
"Final Validation Loss: 0.6537\n"
]
}
],
"source": [
"\n",
"# 4. Initialize XGBoost Classifier with Early Stopping\n",
"model = xgb.XGBClassifier(\n",
" n_estimators=300, # Set higher; early stopping will truncate training automatically\n",
" max_depth=1, # Keep shallow to prevent memorizing noise\n",
" learning_rate=0.01,\n",
" subsample=0.7,\n",
" colsample_bytree=0.7,\n",
" early_stopping_rounds=20, # Stop if validation loss stops improving for 15 rounds\n",
" eval_metric=\"logloss\",\n",
" random_state=42\n",
")\n",
"\n",
"# 5. Fit using Validation set to monitor performance\n",
"model.fit(\n",
" X_train, y_train,\n",
" eval_set=[(X_train, y_train), (X_val, y_val)],\n",
" verbose=False\n",
")\n",
"\n",
"print(f\"Optimal number of trees (best iteration): {model.best_iteration}\")\n",
"\n",
"evals = model.evals_result()\n",
"val_loss = evals['validation_1']['logloss']\n",
"print(f\"Starting Validation Loss: {val_loss[0]:.4f}\")\n",
"print(f\"Final Validation Loss: {val_loss[-1]:.4f}\")\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "2d54aaee",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"--- Final Holdout Test Performance ---\n",
"Test Set Accuracy: 57.69%\n",
"\n",
"Classification Report:\n",
" precision recall f1-score support\n",
"\n",
" 0.0 0.00 0.00 0.00 77\n",
" 1.0 0.58 1.00 0.73 105\n",
"\n",
" accuracy 0.58 182\n",
" macro avg 0.29 0.50 0.37 182\n",
"weighted avg 0.33 0.58 0.42 182\n",
"\n",
"classification_report with custom threshold (0.5819477434679335):\n",
" precision recall f1-score support\n",
"\n",
" 0.0 0.46 0.74 0.57 77\n",
" 1.0 0.66 0.37 0.48 105\n",
"\n",
" accuracy 0.53 182\n",
" macro avg 0.56 0.56 0.52 182\n",
"weighted avg 0.58 0.53 0.52 182\n",
"\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/jarno/repos/trading-bot/.venv/lib/python3.11/site-packages/sklearn/metrics/_classification.py:1879: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.\n",
" _warn_prf(average, modifier, f\"{metric.capitalize()} is\", result.shape[0])\n",
"/home/jarno/repos/trading-bot/.venv/lib/python3.11/site-packages/sklearn/metrics/_classification.py:1879: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.\n",
" _warn_prf(average, modifier, f\"{metric.capitalize()} is\", result.shape[0])\n",
"/home/jarno/repos/trading-bot/.venv/lib/python3.11/site-packages/sklearn/metrics/_classification.py:1879: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.\n",
" _warn_prf(average, modifier, f\"{metric.capitalize()} is\", result.shape[0])\n"
]
},
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 640x480 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"\n",
"# 6. Evaluate strictly on the holdout Test Set\n",
"y_pred = model.predict(X_test)\n",
"y_proba = model.predict_proba(X_test)[:, 1]\n",
"\n",
"print(\"\\n--- Final Holdout Test Performance ---\")\n",
"print(f\"Test Set Accuracy: {accuracy_score(y_test, y_pred):.2%}\\n\")\n",
"print(\"Classification Report:\")\n",
"print(classification_report(y_test, y_pred))\n",
"threshold = y_train.mean()\n",
"y_pred_custom = (y_proba >= threshold).astype(int)\n",
"print(f\"classification_report with custom threshold ({threshold}):\")\n",
"print(classification_report(y_test, y_pred_custom))\n",
"\n",
"# 7. Feature Importance\n",
"xgb.plot_importance(model, importance_type=\"gain\")\n",
"plt.title(\"Feature Importance (Gain)\")\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "1e4b1067",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Min probability: 0.5154\n",
"Max probability: 0.6891\n",
"Mean probability: 0.5719\n",
"Median probability: 0.5641\n"
]
}
],
"source": [
"# 1. Inspect raw predicted probabilities for the test set\n",
"y_proba_test = model.predict_proba(X_test)[:, 1]\n",
"\n",
"print(f\"Min probability: {y_proba_test.min():.4f}\")\n",
"print(f\"Max probability: {y_proba_test.max():.4f}\")\n",
"print(f\"Mean probability: {y_proba_test.mean():.4f}\")\n",
"print(f\"Median probability: {np.median(y_proba_test):.4f}\")"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "0f415ee9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Successfully saved model to models/spy_xgb_v1.json\n"
]
}
],
"source": [
"import json\n",
"from pathlib import Path\n",
"\n",
"# 1. Create the 'models' directory if it doesn't exist\n",
"models_dir = Path(\"models\")\n",
"models_dir.mkdir(parents=True, exist_ok=True)\n",
"\n",
"# 2. Define paths\n",
"model_path = models_dir / \"spy_xgb_v1.json\"\n",
"meta_path = models_dir / \"spy_xgb_v1_meta.json\"\n",
"\n",
"# 3. Save XGBoost model (explicitly converted to str)\n",
"model.save_model(str(model_path))\n",
"\n",
"# 4. Save metadata alongside model\n",
"metadata = {\n",
" \"p_base\": float(y_train.mean()),\n",
" \"feature_cols\": feature_cols,\n",
" \"last_trained_date\": str(df[\"date\"].iloc[-1]),\n",
"}\n",
"\n",
"with open(meta_path, \"w\") as f:\n",
" json.dump(metadata, f, indent=2)\n",
"\n",
"print(f\"Successfully saved model to {model_path}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4d73c06a",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.15"
}
},
"nbformat": 4,
"nbformat_minor": 5
}