{ "cells": [ { "cell_type": "markdown", "id": "66be0f9b-942f-421c-882b-5cf6a8d225ef", "metadata": {}, "source": [ "# Creating an AI Trading Bot using Machine Learning with help of AI" ] }, { "cell_type": "markdown", "id": "f70ecd95-f883-40e1-a73d-1ade318c8783", "metadata": {}, "source": [ "## Used tools" ] }, { "cell_type": "markdown", "id": "314ddf94-059a-452a-8098-22b1ec78d5b1", "metadata": {}, "source": [ "- https://aistudio.google.com\n", "- https://alpaca.markets/\n", "- https://jupyter.org/\n", "- https://superai.pl/courses.html" ] }, { "cell_type": "markdown", "id": "2b8e5201-a668-4b6a-9be0-5172b5e67b95", "metadata": {}, "source": [ "## Creating AI Trading Bot with AI" ] }, { "cell_type": "markdown", "id": "9d07ca5d-c625-496a-bd5c-5efa72b3e404", "metadata": {}, "source": [ "### 1st Prompt" ] }, { "cell_type": "markdown", "id": "bccd6bdd-6e98-4f76-bd50-eb0f13f5636c", "metadata": {}, "source": [ "1. \"I would like to create a trading bot. It should be using machine learning to trade BTCUSD in one minute intervals and Alpaca Markets. Can you help me with that?\"\n", "\n", "(Remember: At the moment Alpaca uses different symbols for executing orders such as “BTC/USD” and checking portfolio positions such as “BTCUSD”.)\n" ] }, { "cell_type": "markdown", "id": "6cf0ec94-cdb3-47e5-918e-5ba2cfd6f16e", "metadata": {}, "source": [ "#### Keys to Alpaca" ] }, { "cell_type": "code", "execution_count": 1, "id": "7d232c4b-7f4a-4f9d-b557-1abe46047672", "metadata": {}, "outputs": [], "source": [ "KEY_ID = \"your API KEY\" #replace it with your own KEY_ID from Alpaca: https://alpaca.markets/\n", "SECRET_KEY = \"your SECRET KEY\" #replace it with your own SECRET_KEY from Alpaca" ] }, { "cell_type": "markdown", "id": "ea5c66d3-6c54-49a0-800e-7dd663fec83c", "metadata": {}, "source": [ "### 1st Attempt" ] }, { "cell_type": "code", "execution_count": 2, "id": "56fce22c-c529-40c1-ae46-3576bcb75475", "metadata": {}, "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings('ignore')" ] }, { "cell_type": "markdown", "id": "86801639-ce2c-4bac-bf23-94ab71cf70b7", "metadata": {}, "source": [ "Step 1: Alpaca Setup & Python Environment\n", "\n", " Sign up for an Alpaca account: https://alpaca.markets/\n", "\n", " Get your API Key ID and Secret Key from your paper trading account dashboard.\n", "\n", " Install necessary Python libraries:" ] }, { "cell_type": "code", "execution_count": 3, "id": "91850edf-7220-407e-a577-7a8cf4ee6ca9", "metadata": { "scrolled": true }, "outputs": [], "source": [ "pip install alpaca-trade-api pandas scikit-learn xgboost numpy pandas-ta" ] }, { "cell_type": "markdown", "id": "41adbca4-5621-4491-8541-3dd4f52eed2b", "metadata": {}, "source": [ "Set up API credentials (environment variables are best):" ] }, { "cell_type": "code", "execution_count": 4, "id": "bc5e6181-d205-476d-8c17-8cecda0c01ed", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Connected to Alpaca Paper Trading.\n" ] } ], "source": [ "import os\n", "import alpaca_trade_api as tradeapi\n", "import pandas as pd\n", "from datetime import datetime, timedelta\n", "import time\n", "\n", "# For Paper Trading\n", "os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets'\n", "# Replace with your actual keys or set them as environment variables\n", "os.environ['APCA_API_KEY_ID'] = 'your API KEY'\n", "os.environ['APCA_API_SECRET_KEY'] = 'your SECRET KEY'\n", "\n", "API_KEY = os.getenv('APCA_API_KEY_ID')\n", "API_SECRET = os.getenv('APCA_API_SECRET_KEY')\n", "BASE_URL = os.getenv('APCA_API_BASE_URL')\n", "\n", "api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')\n", "\n", "print(\"Connected to Alpaca Paper Trading.\")\n", "# Check account\n", "# account = api.get_account()\n", "# print(f\"Account status: {account.status}\")" ] }, { "cell_type": "markdown", "id": "45682be7-b549-422b-8a7d-1d9dbd8f2672", "metadata": {}, "source": [ "Step 2: Data Acquisition" ] }, { "cell_type": "code", "execution_count": 5, "id": "923fea7c-910e-45cb-b19f-3b7a3002322c", "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "module 'alpaca_trade_api' has no attribute 'টাইমফ্রেম'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[1;32mIn[5], line 61\u001b[0m\n\u001b[0;32m 59\u001b[0m \u001b[38;5;66;03m# Example usage:\u001b[39;00m\n\u001b[0;32m 60\u001b[0m symbol \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBTC/USD\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;66;03m# Alpaca uses \"BTC/USD\" for crypto pairs\u001b[39;00m\n\u001b[1;32m---> 61\u001b[0m timeframe \u001b[38;5;241m=\u001b[39m tradeapi\u001b[38;5;241m.\u001b[39m টাইমফ্রেম\u001b[38;5;241m.\u001b[39mমিনিট \u001b[38;5;66;03m# or \"1Min\" for older SDK versions\u001b[39;00m\n\u001b[0;32m 62\u001b[0m \u001b[38;5;66;03m# Fetch last 30 days of data for example\u001b[39;00m\n\u001b[0;32m 63\u001b[0m start_date \u001b[38;5;241m=\u001b[39m (datetime\u001b[38;5;241m.\u001b[39mnow() \u001b[38;5;241m-\u001b[39m timedelta(days\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m30\u001b[39m))\u001b[38;5;241m.\u001b[39mstrftime(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mY-\u001b[39m\u001b[38;5;124m%\u001b[39m\u001b[38;5;124mm-\u001b[39m\u001b[38;5;132;01m%d\u001b[39;00m\u001b[38;5;124m'\u001b[39m)\n", "\u001b[1;31mAttributeError\u001b[0m: module 'alpaca_trade_api' has no attribute 'টাইমফ্রেম'" ] } ], "source": [ "def fetch_data(symbol, timeframe, start_date_str, end_date_str=None):\n", " \"\"\"Fetches historical crypto data from Alpaca.\"\"\"\n", " if end_date_str is None:\n", " end_date_str = datetime.now().strftime('%Y-%m-%d')\n", "\n", " # Alpaca API expects ISO 8601 format for start/end times\n", " # And it has a limit on how many bars can be fetched per request (e.g., 10000 for crypto)\n", " # So we may need to fetch in chunks if requesting a long period.\n", "\n", " all_bars = []\n", " start_dt = pd.to_datetime(start_date_str, utc=True)\n", " end_dt = pd.to_datetime(end_date_str, utc=True)\n", "\n", " # Fetch data in chunks to avoid hitting API limits for very long periods\n", " # For 1-minute data, 10000 bars is about 7 days.\n", " # Let's fetch data in smaller chunks, e.g., 5 days at a time.\n", " current_start = start_dt\n", " while current_start < end_dt:\n", " chunk_end = min(current_start + timedelta(days=5), end_dt) # Adjust chunk size as needed\n", " print(f\"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}\")\n", "\n", " # Alpaca's get_crypto_bars expects start and end in ISO format\n", " bars = api.get_crypto_bars(\n", " symbol,\n", " timeframe,\n", " start=current_start.isoformat(),\n", " end=chunk_end.isoformat(),\n", " limit=10000 # Max limit per request\n", " ).df\n", "\n", " if bars.empty:\n", " print(f\"No data found for chunk starting {current_start.isoformat()}\")\n", " if current_start + timedelta(days=5) > end_dt and not all_bars: # if first chunk and no data\n", " break\n", " current_start += timedelta(days=5) # Move to next chunk period\n", " time.sleep(1) # Be nice to the API\n", " continue\n", "\n", " all_bars.append(bars)\n", " # Update current_start to the timestamp of the last bar fetched + 1 minute\n", " # to avoid overlap and to ensure we move forward.\n", " if not bars.index.empty:\n", " current_start = bars.index[-1] + pd.Timedelta(minutes=1)\n", " else: # Should not happen if bars is not empty, but as a safeguard\n", " current_start += timedelta(days=5)\n", "\n", " time.sleep(1) # Respect API rate limits\n", "\n", " if not all_bars:\n", " print(\"No data fetched. Check your date range or symbol.\")\n", " return pd.DataFrame()\n", "\n", " df = pd.concat(all_bars)\n", " df = df[~df.index.duplicated(keep='first')] # Remove potential duplicates from chunking\n", " df = df.sort_index() # Ensure chronological order\n", " df = df[df.index <= end_dt] # Ensure we don't go past the requested end_dt\n", " return df\n", "\n", "# Example usage:\n", "symbol = \"BTC/USD\" # Alpaca uses \"BTC/USD\" for crypto pairs\n", "timeframe = tradeapi. টাইমফ্রেম.মিনিট # or \"1Min\" for older SDK versions\n", "# Fetch last 30 days of data for example\n", "start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')\n", "# end_date = datetime.now().strftime('%Y-%m-%d') # Fetches up to now\n", "\n", "btc_data = fetch_data(symbol, timeframe, start_date)\n", "if not btc_data.empty:\n", " print(f\"Fetched {len(btc_data)} rows of {symbol} data.\")\n", " print(btc_data.head())\n", " print(btc_data.tail())\n", "else:\n", " print(\"Failed to fetch data.\")" ] }, { "cell_type": "markdown", "id": "dc7a0229-e758-4df6-a8b1-1dbe94c6f7a5", "metadata": {}, "source": [ "Step 3: Feature Engineering\n", "\n", "This is where you create signals for your model. pandas-ta is a great library for this." ] }, { "cell_type": "code", "execution_count": null, "id": "c3eac019-211a-4630-95c9-9fcc61e358ec", "metadata": {}, "outputs": [], "source": [ "import pandas_ta as ta\n", "\n", "def create_features(df):\n", " if df.empty or 'close' not in df.columns:\n", " print(\"DataFrame is empty or 'close' column is missing.\")\n", " return df\n", "\n", " df.ta.sma(length=10, append=True, col_names=('SMA_10'))\n", " df.ta.sma(length=30, append=True, col_names=('SMA_30'))\n", " df.ta.ema(length=10, append=True, col_names=('EMA_10'))\n", " df.ta.rsi(length=14, append=True, col_names=('RSI_14'))\n", " df.ta.macd(append=True, col_names=('MACD_12_26_9', 'MACDh_12_26_9', 'MACDs_12_26_9'))\n", " df.ta.bbands(length=20, append=True, col_names=('BBL_20_2.0', 'BBM_20_2.0', 'BBU_20_2.0', 'BBB_20_2.0', 'BBP_20_2.0'))\n", " df.ta.atr(length=14, append=True, col_names=('ATR_14'))\n", "\n", " # Lagged returns\n", " for lag in [1, 3, 5, 10]:\n", " df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)\n", "\n", " # Add more features: volatility, momentum, volume-based if available, etc.\n", " # e.g., log returns, price relative to moving average, etc.\n", "\n", " df.dropna(inplace=True) # Remove rows with NaNs created by indicators\n", " return df\n", "\n", "if not btc_data.empty:\n", " featured_data = create_features(btc_data.copy()) # Use .copy() to avoid modifying original\n", " print(\"\\nData with features:\")\n", " print(featured_data.head())\n", "else:\n", " print(\"Cannot create features, btc_data is empty.\")\n", " featured_data = pd.DataFrame() # ensure it's a DataFrame" ] }, { "cell_type": "markdown", "id": "8d1536cf-2ee0-4a30-ac4e-9fa82c8936c7", "metadata": {}, "source": [ "Step 4: Model Training - Defining Target & Training\n", "\n", "Let's define a simple target: will the price be higher or lower in N minutes?" ] }, { "cell_type": "code", "execution_count": null, "id": "8e773d98-f353-4ab4-b2a1-5d82024865b0", "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import train_test_split, TimeSeriesSplit\n", "from sklearn.ensemble import RandomForestClassifier # Example model\n", "from sklearn.metrics import accuracy_score, classification_report\n", "import xgboost as xgb\n", "\n", "def prepare_data_for_model(df, target_horizon=5, test_size=0.2):\n", " \"\"\"\n", " Prepares X (features) and y (target) for the ML model.\n", " Target: 1 if price increases by more than a small threshold after target_horizon periods, 0 otherwise.\n", " -1 if price decreases by more than a small threshold. (Optional: make it 3 classes)\n", " \"\"\"\n", " if df.empty or 'close' not in df.columns:\n", " print(\"DataFrame is empty or 'close' column is missing.\")\n", " return pd.DataFrame(), pd.Series(dtype='float64'), pd.DataFrame(), pd.Series(dtype='float64')\n", "\n", " # Define target: 1 if price goes up in `target_horizon` minutes, 0 otherwise\n", " # A small threshold can help avoid noise around 0% change\n", " # price_threshold = 0.0005 # e.g., 0.05% change\n", " # df['future_price'] = df['close'].shift(-target_horizon)\n", " # df['price_change'] = (df['future_price'] - df['close']) / df['close']\n", " # df['target'] = 0 # Hold\n", " # df.loc[df['price_change'] > price_threshold, 'target'] = 1 # Buy\n", " # df.loc[df['price_change'] < -price_threshold, 'target'] = -1 # Sell (for 3-class)\n", " # For 2-class (Up/Not Up):\n", " df['target'] = (df['close'].shift(-target_horizon) > df['close']).astype(int)\n", "\n", " df.dropna(inplace=True) # Remove rows with NaN target (due to shift)\n", "\n", " feature_columns = [col for col in df.columns if col not in ['open', 'high', 'low', 'close', 'volume', 'trade_count', 'vwap', 'target', 'future_price', 'price_change']]\n", " X = df[feature_columns]\n", " y = df['target']\n", "\n", " # Time series split is crucial: DO NOT shuffle time series data for training\n", " # For a simple split:\n", " split_index = int(len(X) * (1 - test_size))\n", " X_train, X_test = X[:split_index], X[split_index:]\n", " y_train, y_test = y[:split_index], y[split_index:]\n", "\n", " # For more robust cross-validation, use TimeSeriesSplit\n", " # tscv = TimeSeriesSplit(n_splits=5)\n", " # for train_index, test_index in tscv.split(X):\n", " # X_train, X_test = X.iloc[train_index], X.iloc[test_index]\n", " # y_train, y_test = y.iloc[train_index], y.iloc[test_index]\n", " # Train and evaluate your model here\n", "\n", " return X_train, X_test, y_train, y_test, feature_columns\n", "\n", "\n", "if not featured_data.empty:\n", " X_train, X_test, y_train, y_test, feature_cols = prepare_data_for_model(featured_data.copy(), target_horizon=5)\n", "\n", " if not X_train.empty:\n", " print(f\"\\nTraining data shape: X_train: {X_train.shape}, y_train: {y_train.shape}\")\n", " print(f\"Test data shape: X_test: {X_test.shape}, y_test: {y_test.shape}\")\n", " print(f\"Features used: {feature_cols}\")\n", "\n", " # Example Model: Random Forest\n", " # model = RandomForestClassifier(n_estimators=100, random_state=42, class_weight='balanced')\n", "\n", " # Example Model: XGBoost (often performs well)\n", " model = xgb.XGBClassifier(\n", " objective='binary:logistic', # or 'multi:softprob' for multi-class\n", " n_estimators=100,\n", " learning_rate=0.1,\n", " max_depth=3,\n", " use_label_encoder=False, # Suppress a warning\n", " eval_metric='logloss' # or 'mlogloss' for multi-class\n", " )\n", "\n", " model.fit(X_train, y_train)\n", "\n", " # Evaluate on test set\n", " y_pred = model.predict(X_test)\n", " print(\"\\nModel Evaluation on Test Set:\")\n", " print(f\"Accuracy: {accuracy_score(y_test, y_pred):.4f}\")\n", " print(classification_report(y_test, y_pred, zero_division=0))\n", "\n", " # Feature importance (for tree-based models)\n", " if hasattr(model, 'feature_importances_'):\n", " importances = pd.Series(model.feature_importances_, index=X_train.columns).sort_values(ascending=False)\n", " print(\"\\nFeature Importances:\")\n", " print(importances.head(10))\n", " else:\n", " print(\"Not enough data to create training/test sets after feature engineering and target creation.\")\n", "else:\n", " print(\"Cannot prepare data for model, featured_data is empty.\")\n", " model = None # Ensure model is defined even if training fails" ] }, { "cell_type": "markdown", "id": "d5279216-4774-4173-a268-a1ca92893ac8", "metadata": {}, "source": [ "Important Considerations for Modeling:\n", "\n", " Target Definition: This is critical. Predicting direction is hard. Predicting magnitude or using a threshold (e.g., price must move > 0.1% to be a \"1\") can be better.\n", "\n", " Class Imbalance: If \"up\" signals are rare, your model might be biased. Use techniques like class_weight='balanced' (for some models) or over/undersampling (e.g., SMOTE).\n", "\n", " Stationarity: Price series are generally non-stationary. Features like returns or indicators often help.\n", "\n", " Overfitting: Models can learn noise from historical data. Robust cross-validation (like TimeSeriesSplit) and regularization are key.\n", "\n", "Step 5: Backtesting (Simplified Vectorized Example)\n", "\n", "A proper backtest is event-driven and considers transaction costs, slippage, etc. This is a very simplified version." ] }, { "cell_type": "code", "execution_count": null, "id": "72a69e0a-484d-490f-8c95-195cccfe8e40", "metadata": {}, "outputs": [], "source": [ "def run_simple_backtest(df_with_predictions, initial_capital=10000, trade_size_usd=1000, transaction_cost_pct=0.003): # Alpaca crypto fee\n", " \"\"\"\n", " A very simplified vectorized backtest.\n", " Assumes df_with_predictions has a 'signal' column (1 for buy, 0 for hold/nothing, -1 for sell if implementing).\n", " For this example, we'll assume our model's prediction (0 or 1) is the signal.\n", " 1 = Go Long, 0 = Exit Long (or do nothing if not in position)\n", " \"\"\"\n", " if df_with_predictions.empty or 'predicted_signal' not in df_with_predictions.columns:\n", " print(\"DataFrame for backtest is empty or 'predicted_signal' column missing.\")\n", " return\n", "\n", " capital = initial_capital\n", " position_btc = 0 # Amount of BTC held\n", " portfolio_value = []\n", "\n", " # Assume 'predicted_signal' comes from your model (1 for predicted up, 0 for predicted down/neutral)\n", " # Let's assume a simple strategy: if signal is 1, buy. If signal is 0 and we have a position, sell.\n", "\n", " for i in range(len(df_with_predictions)):\n", " current_price = df_with_predictions['close'].iloc[i]\n", " signal = df_with_predictions['predicted_signal'].iloc[i]\n", "\n", " # Decision logic\n", " if signal == 1 and position_btc == 0: # Buy signal and no current position\n", " # Buy\n", " amount_to_buy_btc = trade_size_usd / current_price\n", " cost = amount_to_buy_btc * current_price * (1 + transaction_cost_pct)\n", " if capital >= cost:\n", " capital -= cost\n", " position_btc += amount_to_buy_btc\n", " # print(f\"{df_with_predictions.index[i]}: BUY {amount_to_buy_btc:.6f} BTC @ {current_price:.2f}\")\n", "\n", " elif signal == 0 and position_btc > 0: # Sell signal (or neutral) and have a position\n", " # Sell\n", " proceeds = position_btc * current_price * (1 - transaction_cost_pct)\n", " capital += proceeds\n", " # print(f\"{df_with_predictions.index[i]}: SELL {position_btc:.6f} BTC @ {current_price:.2f}\")\n", " position_btc = 0\n", "\n", " current_portfolio_value = capital + (position_btc * current_price)\n", " portfolio_value.append(current_portfolio_value)\n", "\n", " df_with_predictions['portfolio_value'] = portfolio_value\n", " print(\"\\nBacktest Results:\")\n", " print(f\"Initial Capital: ${initial_capital:.2f}\")\n", " print(f\"Final Portfolio Value: ${df_with_predictions['portfolio_value'].iloc[-1]:.2f}\")\n", " returns = (df_with_predictions['portfolio_value'].iloc[-1] / initial_capital - 1) * 100\n", " print(f\"Total Return: {returns:.2f}%\")\n", "\n", " # Plotting (optional)\n", " # import matplotlib.pyplot as plt\n", " # plt.figure(figsize=(12,6))\n", " # plt.plot(df_with_predictions.index, df_with_predictions['portfolio_value'])\n", " # plt.title('Portfolio Value Over Time')\n", " # plt.xlabel('Date')\n", " # plt.ylabel('Portfolio Value ($)')\n", " # plt.show()\n", "\n", "if model and not X_test.empty:\n", " # Use the model to predict on the entire test set for backtesting\n", " # For a more realistic backtest, you'd re-train periodically or use a walk-forward approach.\n", " # Here, we're just using the single model trained on X_train.\n", " all_featured_data_for_backtest = featured_data.loc[X_test.index].copy() # Get original data rows for X_test\n", " all_featured_data_for_backtest['predicted_signal'] = model.predict(X_test) # Use the trained model\n", "\n", " run_simple_backtest(all_featured_data_for_backtest)\n", "else:\n", " print(\"Skipping backtest as model or test data is not available.\")" ] }, { "cell_type": "markdown", "id": "8f950f19-ea56-4d27-bb0c-1aa029f6f909", "metadata": {}, "source": [ "Backtesting Libraries: For more serious backtesting, consider backtrader or zipline-reloaded. They handle many complexities.\n", "\n", "Step 6: Signal Generation & Order Execution (Live/Paper Trading)\n", "\n", "This is where you'd run the bot periodically (e.g., every minute)." ] }, { "cell_type": "code", "execution_count": null, "id": "9454be39-70d8-49ed-a814-8695d69e32b2", "metadata": {}, "outputs": [], "source": [ "SYMBOL = \"BTC/USD\"\n", "TRADE_QTY_USD = 100 # Amount in USD to trade per signal. Adjust based on risk tolerance.\n", "TARGET_HORIZON_MINUTES = 5 # Same as used in training\n", "\n", "# Global model and feature_cols (assuming they are trained and available)\n", "# model = ... (your trained model)\n", "# feature_cols = ... (list of feature column names used for training)\n", "\n", "def get_latest_bar_features():\n", " \"\"\"Fetches latest bars, calculates features for the most recent one.\"\"\"\n", " # Fetch enough data to calculate all features (e.g., max lookback of your indicators)\n", " # If SMA_30 is longest, need at least 30 + target_horizon previous bars\n", " # Let's fetch more to be safe, e.g., 100 bars\n", " now = datetime.now()\n", " start_fetch_dt = (now - timedelta(minutes=150)).strftime('%Y-%m-%d %H:%M:%S') # fetch last 150 mins\n", "\n", " latest_bars_df = api.get_crypto_bars(\n", " SYMBOL,\n", " tradeapi. টাইমফ্রেম.মিনিট,\n", " start=start_fetch_dt, # Alpaca needs ISO format with T\n", " # end defaults to now\n", " limit=150 # fetch a bit more than needed for features\n", " ).df\n", " \n", " if latest_bars_df.empty or len(latest_bars_df) < 35: # Min needed for SMA_30 + some buffer\n", " print(\"Not enough recent bars to calculate features.\")\n", " return None\n", "\n", " featured_bars = create_features(latest_bars_df.copy())\n", " if featured_bars.empty:\n", " print(\"Failed to create features for latest bars.\")\n", " return None\n", " # Return only the features for the most recent complete bar\n", " return featured_bars[feature_cols].iloc[-1:] # Return as DataFrame\n", "\n", "def check_and_place_trade():\n", " global model, feature_cols # Ensure these are accessible\n", "\n", " if model is None or feature_cols is None:\n", " print(\"Model not trained or feature columns not defined. Skipping trade check.\")\n", " return\n", "\n", " print(f\"\\n{datetime.now()}: Checking for trading signal...\")\n", " current_features_df = get_latest_bar_features()\n", "\n", " if current_features_df is None or current_features_df.empty:\n", " print(\"Could not get features for the latest bar.\")\n", " return\n", "\n", " # Ensure columns are in the same order as during training\n", " current_features_df = current_features_df[feature_cols]\n", "\n", " prediction = model.predict(current_features_df)\n", " signal = prediction[0] # 0 for down/neutral, 1 for up\n", "\n", " print(f\"Raw features for prediction: {current_features_df.iloc[0].to_dict()}\")\n", " print(f\"Model prediction: {signal}\")\n", "\n", " try:\n", " positions = api.list_positions()\n", " btc_position = next((p for p in positions if p.symbol == SYMBOL), None)\n", " current_price_info = api.get_latest_crypto_quote(SYMBOL) # Use quote for more current price\n", " current_price = (current_price_info.ap + current_price_info.bp) / 2 # Mid price\n", "\n", " if not current_price:\n", " print(\"Could not get current price for BTC/USD.\")\n", " return\n", "\n", "\n", " if signal == 1: # Predicted UP - Potential BUY\n", " if btc_position is None or float(btc_position.qty) == 0:\n", " qty_to_buy = TRADE_QTY_USD / current_price\n", " print(f\"BUY signal. Attempting to buy {qty_to_buy:.6f} {SYMBOL} at ~${current_price:.2f}\")\n", " api.submit_order(\n", " symbol=SYMBOL,\n", " qty=round(qty_to_buy, 6), # Alpaca crypto needs precision\n", " side='buy',\n", " type='market',\n", " time_in_force='gtc' # Good 'til canceled\n", " )\n", " print(\"BUY order submitted.\")\n", " else:\n", " print(f\"BUY signal, but already have a position of {btc_position.qty} {SYMBOL}. Holding.\")\n", "\n", " elif signal == 0: # Predicted DOWN/NEUTRAL - Potential SELL\n", " if btc_position and float(btc_position.qty) > 0:\n", " qty_to_sell = float(btc_position.qty) # Sell entire position\n", " print(f\"SELL signal. Attempting to sell {qty_to_sell:.6f} {SYMBOL} at ~${current_price:.2f}\")\n", " api.submit_order(\n", " symbol=SYMBOL,\n", " qty=round(qty_to_sell, 6),\n", " side='sell',\n", " type='market',\n", " time_in_force='gtc'\n", " )\n", " print(\"SELL order submitted.\")\n", " else:\n", " print(\"SELL signal, but no open position to sell. Doing nothing.\")\n", " else:\n", " print(\"Neutral signal or unrecognized signal. Doing nothing.\")\n", "\n", " except Exception as e:\n", " print(f\"Error during trade execution: {e}\")\n", "\n", "# Main loop (very basic scheduler)\n", "# For a robust bot, use APScheduler or run it in a more managed environment (e.g., cloud server with cron)\n", "if __name__ == \"__main__\" and model is not None: # Ensure model is trained\n", " # This is a simplified loop. In a real bot, you'd schedule this.\n", " # For example, using APScheduler to run exactly at the start of each minute.\n", " print(\"Starting dummy trading loop (runs a few times for demo). Press Ctrl+C to stop.\")\n", " print(\"IMPORTANT: This is for PAPER TRADING ONLY.\")\n", " print(f\"Will use model: {type(model).__name__} and features: {feature_cols}\")\n", " try:\n", " # Initial run\n", " check_and_place_trade()\n", " for i in range(5): # Run for a few iterations for demo\n", " # Wait for the next minute (approximately)\n", " # A more precise scheduler (like APScheduler) is better for live trading\n", " time.sleep(60)\n", " check_and_place_trade()\n", " except KeyboardInterrupt:\n", " print(\"Trading loop stopped by user.\")\n", " except Exception as e:\n", " print(f\"An error occurred in the trading loop: {e}\")\n", "else:\n", " if model is None:\n", " print(\"Model is not trained. Cannot start trading loop.\")" ] }, { "cell_type": "markdown", "id": "8236c202-3ecf-4e5b-b1cf-f0e0ea222b80", "metadata": {}, "source": [ "Step 7: Risk Management (Conceptual)\n", "\n", " Position Sizing: Don't risk too much on a single trade (e.g., TRADE_QTY_USD should be a small % of your paper capital).\n", "\n", " Stop-Loss: Automatically sell if the price moves against you by a certain percentage or dollar amount after entering a trade. Alpaca supports stop-loss orders." ] }, { "cell_type": "code", "execution_count": null, "id": "aac9b668-35a3-4a53-ab57-c785cd7e4f42", "metadata": {}, "outputs": [], "source": [ "# Example of a market buy order with a trailing stop-loss\n", "# api.submit_order(\n", "# symbol=SYMBOL,\n", "# qty=qty_to_buy,\n", "# side='buy',\n", "# type='market',\n", "# time_in_force='day',\n", "# trail_percent='1.5' # Trail stop loss 1.5% below high water mark\n", "# )" ] }, { "cell_type": "markdown", "id": "327d8404-15c6-48fc-aeee-db45ece2df81", "metadata": {}, "source": [ " Take-Profit: Automatically sell if the price moves in your favor by a certain amount.\n", "\n", " Max Drawdown: If your total capital drops by X%, stop trading and re-evaluate.\n", "\n", "Step 8: Deployment & Monitoring\n", "\n", " Server: Run your bot on a reliable machine or cloud server (AWS EC2, Google Cloud, etc.) that's always on.\n", "\n", " Scheduling: Use cron (Linux) or APScheduler (Python library) to run your check_and_place_trade function every minute precisely.\n", "\n", " Logging: Log every decision, trade, error, and API response.\n", "\n", " Monitoring: Track P&L, number of trades, win rate, errors, API connectivity, etc. Set up alerts.\n", "\n", "Very Important Next Steps & Considerations:\n", "\n", " PAPER TRADE EXTENSIVELY. I cannot stress this enough.\n", "\n", " Improve Feature Engineering: This is key. Explore more advanced features, time-series properties, etc.\n", "\n", " Hyperparameter Tuning: Use GridSearchCV or RandomizedSearchCV (or Optuna/Hyperopt) to find optimal model parameters.\n", "\n", " Robust Backtesting: Implement a proper event-driven backtester that accounts for slippage and realistic transaction costs.\n", "\n", " Walk-Forward Optimization: Markets change. Periodically re-train your model on newer data and test on out-of-sample data that follows.\n", "\n", " Understand Alpaca's Crypto Trading Rules: Fees (currently 0.3% for taker/maker on basic accounts), minimum order sizes, precision for quantities.\n", "\n", " Error Handling & Resilience: Your bot needs to handle API errors, network issues, unexpected data, etc.\n", "\n", " The 1-Minute Problem:\n", "\n", " Noise: Extremely high noise-to-signal ratio.\n", "\n", " Latency: Your reaction time vs. market speed.\n", "\n", " Transaction Costs: Can quickly kill profitability on small moves. Your average win needs to be significantly larger than costs.\n", "\n", " Alternative ML Approaches:\n", "\n", " Reinforcement Learning: Can learn trading policies directly. Very complex.\n", "\n", " LSTMs/Transformers: For sequence modeling, but require significant data and expertise.\n", "\n", " Simplicity First: Start with a very simple model and strategy. Complexity doesn't always mean better results and makes debugging harder.\n", "\n", "This is a massive undertaking. Be patient, learn continuously, and manage your risk (even paper money risk, as it builds habits). Good luck!" ] }, { "cell_type": "markdown", "id": "0a7209fb-f7aa-4eb7-aacc-13a929b9e268", "metadata": {}, "source": [ "
" ] }, { "cell_type": "markdown", "id": "209900ba-c211-4dbb-8356-03b587efc15d", "metadata": {}, "source": [ "### 2nd Prompt" ] }, { "cell_type": "markdown", "id": "b1072761-203f-42b3-93be-b53a5c73483d", "metadata": {}, "source": [ "2. After step 2: Data Acquisition I got an error like this: ---------------------------------------------------------------------------\n", "AttributeError Traceback (most recent call last)\n", "Cell In[5], line 61\n", "59 # Example usage:\n", "60 symbol = \"BTC/USD\" # Alpaca uses \"BTC/USD\" for crypto pairs\n", "---> 61 timeframe = tradeapi. টাইমফ্রেম.মিনিট # or \"1Min\" for older SDK versions\n", "62 # Fetch last 30 days of data for example\n", "63 start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')\n", "\n", "AttributeError: module 'alpaca_trade_api' has no attribute 'টাইমফ্রেম'.\n", "Can you fix it?" ] }, { "cell_type": "markdown", "id": "d60e7a15-2ad6-4b70-b797-ed70113f6302", "metadata": {}, "source": [ "### 2nd Attempt" ] }, { "cell_type": "code", "execution_count": 1, "id": "e60edfaf-ba96-4553-baf2-dbd2b0ec90ec", "metadata": {}, "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings('ignore')" ] }, { "cell_type": "code", "execution_count": 2, "id": "2e661658-394d-48cf-b607-8ffe888551e3", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Attempting to connect to Alpaca Paper Trading...\n", "Successfully connected. Account status: ACTIVE\n", "Fetching data from 2025-05-02T00:00:00+00:00 to 2025-05-08T00:00:00+00:00\n", "Fetching data from 2025-05-08T00:01:00+00:00 to 2025-05-09T00:00:00+00:00\n", "\n", "Fetched 4349 rows of BTC/USD data.\n", "Sample data (first 5 rows):\n", " close high low trade_count \\\n", "timestamp \n", "2025-05-02 00:00:00+00:00 96599.4770 96599.4770 96599.4770 0 \n", "2025-05-02 00:02:00+00:00 96650.8100 96650.8100 96650.8100 0 \n", "2025-05-02 00:03:00+00:00 96588.0115 96588.0115 96588.0115 0 \n", "2025-05-02 00:05:00+00:00 96639.6100 96639.6100 96639.6100 0 \n", "2025-05-02 00:08:00+00:00 96521.6100 96584.7550 96521.6100 0 \n", "\n", " open volume vwap symbol \n", "timestamp \n", "2025-05-02 00:00:00+00:00 96599.4770 0.0 96599.4770 BTC/USD \n", "2025-05-02 00:02:00+00:00 96650.8100 0.0 96650.8100 BTC/USD \n", "2025-05-02 00:03:00+00:00 96588.0115 0.0 96588.0115 BTC/USD \n", "2025-05-02 00:05:00+00:00 96639.6100 0.0 96639.6100 BTC/USD \n", "2025-05-02 00:08:00+00:00 96584.7550 0.0 96553.1825 BTC/USD \n", "\n", "Sample data (last 5 rows):\n", " close high low trade_count \\\n", "timestamp \n", "2025-05-08 23:55:00+00:00 103121.6815 103121.6815 103121.6815 0 \n", "2025-05-08 23:56:00+00:00 103170.6190 103170.6190 103170.6190 0 \n", "2025-05-08 23:57:00+00:00 103204.4250 103204.4250 103204.4250 0 \n", "2025-05-08 23:58:00+00:00 103284.7150 103284.7150 103284.7150 0 \n", "2025-05-09 00:00:00+00:00 103206.1315 103267.4635 103196.4550 1 \n", "\n", " open volume vwap symbol \n", "timestamp \n", "2025-05-08 23:55:00+00:00 103121.6815 0.000000 103121.6815 BTC/USD \n", "2025-05-08 23:56:00+00:00 103170.6190 0.000000 103170.6190 BTC/USD \n", "2025-05-08 23:57:00+00:00 103204.4250 0.000000 103204.4250 BTC/USD \n", "2025-05-08 23:58:00+00:00 103284.7150 0.000000 103284.7150 BTC/USD \n", "2025-05-09 00:00:00+00:00 103230.7000 0.000012 103230.7000 BTC/USD \n" ] } ], "source": [ "import os\n", "import alpaca_trade_api as tradeapi\n", "import pandas as pd\n", "from datetime import datetime, timedelta\n", "import time\n", "\n", "# For Paper Trading\n", "os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets'\n", "# Replace with your actual keys or set them as environment variables\n", "# Make sure these are set correctly if you haven't already\n", "os.environ['APCA_API_KEY_ID'] = 'your API KEY'\n", "os.environ['APCA_API_SECRET_KEY'] = 'your SECRET KEY'\n", "# os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID'\n", "# os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'\n", "\n", "API_KEY = os.getenv('APCA_API_KEY_ID')\n", "API_SECRET = os.getenv('APCA_API_SECRET_KEY')\n", "BASE_URL = os.getenv('APCA_API_BASE_URL')\n", "\n", "# Ensure API_KEY and API_SECRET are not None\n", "if not API_KEY or not API_SECRET:\n", " raise ValueError(\"API_KEY or API_SECRET not set. Please set them as environment variables or directly in the script.\")\n", "\n", "api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')\n", "\n", "print(\"Attempting to connect to Alpaca Paper Trading...\")\n", "try:\n", " account = api.get_account()\n", " print(f\"Successfully connected. Account status: {account.status}\")\n", "except Exception as e:\n", " print(f\"Failed to connect or get account info: {e}\")\n", " # Exit or handle if connection fails\n", " exit()\n", "\n", "\n", "def fetch_data(symbol, timeframe_enum, start_date_str, end_date_str=None): # Changed timeframe to timeframe_enum\n", " \"\"\"Fetches historical crypto data from Alpaca.\"\"\"\n", " if end_date_str is None:\n", " end_date_str = datetime.now().strftime('%Y-%m-%d')\n", "\n", " all_bars = []\n", " start_dt = pd.to_datetime(start_date_str, utc=True)\n", " end_dt = pd.to_datetime(end_date_str, utc=True)\n", "\n", " current_start = start_dt\n", " while current_start < end_dt:\n", " # Calculate chunk_end, ensuring it doesn't exceed end_dt\n", " # For 1-minute data, 10000 bars is approx 6.94 days. Let's use 6 days to be safe.\n", " chunk_end_candidate = current_start + timedelta(days=6)\n", " chunk_end = min(chunk_end_candidate, end_dt)\n", "\n", " print(f\"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}\")\n", "\n", " try:\n", " bars = api.get_crypto_bars(\n", " symbol,\n", " timeframe_enum, # Use the passed enum\n", " start=current_start.isoformat(),\n", " end=chunk_end.isoformat(), # Ensure end is also passed\n", " limit=10000\n", " ).df\n", " except Exception as e:\n", " print(f\"Error fetching data chunk: {e}\")\n", " # Decide how to handle: break, retry, or skip chunk\n", " current_start = chunk_end # Move to next potential period\n", " time.sleep(5) # Wait longer if an error occurred\n", " continue\n", "\n", "\n", " if bars.empty:\n", " print(f\"No data found for chunk starting {current_start.isoformat()}\")\n", " if current_start >= end_dt and not all_bars: # if first chunk and no data\n", " break\n", " current_start = chunk_end # Move to next chunk period\n", " time.sleep(1) # Be nice to the API\n", " continue\n", "\n", " all_bars.append(bars)\n", " if not bars.index.empty:\n", " # Move current_start to the timestamp of the last bar fetched + 1 unit of timeframe\n", " # For TimeFrame.Minute, this is +1 minute\n", " if timeframe_enum == tradeapi.TimeFrame.Minute:\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(minutes=1)\n", " elif timeframe_enum == tradeapi.TimeFrame.Hour:\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(hours=1)\n", " # Add other timeframes if needed\n", " else: # Default for Day or others\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(days=1)\n", "\n", " # Ensure current_start does not go beyond end_dt excessively in the loop condition\n", " if current_start > end_dt and chunk_end_candidate >= end_dt :\n", " break\n", " else: # Should not happen if bars is not empty, but as a safeguard\n", " current_start = chunk_end\n", "\n", " time.sleep(1) # Respect API rate limits\n", "\n", " if not all_bars:\n", " print(\"No data fetched. Check your date range, symbol, or API connection.\")\n", " return pd.DataFrame()\n", "\n", " df = pd.concat(all_bars)\n", " df = df[~df.index.duplicated(keep='first')] # Remove potential duplicates\n", " df = df.sort_index() # Ensure chronological order\n", "\n", " # Filter to ensure we are within the originally requested start_dt and end_dt precisely\n", " df = df[(df.index >= start_dt) & (df.index <= end_dt)]\n", " return df\n", "\n", "# Example usage:\n", "symbol = \"BTC/USD\" # Alpaca uses \"BTC/USD\" for crypto pairs\n", "# CORRECTED TIMEFRAME:\n", "timeframe_to_use = tradeapi.TimeFrame.Minute # Corrected\n", "# Alternative: timeframe_to_use = \"1Min\" (as a string, for some SDK versions or if TimeFrame enum is problematic)\n", "\n", "# Fetch last 7 days of data for example (a smaller range for quicker testing)\n", "start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')\n", "# end_date = datetime.now().strftime('%Y-%m-%d') # Fetches up to now by default if None\n", "\n", "btc_data = fetch_data(symbol, timeframe_to_use, start_date)\n", "\n", "if not btc_data.empty:\n", " print(f\"\\nFetched {len(btc_data)} rows of {symbol} data.\")\n", " print(\"Sample data (first 5 rows):\")\n", " print(btc_data.head())\n", " print(\"\\nSample data (last 5 rows):\")\n", " print(btc_data.tail())\n", "else:\n", " print(\"Failed to fetch data or no data available for the period.\")" ] }, { "cell_type": "markdown", "id": "2dd66ca6-84d0-4ecb-b0be-910aba094c32", "metadata": {}, "source": [ "Key changes:\n", "\n", " timeframe = tradeapi.TimeFrame.Minute: This is the crucial fix.\n", "\n", " I renamed the timeframe parameter in fetch_data to timeframe_enum to avoid confusion if you were to pass the string version.\n", "\n", " Added a check for API_KEY and API_SECRET to ensure they are loaded.\n", "\n", " Improved the connection check with a try-except block.\n", "\n", " Refined the chunking logic in fetch_data slightly, particularly how current_start is advanced and how chunk_end is determined to better respect end_dt.\n", "\n", " Made the advancement of current_start adaptive to the timeframe_enum.\n", "\n", " Final filtering of the concatenated DataFrame to strictly adhere to start_dt and end_dt.\n", "\n", " Reduced the default fetch range in the example to 7 days for faster initial testing.\n", "\n", "Important:\n", "You will also need to make the same correction in Step 6 (Signal Generation & Order Execution) inside the get_latest_bar_features function:" ] }, { "cell_type": "markdown", "id": "4e2810ef-1af9-4406-b2f8-7e025fc20d83", "metadata": {}, "source": [ "Step 3: Feature Engineering\n", "\n", "This is where you create signals for your model. pandas-ta is a great library for this." ] }, { "cell_type": "code", "execution_count": 3, "id": "00bea5b7-3fa8-475d-86c8-4f67199e06c4", "metadata": {}, "outputs": [ { "ename": "ImportError", "evalue": "cannot import name 'NaN' from 'numpy' (C:\\Users\\micro\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\numpy\\__init__.py)", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mImportError\u001b[0m Traceback (most recent call last)", "Cell \u001b[1;32mIn[3], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpandas_ta\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mta\u001b[39;00m\n\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcreate_features\u001b[39m(df):\n\u001b[0;32m 4\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m df\u001b[38;5;241m.\u001b[39mempty \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mclose\u001b[39m\u001b[38;5;124m'\u001b[39m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m df\u001b[38;5;241m.\u001b[39mcolumns:\n", "File \u001b[1;32m~\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\pandas_ta\\__init__.py:116\u001b[0m\n\u001b[0;32m 97\u001b[0m EXCHANGE_TZ \u001b[38;5;241m=\u001b[39m {\n\u001b[0;32m 98\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNZSX\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m12\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mASX\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m11\u001b[39m,\n\u001b[0;32m 99\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTSE\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m9\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mHKE\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m8\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mSSE\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m8\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mSGX\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m8\u001b[39m,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 102\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBMF\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m2\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNYSE\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m4\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTSX\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m4\u001b[39m\n\u001b[0;32m 103\u001b[0m }\n\u001b[0;32m 105\u001b[0m RATE \u001b[38;5;241m=\u001b[39m {\n\u001b[0;32m 106\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mDAYS_PER_MONTH\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m21\u001b[39m,\n\u001b[0;32m 107\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mMINUTES_PER_HOUR\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m60\u001b[39m,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 113\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mYEARLY\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m1\u001b[39m,\n\u001b[0;32m 114\u001b[0m }\n\u001b[1;32m--> 116\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas_ta\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n", "File \u001b[1;32m~\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\pandas_ta\\core.py:18\u001b[0m\n\u001b[0;32m 16\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas_ta\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcandles\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n\u001b[0;32m 17\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas_ta\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcycles\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n\u001b[1;32m---> 18\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas_ta\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmomentum\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n\u001b[0;32m 19\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas_ta\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01moverlap\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n\u001b[0;32m 20\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas_ta\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mperformance\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n", "File \u001b[1;32m~\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\pandas_ta\\momentum\\__init__.py:34\u001b[0m\n\u001b[0;32m 32\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01msmi\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m smi\n\u001b[0;32m 33\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01msqueeze\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m squeeze\n\u001b[1;32m---> 34\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01msqueeze_pro\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m squeeze_pro\n\u001b[0;32m 35\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mstc\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m stc\n\u001b[0;32m 36\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mstoch\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m stoch\n", "File \u001b[1;32m~\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\pandas_ta\\momentum\\squeeze_pro.py:2\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;66;03m# -*- coding: utf-8 -*-\u001b[39;00m\n\u001b[1;32m----> 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m NaN \u001b[38;5;28;01mas\u001b[39;00m npNaN\n\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m DataFrame\n\u001b[0;32m 4\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas_ta\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmomentum\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m mom\n", "\u001b[1;31mImportError\u001b[0m: cannot import name 'NaN' from 'numpy' (C:\\Users\\micro\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\numpy\\__init__.py)" ] } ], "source": [ "import pandas_ta as ta\n", "\n", "def create_features(df):\n", " if df.empty or 'close' not in df.columns:\n", " print(\"DataFrame is empty or 'close' column is missing.\")\n", " return df\n", "\n", " df.ta.sma(length=10, append=True, col_names=('SMA_10'))\n", " df.ta.sma(length=30, append=True, col_names=('SMA_30'))\n", " df.ta.ema(length=10, append=True, col_names=('EMA_10'))\n", " df.ta.rsi(length=14, append=True, col_names=('RSI_14'))\n", " df.ta.macd(append=True, col_names=('MACD_12_26_9', 'MACDh_12_26_9', 'MACDs_12_26_9'))\n", " df.ta.bbands(length=20, append=True, col_names=('BBL_20_2.0', 'BBM_20_2.0', 'BBU_20_2.0', 'BBB_20_2.0', 'BBP_20_2.0'))\n", " df.ta.atr(length=14, append=True, col_names=('ATR_14'))\n", "\n", " # Lagged returns\n", " for lag in [1, 3, 5, 10]:\n", " df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)\n", "\n", " # Add more features: volatility, momentum, volume-based if available, etc.\n", " # e.g., log returns, price relative to moving average, etc.\n", "\n", " df.dropna(inplace=True) # Remove rows with NaNs created by indicators\n", " return df\n", "\n", "if not btc_data.empty:\n", " featured_data = create_features(btc_data.copy()) # Use .copy() to avoid modifying original\n", " print(\"\\nData with features:\")\n", " print(featured_data.head())\n", "else:\n", " print(\"Cannot create features, btc_data is empty.\")\n", " featured_data = pd.DataFrame() # ensure it's a DataFrame" ] }, { "cell_type": "markdown", "id": "8b03dee5-3fb7-4546-a8e1-1a9cd86d4edd", "metadata": {}, "source": [ "### 3rd Prompt" ] }, { "cell_type": "markdown", "id": "7b6ad530-9624-4302-bd2c-8bf070e15f4f", "metadata": {}, "source": [ "So, we are working on a trading bot. It should be using machine learning to trade BTCUSD in one minute intervals and Alpaca Markets.\n", "So far, we got as much as you can find at https://superai.pl/courses/building_an_ai_trading_bot_with_python_gemini_alpaca_p2.html\n", "\n", "Step 2 was OK, but in Step 3 I got this an error (check below).\n", "Can you fix it by using a different library, because it seams like the latest version of pandas-ta is from 2021 and it might not be compatible with a lot of other libraries.\n", "\n", "--------------------------------------------------------------------------- \n", "GOOD CODE:\n", "import os\n", "import alpaca_trade_api as tradeapi\n", "import pandas as pd\n", "from datetime import datetime, timedelta\n", "import time\n", "\n", "#For Paper Trading\n", "os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets'\n", "#Replace with your actual keys or set them as environment variables\n", "#Make sure these are set correctly if you haven't already\n", "os.environ['APCA_API_KEY_ID'] = 'your API KEY'\n", "os.environ['APCA_API_SECRET_KEY'] = 'your SECRET KEY'\n", "#os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID'\n", "#os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'\n", "\n", "API_KEY = os.getenv('APCA_API_KEY_ID')\n", "API_SECRET = os.getenv('APCA_API_SECRET_KEY')\n", "BASE_URL = os.getenv('APCA_API_BASE_URL')\n", "\n", "#Ensure API_KEY and API_SECRET are not None\n", "if not API_KEY or not API_SECRET:\n", " raise ValueError(\"API_KEY or API_SECRET not set. Please set them as environment variables or directly in the script.\")\n", "\n", "api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')\n", "\n", "print(\"Attempting to connect to Alpaca Paper Trading...\")\n", "try:\n", " account = api.get_account()\n", " print(f\"Successfully connected. Account status: {account.status}\")\n", "except Exception as e:\n", " print(f\"Failed to connect or get account info: {e}\")\n", " # Exit or handle if connection fails\n", " exit()\n", "\n", "\n", "def fetch_data(symbol, timeframe_enum, start_date_str, end_date_str=None): # Changed timeframe to timeframe_enum\n", " \"\"\"Fetches historical crypto data from Alpaca.\"\"\"\n", " if end_date_str is None:\n", " end_date_str = datetime.now().strftime('%Y-%m-%d')\n", "\n", " all_bars = []\n", " start_dt = pd.to_datetime(start_date_str, utc=True)\n", " end_dt = pd.to_datetime(end_date_str, utc=True)\n", "\n", " current_start = start_dt\n", " while current_start < end_dt:\n", " # Calculate chunk_end, ensuring it doesn't exceed end_dt\n", " # For 1-minute data, 10000 bars is approx 6.94 days. Let's use 6 days to be safe.\n", " chunk_end_candidate = current_start + timedelta(days=6)\n", " chunk_end = min(chunk_end_candidate, end_dt)\n", "\n", " print(f\"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}\")\n", "\n", " try:\n", " bars = api.get_crypto_bars(\n", " symbol,\n", " timeframe_enum, # Use the passed enum\n", " start=current_start.isoformat(),\n", " end=chunk_end.isoformat(), # Ensure end is also passed\n", " limit=10000\n", " ).df\n", " except Exception as e:\n", " print(f\"Error fetching data chunk: {e}\")\n", " # Decide how to handle: break, retry, or skip chunk\n", " current_start = chunk_end # Move to next potential period\n", " time.sleep(5) # Wait longer if an error occurred\n", " continue\n", "\n", "\n", " if bars.empty:\n", " print(f\"No data found for chunk starting {current_start.isoformat()}\")\n", " if current_start >= end_dt and not all_bars: # if first chunk and no data\n", " break\n", " current_start = chunk_end # Move to next chunk period\n", " time.sleep(1) # Be nice to the API\n", " continue\n", "\n", " all_bars.append(bars)\n", " if not bars.index.empty:\n", " # Move current_start to the timestamp of the last bar fetched + 1 unit of timeframe\n", " # For TimeFrame.Minute, this is +1 minute\n", " if timeframe_enum == tradeapi.TimeFrame.Minute:\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(minutes=1)\n", " elif timeframe_enum == tradeapi.TimeFrame.Hour:\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(hours=1)\n", " # Add other timeframes if needed\n", " else: # Default for Day or others\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(days=1)\n", "\n", " # Ensure current_start does not go beyond end_dt excessively in the loop condition\n", " if current_start > end_dt and chunk_end_candidate >= end_dt :\n", " break\n", " else: # Should not happen if bars is not empty, but as a safeguard\n", " current_start = chunk_end\n", "\n", " time.sleep(1) # Respect API rate limits\n", "\n", " if not all_bars:\n", " print(\"No data fetched. Check your date range, symbol, or API connection.\")\n", " return pd.DataFrame()\n", "\n", " df = pd.concat(all_bars)\n", " df = df[~df.index.duplicated(keep='first')] # Remove potential duplicates\n", " df = df.sort_index() # Ensure chronological order\n", "\n", " # Filter to ensure we are within the originally requested start_dt and end_dt precisely\n", " df = df[(df.index >= start_dt) & (df.index <= end_dt)]\n", " return df\n", "\n", "#Example usage:\n", "symbol = \"BTC/USD\" #Alpaca uses \"BTC/USD\" for crypto pairs\n", "#CORRECTED TIMEFRAME:\n", "timeframe_to_use = tradeapi.TimeFrame.Minute # Corrected\n", "#Alternative: timeframe_to_use = \"1Min\" (as a string, for some SDK versions or if TimeFrame enum is problematic)\n", "\n", "#Fetch last 7 days of data for example (a smaller range for quicker testing)\n", "start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')\n", "#end_date = datetime.now().strftime('%Y-%m-%d') #Fetches up to now by default if None\n", "\n", "btc_data = fetch_data(symbol, timeframe_to_use, start_date)\n", "\n", "if not btc_data.empty:\n", " print(f\"\\nFetched {len(btc_data)} rows of {symbol} data.\")\n", " print(\"Sample data (first 5 rows):\")\n", " print(btc_data.head())\n", " print(\"\\nSample data (last 5 rows):\")\n", " print(btc_data.tail())\n", "else:\n", " print(\"Failed to fetch data or no data available for the period.\")\n", " \n", "--------------------------------------------------------------------------- \n", "BAD CODE: \n", "import pandas_ta as ta\n", "\n", "def create_features(df):\n", " if df.empty or 'close' not in df.columns:\n", " print(\"DataFrame is empty or 'close' column is missing.\")\n", " return df\n", "\n", " df.ta.sma(length=10, append=True, col_names=('SMA_10'))\n", " df.ta.sma(length=30, append=True, col_names=('SMA_30'))\n", " df.ta.ema(length=10, append=True, col_names=('EMA_10'))\n", " df.ta.rsi(length=14, append=True, col_names=('RSI_14'))\n", " df.ta.macd(append=True, col_names=('MACD_12_26_9', 'MACDh_12_26_9', 'MACDs_12_26_9'))\n", " df.ta.bbands(length=20, append=True, col_names=('BBL_20_2.0', 'BBM_20_2.0', 'BBU_20_2.0', 'BBB_20_2.0', 'BBP_20_2.0'))\n", " df.ta.atr(length=14, append=True, col_names=('ATR_14'))\n", "\n", " # Lagged returns\n", " for lag in [1, 3, 5, 10]:\n", " df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)\n", "\n", " # Add more features: volatility, momentum, volume-based if available, etc.\n", " # e.g., log returns, price relative to moving average, etc.\n", "\n", " df.dropna(inplace=True) # Remove rows with NaNs created by indicators\n", " return df\n", "\n", "if not btc_data.empty:\n", " featured_data = create_features(btc_data.copy()) # Use .copy() to avoid modifying original\n", " print(\"\\nData with features:\")\n", " print(featured_data.head())\n", "else:\n", " print(\"Cannot create features, btc_data is empty.\")\n", " featured_data = pd.DataFrame() # ensure it's a DataFrame\n", "\n", "--------------------------------------------------------------------------- \n", "ERROR:\n", "ImportError Traceback (most recent call last) Cell In[3], line 1 ----> 1 import pandas_ta as ta 3 def create_features(df): 4 if df.empty or 'close' not in df.columns:\n", "\n", "File ~\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\pandas_ta_init_.py:116 97 EXCHANGE_TZ = { 98 \"NZSX\": 12, \"ASX\": 11, 99 \"TSE\": 9, \"HKE\": 8, \"SSE\": 8, \"SGX\": 8, (...) 102 \"BMF\": -2, \"NYSE\": -4, \"TSX\": -4 103 } 105 RATE = { 106 \"DAYS_PER_MONTH\": 21, 107 \"MINUTES_PER_HOUR\": 60, (...) 113 \"YEARLY\": 1, 114 } --> 116 from pandas_ta.core import *\n", "\n", "File ~\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\pandas_ta\\core.py:18 16 from pandas_ta.candles import * 17 from pandas_ta.cycles import * ---> 18 from pandas_ta.momentum import * 19 from pandas_ta.overlap import * 20 from pandas_ta.performance import *\n", "\n", "File ~\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\pandas_ta\\momentum_init_.py:34 32 from .smi import smi 33 from .squeeze import squeeze ---> 34 from .squeeze_pro import squeeze_pro 35 from .stc import stc 36 from .stoch import stoch\n", "\n", "File ~\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\pandas_ta\\momentum\\squeeze_pro.py:2 1 # -- coding: utf-8 -- ----> 2 from numpy import NaN as npNaN 3 from pandas import DataFrame 4 from pandas_ta.momentum import mom\n", "\n", "ImportError: cannot import name 'NaN' from 'numpy' (C:\\Users\\micro\\anaconda3\\envs\\alpaca_AI_env\\Lib\\site-packages\\numpy_init_.py)\n", "\n" ] }, { "cell_type": "markdown", "id": "3a5e2866-9ee7-473c-9f18-d7bc1b150a2f", "metadata": {}, "source": [ "### 3rd Attempt" ] }, { "cell_type": "code", "execution_count": 1, "id": "354e64e9-1a00-4c73-9992-641f30bd1b4e", "metadata": {}, "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings('ignore')" ] }, { "cell_type": "code", "execution_count": 2, "id": "2afaa1aa-1d09-47d4-ae8c-1a3b3ed4955f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Attempting to connect to Alpaca Paper Trading...\n", "Successfully connected. Account status: ACTIVE\n", "Fetching data from 2025-05-29T00:00:00+00:00 to 2025-06-04T00:00:00+00:00\n", "Fetching data from 2025-06-04T00:01:00+00:00 to 2025-06-05T00:00:00+00:00\n", "\n", "Fetched 5587 rows of BTC/USD data.\n", "Sample data (first 5 rows):\n", " close high low trade_count \\\n", "timestamp \n", "2025-05-29 00:00:00+00:00 107854.950 107854.9500 107797.350 0 \n", "2025-05-29 00:01:00+00:00 107730.050 107786.4545 107730.050 0 \n", "2025-05-29 00:02:00+00:00 107784.335 107784.3350 107784.335 0 \n", "2025-05-29 00:03:00+00:00 107749.815 107749.8150 107749.815 0 \n", "2025-05-29 00:04:00+00:00 107669.645 107680.6050 107524.300 0 \n", "\n", " open volume vwap symbol \n", "timestamp \n", "2025-05-29 00:00:00+00:00 107853.9450 0.0 107826.15000 BTC/USD \n", "2025-05-29 00:01:00+00:00 107786.4545 0.0 107758.25225 BTC/USD \n", "2025-05-29 00:02:00+00:00 107784.3350 0.0 107784.33500 BTC/USD \n", "2025-05-29 00:03:00+00:00 107749.8150 0.0 107749.81500 BTC/USD \n", "2025-05-29 00:04:00+00:00 107680.6050 0.0 107602.45250 BTC/USD \n", "\n", "Sample data (last 5 rows):\n", " close high low trade_count \\\n", "timestamp \n", "2025-06-04 23:54:00+00:00 104807.747 104845.250 104807.747 0 \n", "2025-06-04 23:55:00+00:00 104816.255 104816.255 104782.185 0 \n", "2025-06-04 23:57:00+00:00 104744.600 104744.600 104744.600 0 \n", "2025-06-04 23:58:00+00:00 104732.050 104739.899 104732.050 0 \n", "2025-06-04 23:59:00+00:00 104748.560 104758.500 104657.600 1 \n", "\n", " open volume vwap symbol \n", "timestamp \n", "2025-06-04 23:54:00+00:00 104845.250 0.000000 104826.4985 BTC/USD \n", "2025-06-04 23:55:00+00:00 104782.185 0.000000 104799.2200 BTC/USD \n", "2025-06-04 23:57:00+00:00 104744.600 0.000000 104744.6000 BTC/USD \n", "2025-06-04 23:58:00+00:00 104739.899 0.000000 104735.9745 BTC/USD \n", "2025-06-04 23:59:00+00:00 104657.600 0.000072 104657.6000 BTC/USD \n" ] } ], "source": [ "import os\n", "import alpaca_trade_api as tradeapi\n", "import pandas as pd\n", "from datetime import datetime, timedelta\n", "import time\n", "\n", "# For Paper Trading\n", "os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets'\n", "# Replace with your actual keys or set them as environment variables\n", "# Make sure these are set correctly if you haven't already\n", "os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID'\n", "os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'\n", "# os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID'\n", "# os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'\n", "\n", "API_KEY = os.getenv('APCA_API_KEY_ID')\n", "API_SECRET = os.getenv('APCA_API_SECRET_KEY')\n", "BASE_URL = os.getenv('APCA_API_BASE_URL')\n", "\n", "# Ensure API_KEY and API_SECRET are not None\n", "if not API_KEY or not API_SECRET:\n", " raise ValueError(\"API_KEY or API_SECRET not set. Please set them as environment variables or directly in the script.\")\n", "\n", "api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')\n", "\n", "print(\"Attempting to connect to Alpaca Paper Trading...\")\n", "try:\n", " account = api.get_account()\n", " print(f\"Successfully connected. Account status: {account.status}\")\n", "except Exception as e:\n", " print(f\"Failed to connect or get account info: {e}\")\n", " # Exit or handle if connection fails\n", " exit()\n", "\n", "\n", "def fetch_data(symbol, timeframe_enum, start_date_str, end_date_str=None): # Changed timeframe to timeframe_enum\n", " \"\"\"Fetches historical crypto data from Alpaca.\"\"\"\n", " if end_date_str is None:\n", " end_date_str = datetime.now().strftime('%Y-%m-%d')\n", "\n", " all_bars = []\n", " start_dt = pd.to_datetime(start_date_str, utc=True)\n", " end_dt = pd.to_datetime(end_date_str, utc=True)\n", "\n", " current_start = start_dt\n", " while current_start < end_dt:\n", " # Calculate chunk_end, ensuring it doesn't exceed end_dt\n", " # For 1-minute data, 10000 bars is approx 6.94 days. Let's use 6 days to be safe.\n", " chunk_end_candidate = current_start + timedelta(days=6)\n", " chunk_end = min(chunk_end_candidate, end_dt)\n", "\n", " print(f\"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}\")\n", "\n", " try:\n", " bars = api.get_crypto_bars(\n", " symbol,\n", " timeframe_enum, # Use the passed enum\n", " start=current_start.isoformat(),\n", " end=chunk_end.isoformat(), # Ensure end is also passed\n", " limit=10000\n", " ).df\n", " except Exception as e:\n", " print(f\"Error fetching data chunk: {e}\")\n", " # Decide how to handle: break, retry, or skip chunk\n", " current_start = chunk_end # Move to next potential period\n", " time.sleep(5) # Wait longer if an error occurred\n", " continue\n", "\n", "\n", " if bars.empty:\n", " print(f\"No data found for chunk starting {current_start.isoformat()}\")\n", " if current_start >= end_dt and not all_bars: # if first chunk and no data\n", " break\n", " current_start = chunk_end # Move to next chunk period\n", " time.sleep(1) # Be nice to the API\n", " continue\n", "\n", " all_bars.append(bars)\n", " if not bars.index.empty:\n", " # Move current_start to the timestamp of the last bar fetched + 1 unit of timeframe\n", " # For TimeFrame.Minute, this is +1 minute\n", " if timeframe_enum == tradeapi.TimeFrame.Minute:\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(minutes=1)\n", " elif timeframe_enum == tradeapi.TimeFrame.Hour:\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(hours=1)\n", " # Add other timeframes if needed\n", " else: # Default for Day or others\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(days=1)\n", "\n", " # Ensure current_start does not go beyond end_dt excessively in the loop condition\n", " if current_start > end_dt and chunk_end_candidate >= end_dt :\n", " break\n", " else: # Should not happen if bars is not empty, but as a safeguard\n", " current_start = chunk_end\n", "\n", " time.sleep(1) # Respect API rate limits\n", "\n", " if not all_bars:\n", " print(\"No data fetched. Check your date range, symbol, or API connection.\")\n", " return pd.DataFrame()\n", "\n", " df = pd.concat(all_bars)\n", " df = df[~df.index.duplicated(keep='first')] # Remove potential duplicates\n", " df = df.sort_index() # Ensure chronological order\n", "\n", " # Filter to ensure we are within the originally requested start_dt and end_dt precisely\n", " df = df[(df.index >= start_dt) & (df.index <= end_dt)]\n", " return df\n", "\n", "# Example usage:\n", "symbol = \"BTC/USD\" # Alpaca uses \"BTC/USD\" for crypto pairs\n", "# CORRECTED TIMEFRAME:\n", "timeframe_to_use = tradeapi.TimeFrame.Minute # Corrected\n", "# Alternative: timeframe_to_use = \"1Min\" (as a string, for some SDK versions or if TimeFrame enum is problematic)\n", "\n", "# Fetch last 7 days of data for example (a smaller range for quicker testing)\n", "start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')\n", "# end_date = datetime.now().strftime('%Y-%m-%d') # Fetches up to now by default if None\n", "\n", "btc_data = fetch_data(symbol, timeframe_to_use, start_date)\n", "\n", "if not btc_data.empty:\n", " print(f\"\\nFetched {len(btc_data)} rows of {symbol} data.\")\n", " print(\"Sample data (first 5 rows):\")\n", " print(btc_data.head())\n", " print(\"\\nSample data (last 5 rows):\")\n", " print(btc_data.tail())\n", "else:\n", " print(\"Failed to fetch data or no data available for the period.\")" ] }, { "cell_type": "markdown", "id": "61ffc899-c0f9-4b22-9b02-4fcfba4837d5", "metadata": {}, "source": [ "Step 3: Feature Engineering\n", "\n", "This is where you create signals for your model. pandas-ta is a great library for this." ] }, { "cell_type": "markdown", "id": "e34bfdca-79b3-4600-a796-c5a6282af941", "metadata": {}, "source": [ "Okay, that ImportError: cannot import name 'NaN' from 'numpy' is a classic sign of an older library (like pandas-ta from 2021) not being compatible with newer versions of NumPy (where np.NaN was deprecated and then removed in favor of np.nan).\n", "\n", "We can definitely replace pandas-ta with other libraries. A very popular and robust library for technical analysis is TA-Lib, but it requires installing the underlying C library which can sometimes be a bit tricky depending on your OS.\n", "\n", "A simpler, pure-Python alternative that covers many common indicators is finta (FinancialIndicator). It's generally easier to install (pip install finta). Let's try to implement your create_features function using finta.\n", "\n", "Important Notes for finta:\n", "\n", " It typically expects column names like 'open', 'high', 'low', 'close' (all lowercase). The Alpaca API client's .df accessor usually provides these in lowercase, so we should be good.\n", "\n", " Some functions in finta return a single Series, while others (like MACD or BBANDS) return a DataFrame with multiple columns. We'll need to handle that.\n", "\n", " finta's Bollinger Bands function (TA.BBANDS) gives Upper, Middle, and Lower bands. We'll need to calculate Bandwidth (BBB_) and Percent B (BBP_) manually if finta doesn't provide them directly in the same way pandas-ta did.\n", "\n", "Here's how you can modify your create_features function using finta:\n", "\n", "First, make sure to install finta:\n", "\n", " \n", "pip install finta\n", "\n", " Then, replace your BAD CODE section with the following:" ] }, { "cell_type": "code", "execution_count": 3, "id": "be6910ed-ca32-419a-ba0a-e86b33550685", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Collecting finta\n", " Downloading finta-1.3-py3-none-any.whl.metadata (6.4 kB)\n", "Requirement already satisfied: numpy in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from finta) (2.2.5)\n", "Requirement already satisfied: pandas in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from finta) (2.2.3)\n", "Requirement already satisfied: python-dateutil>=2.8.2 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from pandas->finta) (2.9.0.post0)\n", "Requirement already satisfied: pytz>=2020.1 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from pandas->finta) (2024.1)\n", "Requirement already satisfied: tzdata>=2022.7 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from pandas->finta) (2024.2)\n", "Requirement already satisfied: six>=1.5 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from python-dateutil>=2.8.2->pandas->finta) (1.16.0)\n", "Downloading finta-1.3-py3-none-any.whl (29 kB)\n", "Installing collected packages: finta\n", "Successfully installed finta-1.3\n", "Note: you may need to restart the kernel to use updated packages.\n" ] } ], "source": [ "pip install finta" ] }, { "cell_type": "code", "execution_count": 4, "id": "e3282011-4179-49a5-a10a-3854a718597a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Data with features (using finta):\n", " close high low trade_count \\\n", "timestamp \n", "2025-05-29 00:40:00+00:00 107891.3515 107891.3515 107891.3515 0 \n", "2025-05-29 00:42:00+00:00 107819.7350 107819.7350 107819.7350 0 \n", "2025-05-29 00:45:00+00:00 107887.0150 107887.0150 107887.0150 0 \n", "2025-05-29 00:47:00+00:00 107962.3700 107962.3700 107962.3700 0 \n", "2025-05-29 00:48:00+00:00 108009.4200 108009.4200 108009.4200 0 \n", "\n", " open volume vwap symbol \\\n", "timestamp \n", "2025-05-29 00:40:00+00:00 107891.3515 0.0 107891.3515 BTC/USD \n", "2025-05-29 00:42:00+00:00 107819.7350 0.0 107819.7350 BTC/USD \n", "2025-05-29 00:45:00+00:00 107887.0150 0.0 107887.0150 BTC/USD \n", "2025-05-29 00:47:00+00:00 107962.3700 0.0 107962.3700 BTC/USD \n", "2025-05-29 00:48:00+00:00 108009.4200 0.0 108009.4200 BTC/USD \n", "\n", " SMA_10 SMA_30 ... BBL_20_2.0 \\\n", "timestamp ... \n", "2025-05-29 00:40:00+00:00 107940.20850 107797.621567 ... 107628.818315 \n", "2025-05-29 00:42:00+00:00 107922.53925 107796.447733 ... 107677.261002 \n", "2025-05-29 00:45:00+00:00 107904.22360 107801.679900 ... 107707.198595 \n", "2025-05-29 00:47:00+00:00 107901.82610 107807.614400 ... 107744.779959 \n", "2025-05-29 00:48:00+00:00 107911.29420 107816.267900 ... 107774.213800 \n", "\n", " BBM_20_2.0 BBU_20_2.0 BBB_20_2.0 \\\n", "timestamp \n", "2025-05-29 00:40:00+00:00 107862.704675 108096.591035 0.004337 \n", "2025-05-29 00:42:00+00:00 107874.226550 108071.192098 0.003652 \n", "2025-05-29 00:45:00+00:00 107884.072300 108060.946005 0.003279 \n", "2025-05-29 00:47:00+00:00 107897.923525 108051.067091 0.002839 \n", "2025-05-29 00:48:00+00:00 107912.089525 108049.965250 0.002555 \n", "\n", " BBP_20_2.0 ATR_14 return_1m return_3m \\\n", "timestamp \n", "2025-05-29 00:40:00+00:00 0.561241 76.081607 0.000338 -0.000014 \n", "2025-05-29 00:42:00+00:00 0.361672 76.737786 -0.000664 -0.001301 \n", "2025-05-29 00:45:00+00:00 0.508319 76.117071 0.000624 0.000297 \n", "2025-05-29 00:47:00+00:00 0.710412 78.004607 0.000698 0.000658 \n", "2025-05-29 00:48:00+00:00 0.852965 77.682679 0.000436 0.001759 \n", "\n", " return_5m return_10m \n", "timestamp \n", "2025-05-29 00:40:00+00:00 -0.000097 0.000023 \n", "2025-05-29 00:42:00+00:00 -0.001051 -0.001636 \n", "2025-05-29 00:45:00+00:00 -0.000054 -0.001695 \n", "2025-05-29 00:47:00+00:00 0.000020 -0.000222 \n", "2025-05-29 00:48:00+00:00 0.001432 0.000877 \n", "\n", "[5 rows x 25 columns]\n", "\n", "Number of rows in featured_data: 5558\n" ] } ], "source": [ "import pandas as pd\n", "from finta import TA # Import the TA module from finta\n", "import numpy as np # For np.nan if needed for BBP/BBB calculation edge cases\n", "\n", "# This is your existing GOOD CODE (data fetching part)\n", "# I'm including it here just for completeness of the script context\n", "# ... (paste your GOOD CODE here, from 'import os' down to the btc_data fetching and printing)\n", "# For brevity, I'll assume btc_data is already fetched as per your GOOD CODE.\n", "\n", "# --- MODIFIED create_features FUNCTION USING finta ---\n", "def create_features_finta(df):\n", " if df.empty or 'close' not in df.columns:\n", " print(\"DataFrame is empty or 'close' column is missing.\")\n", " return df.copy() # Return a copy to avoid modifying original if it's returned early\n", "\n", " # Ensure required columns for some indicators are present\n", " required_ohlc = ['open', 'high', 'low', 'close']\n", " if not all(col in df.columns for col in required_ohlc):\n", " print(f\"DataFrame is missing one or more required columns: {required_ohlc}\")\n", " # Attempt to continue if only 'close' is needed for some indicators,\n", " # but ATR and BBands might fail or need to be skipped.\n", " # For now, let's be strict for this example.\n", " # You might want to add more nuanced handling later.\n", " # return df.copy() # Or raise an error\n", "\n", " # SMA (Simple Moving Average)\n", " df['SMA_10'] = TA.SMA(df, period=10, column='close')\n", " df['SMA_30'] = TA.SMA(df, period=30, column='close')\n", "\n", " # EMA (Exponential Moving Average)\n", " df['EMA_10'] = TA.EMA(df, period=10, column='close') # finta's EMA defaults to 'close' if not specified\n", "\n", " # RSI (Relative Strength Index)\n", " df['RSI_14'] = TA.RSI(df, period=14, column='close')\n", "\n", " # MACD (Moving Average Convergence Divergence)\n", " # TA.MACD returns a DataFrame with 'MACD' and 'SIGNAL' columns\n", " macd_df = TA.MACD(df, period_fast=12, period_slow=26, signal=9, column='close')\n", " df['MACD_12_26_9'] = macd_df['MACD']\n", " df['MACDs_12_26_9'] = macd_df['SIGNAL'] # This is the signal line\n", " df['MACDh_12_26_9'] = df['MACD_12_26_9'] - df['MACDs_12_26_9'] # Histogram is MACD - Signal\n", "\n", " # Bollinger Bands (BBANDS)\n", " # TA.BBANDS returns a DataFrame with 'BB_UPPER', 'BB_MIDDLE', 'BB_LOWER'\n", " # It uses 'high', 'low', 'close' columns by default.\n", " if all(col in df.columns for col in ['high', 'low', 'close']):\n", " bb_df = TA.BBANDS(df, period=20, std_multiplier=2.0) # std_multiplier is for standard deviations\n", " df['BBL_20_2.0'] = bb_df['BB_LOWER']\n", " df['BBM_20_2.0'] = bb_df['BB_MIDDLE'] # This is the SMA(20)\n", " df['BBU_20_2.0'] = bb_df['BB_UPPER']\n", "\n", " # Calculate BBB (Bollinger Band Width) and BBP (Bollinger Band Percent B) manually\n", " # BBB = (Upper Band - Lower Band) / Middle Band\n", " df['BBB_20_2.0'] = (df['BBU_20_2.0'] - df['BBL_20_2.0']) / df['BBM_20_2.0']\n", " # BBP = (Price - Lower Band) / (Upper Band - Lower Band)\n", " # Handle division by zero if Upper Band == Lower Band (can happen in flat markets or with very short periods)\n", " band_range = df['BBU_20_2.0'] - df['BBL_20_2.0']\n", " df['BBP_20_2.0'] = np.where(band_range == 0, 0.5, (df['close'] - df['BBL_20_2.0']) / band_range) # if range is 0, BBP is undefined, set to 0.5 (middle) or NaN\n", " else:\n", " print(\"Skipping Bollinger Bands calculation as 'high', 'low', 'close' columns are not all present.\")\n", " for col_name in ['BBL_20_2.0', 'BBM_20_2.0', 'BBU_20_2.0', 'BBB_20_2.0', 'BBP_20_2.0']:\n", " df[col_name] = np.nan\n", "\n", "\n", " # ATR (Average True Range)\n", " # TA.ATR requires 'high', 'low', 'close' columns.\n", " if all(col in df.columns for col in ['high', 'low', 'close']):\n", " df['ATR_14'] = TA.ATR(df, period=14)\n", " else:\n", " print(\"Skipping ATR calculation as 'high', 'low', 'close' columns are not all present.\")\n", " df['ATR_14'] = np.nan\n", "\n", "\n", " # Lagged returns (this part is standard pandas, no change needed)\n", " for lag in [1, 3, 5, 10]:\n", " df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)\n", "\n", " df.dropna(inplace=True) # Remove rows with NaNs created by indicators\n", " return df\n", "\n", "# --- End of MODIFIED create_features FUNCTION ---\n", "\n", "# Your existing code to call the function (make sure btc_data is defined from GOOD CODE)\n", "if 'btc_data' in locals() and not btc_data.empty:\n", " # It's good practice to lowercase column names if the library expects it,\n", " # though Alpaca's .df usually does this. Let's be explicit.\n", " btc_data.columns = [col.lower() for col in btc_data.columns]\n", " \n", " featured_data = create_features_finta(btc_data.copy()) # Use .copy() to avoid modifying original\n", " print(\"\\nData with features (using finta):\")\n", " if not featured_data.empty:\n", " print(featured_data.head())\n", " print(f\"\\nNumber of rows in featured_data: {len(featured_data)}\")\n", " else:\n", " print(\"Featured data is empty after processing.\")\n", "elif 'btc_data' in locals() and btc_data.empty:\n", " print(\"Cannot create features, btc_data is empty.\")\n", " featured_data = pd.DataFrame() # ensure it's a DataFrame\n", "else:\n", " print(\"btc_data not found. Please run the data fetching part first.\")\n", " featured_data = pd.DataFrame()" ] }, { "cell_type": "markdown", "id": "d89232a3-b50e-467d-ab7a-f0af5cb884f9", "metadata": {}, "source": [ "To make this a complete, runnable script, combine your \"GOOD CODE\" with the modified \"BAD CODE\" section:" ] }, { "cell_type": "code", "execution_count": 5, "id": "235436b7-58a2-4531-8f4d-80aaf8377889", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Attempting to connect to Alpaca Paper Trading...\n", "Successfully connected. Account status: ACTIVE\n", "Fetching data from 2025-05-29T00:00:00+00:00 to 2025-06-04T00:00:00+00:00\n", "Fetching data from 2025-06-04T00:01:00+00:00 to 2025-06-05T00:00:00+00:00\n", "\n", "Fetched 5587 rows of BTC/USD data.\n", "Sample data (first 5 rows):\n", " close high low trade_count \\\n", "timestamp \n", "2025-05-29 00:00:00+00:00 107854.950 107854.9500 107797.350 0 \n", "2025-05-29 00:01:00+00:00 107730.050 107786.4545 107730.050 0 \n", "2025-05-29 00:02:00+00:00 107784.335 107784.3350 107784.335 0 \n", "2025-05-29 00:03:00+00:00 107749.815 107749.8150 107749.815 0 \n", "2025-05-29 00:04:00+00:00 107669.645 107680.6050 107524.300 0 \n", "\n", " open volume vwap symbol \n", "timestamp \n", "2025-05-29 00:00:00+00:00 107853.9450 0.0 107826.15000 BTC/USD \n", "2025-05-29 00:01:00+00:00 107786.4545 0.0 107758.25225 BTC/USD \n", "2025-05-29 00:02:00+00:00 107784.3350 0.0 107784.33500 BTC/USD \n", "2025-05-29 00:03:00+00:00 107749.8150 0.0 107749.81500 BTC/USD \n", "2025-05-29 00:04:00+00:00 107680.6050 0.0 107602.45250 BTC/USD \n", "\n", "Sample data (last 5 rows):\n", " close high low trade_count \\\n", "timestamp \n", "2025-06-04 23:54:00+00:00 104807.747 104845.250 104807.747 0 \n", "2025-06-04 23:55:00+00:00 104816.255 104816.255 104782.185 0 \n", "2025-06-04 23:57:00+00:00 104744.600 104744.600 104744.600 0 \n", "2025-06-04 23:58:00+00:00 104732.050 104739.899 104732.050 0 \n", "2025-06-04 23:59:00+00:00 104748.560 104758.500 104657.600 1 \n", "\n", " open volume vwap symbol \n", "timestamp \n", "2025-06-04 23:54:00+00:00 104845.250 0.000000 104826.4985 BTC/USD \n", "2025-06-04 23:55:00+00:00 104782.185 0.000000 104799.2200 BTC/USD \n", "2025-06-04 23:57:00+00:00 104744.600 0.000000 104744.6000 BTC/USD \n", "2025-06-04 23:58:00+00:00 104739.899 0.000000 104735.9745 BTC/USD \n", "2025-06-04 23:59:00+00:00 104657.600 0.000072 104657.6000 BTC/USD \n", "\n", "Data with features (using finta):\n", " close high low trade_count \\\n", "timestamp \n", "2025-05-29 00:40:00+00:00 107891.3515 107891.3515 107891.3515 0 \n", "2025-05-29 00:42:00+00:00 107819.7350 107819.7350 107819.7350 0 \n", "2025-05-29 00:45:00+00:00 107887.0150 107887.0150 107887.0150 0 \n", "2025-05-29 00:47:00+00:00 107962.3700 107962.3700 107962.3700 0 \n", "2025-05-29 00:48:00+00:00 108009.4200 108009.4200 108009.4200 0 \n", "\n", " open volume vwap symbol \\\n", "timestamp \n", "2025-05-29 00:40:00+00:00 107891.3515 0.0 107891.3515 BTC/USD \n", "2025-05-29 00:42:00+00:00 107819.7350 0.0 107819.7350 BTC/USD \n", "2025-05-29 00:45:00+00:00 107887.0150 0.0 107887.0150 BTC/USD \n", "2025-05-29 00:47:00+00:00 107962.3700 0.0 107962.3700 BTC/USD \n", "2025-05-29 00:48:00+00:00 108009.4200 0.0 108009.4200 BTC/USD \n", "\n", " SMA_10 SMA_30 ... BBL_20_2.0 \\\n", "timestamp ... \n", "2025-05-29 00:40:00+00:00 107940.20850 107797.621567 ... 107628.818315 \n", "2025-05-29 00:42:00+00:00 107922.53925 107796.447733 ... 107677.261002 \n", "2025-05-29 00:45:00+00:00 107904.22360 107801.679900 ... 107707.198595 \n", "2025-05-29 00:47:00+00:00 107901.82610 107807.614400 ... 107744.779959 \n", "2025-05-29 00:48:00+00:00 107911.29420 107816.267900 ... 107774.213800 \n", "\n", " BBM_20_2.0 BBU_20_2.0 BBB_20_2.0 \\\n", "timestamp \n", "2025-05-29 00:40:00+00:00 107862.704675 108096.591035 0.004337 \n", "2025-05-29 00:42:00+00:00 107874.226550 108071.192098 0.003652 \n", "2025-05-29 00:45:00+00:00 107884.072300 108060.946005 0.003279 \n", "2025-05-29 00:47:00+00:00 107897.923525 108051.067091 0.002839 \n", "2025-05-29 00:48:00+00:00 107912.089525 108049.965250 0.002555 \n", "\n", " BBP_20_2.0 ATR_14 return_1m return_3m \\\n", "timestamp \n", "2025-05-29 00:40:00+00:00 0.561241 76.081607 0.000338 -0.000014 \n", "2025-05-29 00:42:00+00:00 0.361672 76.737786 -0.000664 -0.001301 \n", "2025-05-29 00:45:00+00:00 0.508319 76.117071 0.000624 0.000297 \n", "2025-05-29 00:47:00+00:00 0.710412 78.004607 0.000698 0.000658 \n", "2025-05-29 00:48:00+00:00 0.852965 77.682679 0.000436 0.001759 \n", "\n", " return_5m return_10m \n", "timestamp \n", "2025-05-29 00:40:00+00:00 -0.000097 0.000023 \n", "2025-05-29 00:42:00+00:00 -0.001051 -0.001636 \n", "2025-05-29 00:45:00+00:00 -0.000054 -0.001695 \n", "2025-05-29 00:47:00+00:00 0.000020 -0.000222 \n", "2025-05-29 00:48:00+00:00 0.001432 0.000877 \n", "\n", "[5 rows x 25 columns]\n", "\n", "Number of rows in featured_data: 5558\n", "\n", "Columns in featured_data:\n", "['close', 'high', 'low', 'trade_count', 'open', 'volume', 'vwap', 'symbol', 'SMA_10', 'SMA_30', 'EMA_10', 'RSI_14', 'MACD_12_26_9', 'MACDs_12_26_9', 'MACDh_12_26_9', 'BBL_20_2.0', 'BBM_20_2.0', 'BBU_20_2.0', 'BBB_20_2.0', 'BBP_20_2.0', 'ATR_14', 'return_1m', 'return_3m', 'return_5m', 'return_10m']\n" ] } ], "source": [ "import os\n", "import alpaca_trade_api as tradeapi\n", "import pandas as pd\n", "from datetime import datetime, timedelta\n", "import time\n", "from finta import TA # Import for technical indicators\n", "import numpy as np # For np.nan\n", "\n", "# For Paper Trading\n", "os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets'\n", "# Replace with your actual keys or set them as environment variables\n", "# Make sure these are set correctly if you haven't already\n", "# IMPORTANT: Replace with your actual keys for testing\n", "#os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID' # <--- REPLACE\n", "#os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY' # <--- REPLACE\n", "os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID'\n", "os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'\n", "\n", "API_KEY = os.getenv('APCA_API_KEY_ID')\n", "API_SECRET = os.getenv('APCA_API_SECRET_KEY')\n", "BASE_URL = os.getenv('APCA_API_BASE_URL')\n", "\n", "# Ensure API_KEY and API_SECRET are not None\n", "if not API_KEY or not API_SECRET or API_KEY == 'YOUR_PAPER_API_KEY_ID':\n", " raise ValueError(\"API_KEY or API_SECRET not set or still default. Please set them as environment variables or directly in the script.\")\n", "\n", "api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')\n", "\n", "print(\"Attempting to connect to Alpaca Paper Trading...\")\n", "try:\n", " account = api.get_account()\n", " print(f\"Successfully connected. Account status: {account.status}\")\n", "except Exception as e:\n", " print(f\"Failed to connect or get account info: {e}\")\n", " exit()\n", "\n", "\n", "def fetch_data(symbol, timeframe_enum, start_date_str, end_date_str=None):\n", " if end_date_str is None:\n", " end_date_str = datetime.now().strftime('%Y-%m-%d')\n", "\n", " all_bars = []\n", " start_dt = pd.to_datetime(start_date_str, utc=True)\n", " end_dt = pd.to_datetime(end_date_str, utc=True)\n", "\n", " current_start = start_dt\n", " while current_start < end_dt:\n", " chunk_end_candidate = current_start + timedelta(days=6)\n", " chunk_end = min(chunk_end_candidate, end_dt)\n", " print(f\"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}\")\n", " try:\n", " bars = api.get_crypto_bars(\n", " symbol,\n", " timeframe_enum,\n", " start=current_start.isoformat(),\n", " end=chunk_end.isoformat(),\n", " limit=10000\n", " ).df\n", " except Exception as e:\n", " print(f\"Error fetching data chunk: {e}\")\n", " current_start = chunk_end\n", " time.sleep(5)\n", " continue\n", "\n", " if bars.empty:\n", " print(f\"No data found for chunk starting {current_start.isoformat()}\")\n", " if current_start >= end_dt and not all_bars:\n", " break\n", " current_start = chunk_end\n", " time.sleep(1)\n", " continue\n", "\n", " all_bars.append(bars)\n", " if not bars.index.empty:\n", " if timeframe_enum == tradeapi.TimeFrame.Minute:\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(minutes=1)\n", " elif timeframe_enum == tradeapi.TimeFrame.Hour:\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(hours=1)\n", " else:\n", " current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(days=1)\n", " if current_start > end_dt and chunk_end_candidate >= end_dt :\n", " break\n", " else:\n", " current_start = chunk_end\n", " time.sleep(1)\n", "\n", " if not all_bars:\n", " print(\"No data fetched. Check your date range, symbol, or API connection.\")\n", " return pd.DataFrame()\n", "\n", " df = pd.concat(all_bars)\n", " df = df[~df.index.duplicated(keep='first')]\n", " df = df.sort_index()\n", " df = df[(df.index >= start_dt) & (df.index <= end_dt)]\n", " return df\n", "\n", "# Example usage:\n", "symbol = \"BTC/USD\"\n", "timeframe_to_use = tradeapi.TimeFrame.Minute\n", "start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')\n", "\n", "btc_data = fetch_data(symbol, timeframe_to_use, start_date)\n", "\n", "if not btc_data.empty:\n", " print(f\"\\nFetched {len(btc_data)} rows of {symbol} data.\")\n", " print(\"Sample data (first 5 rows):\")\n", " print(btc_data.head())\n", " print(\"\\nSample data (last 5 rows):\")\n", " print(btc_data.tail())\n", " # Ensure column names are lowercase for finta\n", " btc_data.columns = [col.lower() for col in btc_data.columns]\n", "else:\n", " print(\"Failed to fetch data or no data available for the period.\")\n", " btc_data = pd.DataFrame() # ensure it's a DataFrame for subsequent steps\n", "\n", "\n", "# --- REVISED create_features FUNCTION USING finta ---\n", "def create_features(df_input): # Renamed input to df_input to avoid clash with outer scope df\n", " df = df_input.copy() # Work on a copy\n", "\n", " if df.empty or 'close' not in df.columns:\n", " print(\"DataFrame is empty or 'close' column is missing.\")\n", " return df # Return the empty or deficient (but copied) DataFrame\n", "\n", " # Ensure required columns for some indicators are present\n", " required_ohlc = ['open', 'high', 'low', 'close']\n", " has_ohlc = all(col in df.columns for col in required_ohlc)\n", "\n", " # SMA (Simple Moving Average)\n", " df['SMA_10'] = TA.SMA(df, period=10, column='close')\n", " df['SMA_30'] = TA.SMA(df, period=30, column='close')\n", "\n", " # EMA (Exponential Moving Average)\n", " df['EMA_10'] = TA.EMA(df, period=10, column='close')\n", "\n", " # RSI (Relative Strength Index)\n", " df['RSI_14'] = TA.RSI(df, period=14, column='close')\n", "\n", " # MACD (Moving Average Convergence Divergence)\n", " macd_df = TA.MACD(df, period_fast=12, period_slow=26, signal=9, column='close')\n", " df['MACD_12_26_9'] = macd_df['MACD']\n", " df['MACDs_12_26_9'] = macd_df['SIGNAL']\n", " df['MACDh_12_26_9'] = df['MACD_12_26_9'] - df['MACDs_12_26_9']\n", "\n", " # Bollinger Bands (BBANDS)\n", " if has_ohlc:\n", " bb_df = TA.BBANDS(df, period=20, std_multiplier=2.0)\n", " df['BBL_20_2.0'] = bb_df['BB_LOWER']\n", " df['BBM_20_2.0'] = bb_df['BB_MIDDLE']\n", " df['BBU_20_2.0'] = bb_df['BB_UPPER']\n", " df['BBB_20_2.0'] = (df['BBU_20_2.0'] - df['BBL_20_2.0']) / df['BBM_20_2.0']\n", " band_range = df['BBU_20_2.0'] - df['BBL_20_2.0']\n", " df['BBP_20_2.0'] = np.where(band_range == 0, 0.5, (df['close'] - df['BBL_20_2.0']) / band_range)\n", " else:\n", " print(\"Skipping Bollinger Bands calculation as 'open', 'high', 'low', 'close' columns are not all present.\")\n", " for col_name in ['BBL_20_2.0', 'BBM_20_2.0', 'BBU_20_2.0', 'BBB_20_2.0', 'BBP_20_2.0']:\n", " df[col_name] = np.nan\n", "\n", " # ATR (Average True Range)\n", " if has_ohlc:\n", " df['ATR_14'] = TA.ATR(df, period=14)\n", " else:\n", " print(\"Skipping ATR calculation as 'open', 'high', 'low', 'close' columns are not all present.\")\n", " df['ATR_14'] = np.nan\n", "\n", " # Lagged returns\n", " for lag in [1, 3, 5, 10]:\n", " df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)\n", "\n", " df.dropna(inplace=True)\n", " return df\n", "# --- End of REVISED create_features FUNCTION ---\n", "\n", "\n", "# Call the feature creation function\n", "if not btc_data.empty:\n", " featured_data = create_features(btc_data) # No need for .copy() here as create_features does it\n", " print(\"\\nData with features (using finta):\")\n", " if not featured_data.empty:\n", " print(featured_data.head())\n", " print(f\"\\nNumber of rows in featured_data: {len(featured_data)}\")\n", " print(\"\\nColumns in featured_data:\")\n", " print(featured_data.columns.tolist())\n", " else:\n", " print(\"Featured data is empty after processing and NaN removal.\")\n", "else:\n", " print(\"Cannot create features, btc_data is empty.\")\n", " featured_data = pd.DataFrame()" ] }, { "cell_type": "markdown", "id": "3d7c387a-6e69-4e82-9255-725b9f653604", "metadata": {}, "source": [ "Explanation of Changes and finta Usage:\n", "\n", " Import finta: from finta import TA\n", "\n", " Column Names: finta generally expects lowercase 'open', 'high', 'low', 'close'. I've added btc_data.columns = [col.lower() for col in btc_data.columns] after fetching data to ensure this.\n", "\n", " SMA, EMA, RSI: These are straightforward. TA.SMA(df, period=X, column='close').\n", "\n", " MACD: TA.MACD() returns a DataFrame. We extract 'MACD' and 'SIGNAL' columns and calculate the histogram (MACDh) as their difference.\n", "\n", " Bollinger Bands: TA.BBANDS() also returns a DataFrame with 'BB_UPPER', 'BB_MIDDLE', 'BB_LOWER'.\n", "\n", " I've added manual calculation for BBB_20_2.0 (Bandwidth) and BBP_20_2.0 (Percent B) as finta doesn't provide these directly in the same format as pandas-ta.\n", "\n", " Added a check for band_range == 0 in BBP calculation to prevent division by zero.\n", "\n", " ATR: TA.ATR() is used. This function expects 'high', 'low', and 'close' columns in the input DataFrame.\n", "\n", " OHLC Check: Added a has_ohlc check because ATR and BBANDS require 'open', 'high', 'low', 'close' columns. If not present, these indicators are skipped and their columns filled with NaN (which will then be dropped by dropna).\n", "\n", " .copy(): The create_features function now explicitly works on a copy of the input DataFrame (df = df_input.copy()) to prevent unintended side effects on the original btc_data.\n", "\n", " Error Handling/Column Existence: Added checks to ensure 'close' and other OHLC columns exist before attempting to use them.\n", "\n", "This should resolve the ImportError and give you the desired features using the finta library. Remember to replace 'YOUR_PAPER_API_KEY_ID' and 'YOUR_PAPER_API_SECRET_KEY' with your actual Alpaca paper trading credentials." ] }, { "cell_type": "markdown", "id": "d91cfcab-87c3-4a45-a654-fbfddfc0d9a8", "metadata": {}, "source": [ "### 3rd Prompt" ] }, { "cell_type": "markdown", "id": "eaee3b64-81c6-4c66-8459-87d32021a260", "metadata": {}, "source": [ "Ok. Next step you previously proposed was like this (below). Can you adjust it to make it work with this new library and all the other changes if neccessary:..." ] }, { "cell_type": "markdown", "id": "a915ced3-3eff-498d-a742-83c89f242897", "metadata": {}, "source": [ "Step 4: Model Training - Defining Target & Training\n", "\n", "Let's define a simple target: will the price be higher or lower in N minutes?\n" ] }, { "cell_type": "code", "execution_count": null, "id": "9312cf42-69a6-401b-83d2-71bb47006754", "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import train_test_split, TimeSeriesSplit\n", "from sklearn.ensemble import RandomForestClassifier # Example model\n", "from sklearn.metrics import accuracy_score, classification_report\n", "import xgboost as xgb\n", "\n", "def prepare_data_for_model(df, target_horizon=5, test_size=0.2):\n", " \"\"\"\n", " Prepares X (features) and y (target) for the ML model.\n", " Target: 1 if price increases by more than a small threshold after target_horizon periods, 0 otherwise.\n", " -1 if price decreases by more than a small threshold. (Optional: make it 3 classes)\n", " \"\"\"\n", " if df.empty or 'close' not in df.columns:\n", " print(\"DataFrame is empty or 'close' column is missing.\")\n", " return pd.DataFrame(), pd.Series(dtype='float64'), pd.DataFrame(), pd.Series(dtype='float64')\n", "\n", " # Define target: 1 if price goes up in `target_horizon` minutes, 0 otherwise\n", " # A small threshold can help avoid noise around 0% change\n", " # price_threshold = 0.0005 # e.g., 0.05% change\n", " # df['future_price'] = df['close'].shift(-target_horizon)\n", " # df['price_change'] = (df['future_price'] - df['close']) / df['close']\n", " # df['target'] = 0 # Hold\n", " # df.loc[df['price_change'] > price_threshold, 'target'] = 1 # Buy\n", " # df.loc[df['price_change'] < -price_threshold, 'target'] = -1 # Sell (for 3-class)\n", " # For 2-class (Up/Not Up):\n", " df['target'] = (df['close'].shift(-target_horizon) > df['close']).astype(int)\n", "\n", " df.dropna(inplace=True) # Remove rows with NaN target (due to shift)\n", "\n", " feature_columns = [col for col in df.columns if col not in ['open', 'high', 'low', 'close', 'volume', 'trade_count', 'vwap', 'target', 'future_price', 'price_change']]\n", " X = df[feature_columns]\n", " y = df['target']\n", "\n", " # Time series split is crucial: DO NOT shuffle time series data for training\n", " # For a simple split:\n", " split_index = int(len(X) * (1 - test_size))\n", " X_train, X_test = X[:split_index], X[split_index:]\n", " y_train, y_test = y[:split_index], y[split_index:]\n", "\n", " # For more robust cross-validation, use TimeSeriesSplit\n", " # tscv = TimeSeriesSplit(n_splits=5)\n", " # for train_index, test_index in tscv.split(X):\n", " # X_train, X_test = X.iloc[train_index], X.iloc[test_index]\n", " # y_train, y_test = y.iloc[train_index], y.iloc[test_index]\n", " # Train and evaluate your model here\n", "\n", " return X_train, X_test, y_train, y_test, feature_columns\n", "\n", "\n", "if not featured_data.empty:\n", " X_train, X_test, y_train, y_test, feature_cols = prepare_data_for_model(featured_data.copy(), target_horizon=5)\n", "\n", " if not X_train.empty:\n", " print(f\"\\nTraining data shape: X_train: {X_train.shape}, y_train: {y_train.shape}\")\n", " print(f\"Test data shape: X_test: {X_test.shape}, y_test: {y_test.shape}\")\n", " print(f\"Features used: {feature_cols}\")\n", "\n", " # Example Model: Random Forest\n", " # model = RandomForestClassifier(n_estimators=100, random_state=42, class_weight='balanced')\n", "\n", " # Example Model: XGBoost (often performs well)\n", " model = xgb.XGBClassifier(\n", " objective='binary:logistic', # or 'multi:softprob' for multi-class\n", " n_estimators=100,\n", " learning_rate=0.1,\n", " max_depth=3,\n", " use_label_encoder=False, # Suppress a warning\n", " eval_metric='logloss' # or 'mlogloss' for multi-class\n", " )\n", "\n", " model.fit(X_train, y_train)\n", "\n", " # Evaluate on test set\n", " y_pred = model.predict(X_test)\n", " print(\"\\nModel Evaluation on Test Set:\")\n", " print(f\"Accuracy: {accuracy_score(y_test, y_pred):.4f}\")\n", " print(classification_report(y_test, y_pred, zero_division=0))\n", "\n", " # Feature importance (for tree-based models)\n", " if hasattr(model, 'feature_importances_'):\n", " importances = pd.Series(model.feature_importances_, index=X_train.columns).sort_values(ascending=False)\n", " print(\"\\nFeature Importances:\")\n", " print(importances.head(10))\n", " else:\n", " print(\"Not enough data to create training/test sets after feature engineering and target creation.\")\n", "else:\n", " print(\"Cannot prepare data for model, featured_data is empty.\")\n", " model = None # Ensure model is defined even if training fails" ] }, { "cell_type": "markdown", "id": "b9be78f1-5129-4f86-a95b-18c15f861140", "metadata": {}, "source": [ "Important Considerations for Modeling:\n", "\n", "Target Definition: This is critical. Predicting direction is hard. Predicting magnitude or using a threshold (e.g., price must move > 0.1% to be a \"1\") can be better.\n", "\n", "Class Imbalance: If \"up\" signals are rare, your model might be biased. Use techniques like class_weight='balanced' (for some models) or over/undersampling (e.g., SMOTE).\n", "\n", "Stationarity: Price series are generally non-stationary. Features like returns or indicators often help.\n", "\n", "Overfitting: Models can learn noise from historical data. Robust cross-validation (like TimeSeriesSplit) and regularization are key.\n", "\n" ] }, { "cell_type": "markdown", "id": "092911df-195c-499c-b0fa-4000e1997ae4", "metadata": {}, "source": [ "### Gemini:\n", "\n", "Okay, this \"Step 4: Model Training\" code can be adapted quite smoothly. The key is that featured_data (which is the output from your create_features function using finta) contains the 'close' column for target generation and all the engineered features. The logic for selecting feature_columns by excluding raw data columns and target-related columns should still work correctly.\n", "\n", "Here are the adjustments and the complete code for Step 4:\n", "\n", "Prerequisites:\n", "\n", " Make sure you have the necessary libraries installed:\n", "\n", " \n", " pip install scikit-learn xgboost pandas numpy\n", "\n", "..." ] }, { "cell_type": "markdown", "id": "7cf37c60-87f0-4f94-91b4-f8c1e0c9a3c5", "metadata": {}, "source": [ " This code assumes that featured_data (a pandas DataFrame) has been successfully created in the previous step (using finta for feature engineering) and is available in the global scope.\n", "\n", "Adjusted Step 4 Code:\n", " " ] }, { "cell_type": "code", "execution_count": 6, "id": "28188ce7-9a2a-4b92-a755-10d0890a05b5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Training data shape: X_train: (4446, 18), y_train: (4446,)\n", "Test data shape: X_test: (1112, 18), y_test: (1112,)\n", "Number of features: 18\n", "First 5 features: ['symbol', 'SMA_10', 'SMA_30', 'EMA_10', 'RSI_14']\n", "\n", "Training the XGBoost model...\n", "An error occurred during model training or evaluation: DataFrame.dtypes for data must be int, float, bool or category. When categorical type is supplied, the experimental DMatrix parameter`enable_categorical` must be set to `True`. Invalid columns:symbol: object\n" ] } ], "source": [ "import pandas as pd\n", "from sklearn.model_selection import train_test_split, TimeSeriesSplit # TimeSeriesSplit for more advanced CV\n", "from sklearn.ensemble import RandomForestClassifier # Example alternative model\n", "from sklearn.metrics import accuracy_score, classification_report\n", "import xgboost as xgb\n", "import numpy as np # For np.nan and other numpy operations if needed\n", "\n", "def prepare_data_for_model(df_input, target_horizon=5, test_size=0.2):\n", " \"\"\"\n", " Prepares X (features) and y (target) for the ML model.\n", " Target: 1 if price increases after target_horizon periods, 0 otherwise.\n", " \"\"\"\n", " df = df_input.copy() # Work on a copy to avoid modifying the original DataFrame\n", "\n", " if df.empty or 'close' not in df.columns:\n", " print(\"DataFrame is empty or 'close' column is missing for target creation.\")\n", " # Ensure 5 items are returned to match unpacking expectations\n", " return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), []\n", "\n", " # Define target: 1 if price goes up in `target_horizon` minutes, 0 otherwise\n", " # df['future_price'] = df['close'].shift(-target_horizon) # Intermediate step\n", " # df['target'] = (df['future_price'] > df['close']).astype(int)\n", " # Simplified:\n", " df['target'] = (df['close'].shift(-target_horizon) > df['close']).astype(int)\n", "\n", " # Optional: Define target with a threshold (more robust against noise)\n", " # price_threshold = 0.0005 # 0.05%\n", " # df['price_change_pct'] = (df['close'].shift(-target_horizon) - df['close']) / df['close']\n", " # df['target'] = 0 # Default to 'hold' or 'no significant change'\n", " # df.loc[df['price_change_pct'] > price_threshold, 'target'] = 1 # Price went up significantly\n", " # df.loc[df['price_change_pct'] < -price_threshold, 'target'] = 2 # Price went down significantly (for 3-class)\n", " # For binary (up vs not up significantly):\n", " # df['target'] = (df['price_change_pct'] > price_threshold).astype(int)\n", "\n", "\n", " df.dropna(inplace=True) # Remove rows with NaN target (due to shift) or NaNs from features\n", "\n", " # Identify feature columns:\n", " # These are the columns from the original data or intermediate target calculation steps\n", " # The 'featured_data' (input df) should have had its columns lowercased in the previous step.\n", " cols_to_exclude = ['open', 'high', 'low', 'close', 'volume', # Raw data\n", " 'trade_count', 'vwap', # Other raw data (if present)\n", " 'future_price', 'price_change_pct', 'target'] # Target-related (add any you create)\n", "\n", " # Filter out columns that might not exist in df to avoid errors, then exclude\n", " actual_cols_to_exclude = [col for col in cols_to_exclude if col in df.columns]\n", " \n", " feature_columns = [col for col in df.columns if col not in actual_cols_to_exclude]\n", "\n", " if not feature_columns:\n", " print(\"No feature columns found after exclusion. Check exclusion list and DataFrame contents.\")\n", " return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), []\n", " \n", " X = df[feature_columns]\n", " y = df['target']\n", "\n", " if X.empty or y.empty:\n", " print(\"X or y is empty after processing. Not enough data.\")\n", " return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), feature_columns\n", "\n", "\n", " # Time series split is crucial: DO NOT shuffle time series data for training\n", " # Simple chronological split:\n", " split_index = int(len(X) * (1 - test_size))\n", " X_train, X_test = X.iloc[:split_index], X.iloc[split_index:]\n", " y_train, y_test = y.iloc[:split_index], y.iloc[split_index:]\n", "\n", " # For more robust cross-validation, consider TimeSeriesSplit from sklearn.model_selection\n", " # tscv = TimeSeriesSplit(n_splits=5)\n", " # for train_idx, val_idx in tscv.split(X):\n", " # X_train_fold, X_val_fold = X.iloc[train_idx], X.iloc[val_idx]\n", " # y_train_fold, y_val_fold = y.iloc[train_idx], y.iloc[val_idx]\n", " # # Train model on X_train_fold, y_train_fold; evaluate on X_val_fold, y_val_fold\n", "\n", " return X_train, X_test, y_train, y_test, feature_columns\n", "\n", "\n", "# --- Main script execution for Step 4 ---\n", "# This assumes 'featured_data' DataFrame is available from the previous step\n", "model = None # Initialize model variable\n", "\n", "if 'featured_data' in locals() and not featured_data.empty:\n", " # Ensure 'close' column exists if not already checked by create_features\n", " if 'close' not in featured_data.columns:\n", " print(\"Error: 'close' column is missing in featured_data. Cannot proceed with model training.\")\n", " else:\n", " X_train, X_test, y_train, y_test, feature_cols = prepare_data_for_model(\n", " featured_data, # No .copy() needed here as prepare_data_for_model does it\n", " target_horizon=5, # Predict 5 minutes into the future\n", " test_size=0.2 # 20% of data for testing\n", " )\n", "\n", " if not X_train.empty and not y_train.empty and not X_test.empty and not y_test.empty:\n", " print(f\"\\nTraining data shape: X_train: {X_train.shape}, y_train: {y_train.shape}\")\n", " print(f\"Test data shape: X_test: {X_test.shape}, y_test: {y_test.shape}\")\n", " print(f\"Number of features: {len(feature_cols)}\")\n", " # print(f\"Features used: {feature_cols}\") # Can be very long, print first few if needed\n", " print(f\"First 5 features: {feature_cols[:5]}\")\n", "\n", "\n", " # Example Model: XGBoost (often performs well)\n", " # For XGBoost versions < 1.6, you might need use_label_encoder=False if y_train is a Pandas Series\n", " # For XGBoost versions >= 1.6, use_label_encoder is deprecated/removed.\n", " # If you encounter issues, ensure y_train is a NumPy array: y_train.to_numpy()\n", " model = xgb.XGBClassifier(\n", " objective='binary:logistic', # For binary up/down prediction\n", " n_estimators=100,\n", " learning_rate=0.1,\n", " max_depth=3,\n", " # use_label_encoder=False, # Removed for better compatibility with XGBoost >= 1.6\n", " eval_metric='logloss' # Evaluation metric for binary classification\n", " )\n", "\n", " print(\"\\nTraining the XGBoost model...\")\n", " try:\n", " model.fit(X_train, y_train) # XGBoost can often handle pandas DataFrames/Series directly\n", "\n", " # Evaluate on test set\n", " y_pred = model.predict(X_test)\n", " y_pred_proba = model.predict_proba(X_test)[:, 1] # Probabilities for the positive class\n", "\n", " print(\"\\nModel Evaluation on Test Set:\")\n", " print(f\"Accuracy: {accuracy_score(y_test, y_pred):.4f}\")\n", " \n", " # Check unique classes in y_test and y_pred for classification_report\n", " # This helps avoid errors if one class is missing in predictions or test set after split/dropna\n", " report = classification_report(y_test, y_pred, zero_division=0)\n", " print(report)\n", "\n", " # Feature importance (for tree-based models like XGBoost)\n", " if hasattr(model, 'feature_importances_'):\n", " importances = pd.Series(model.feature_importances_, index=X_train.columns).sort_values(ascending=False)\n", " print(\"\\nTop 10 Feature Importances:\")\n", " print(importances.head(10))\n", "\n", " except Exception as e:\n", " print(f\"An error occurred during model training or evaluation: {e}\")\n", " model = None # Ensure model is None if training failed\n", "\n", " else:\n", " print(\"Not enough data to create training/test sets after feature engineering and target creation.\")\n", " if 'feature_cols' in locals() and feature_cols: # Check if feature_cols was populated\n", " print(f\"Identified feature columns: {feature_cols[:5]}...\")\n", " else:\n", " print(\"Could not identify feature columns.\")\n", "\n", "else:\n", " print(\"Cannot prepare data for model, 'featured_data' is empty or not defined.\")\n", " # X_train, X_test, y_train, y_test, feature_cols will not be defined here" ] }, { "cell_type": "markdown", "id": "563f24c0-774d-47bf-b89d-e7087406f92e", "metadata": {}, "source": [ "Key changes and considerations:\n", "\n", " prepare_data_for_model adjustments:\n", "\n", " The early return now correctly returns 5 items to match the unpacking: return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), [].\n", "\n", " The cols_to_exclude list for defining feature_columns is made more robust by checking if the columns actually exist in the DataFrame before trying to use them in the list comprehension. This prevents errors if, for example, vwap or trade_count weren't available in the fetched data.\n", "\n", " Added more checks for empty X, y, or feature_columns inside prepare_data_for_model.\n", "\n", " XGBoost use_label_encoder: I've commented out use_label_encoder=False. Modern versions of XGBoost (>=1.6) handle this better or have deprecated the parameter. If you are using an older version and get a specific warning, you might need to add it back or convert y_train to a NumPy array (y_train.to_numpy()).\n", "\n", " Robustness: Added checks for featured_data existence and emptiness before calling prepare_data_for_model. Also added checks for the output of prepare_data_for_model before proceeding to train.\n", "\n", " Target Definition: The code uses a simple \"price up vs not up\" target. The commented-out section in prepare_data_for_model shows how you might introduce a price_threshold or create a 3-class target (up, down, hold), which often leads to more practical trading signals. If you change to a 3-class target, remember to update objective and eval_metric in XGBClassifier.\n", "\n", " Error Handling: Basic try-except block around model fitting and evaluation.\n", "\n", " Clarity: Added more print statements for feedback during execution.\n", "\n", "This Step 4 code should now integrate well with your previous steps using finta and provide a basic framework for training your first trading model. Remember that building a profitable trading bot is an iterative process involving careful feature engineering, robust target definition, rigorous backtesting, and risk management." ] }, { "cell_type": "markdown", "id": "14d95caf-73c9-4738-84c2-a718589389aa", "metadata": {}, "source": [ "An error occurred during model training or evaluation: DataFrame.dtypes for data must be int, float, bool or category. When categorical type is supplied, the experimental DMatrix parameter`enable_categorical` must be set to `True`. Invalid columns:symbol: object\n", "\n", "\n", "\n", "### Gemini:\n", "\n", "Ah, that's a very common and clear error message from XGBoost!\n", "\n", "The problem is Invalid columns:symbol: object.\n", "\n", "XGBoost, like most machine learning models, requires all input features to be numerical (integer, float, boolean). The symbol column in your featured_data is likely a string (e.g., \"BTC/USD\"), which is an object dtype in pandas, and XGBoost doesn't know how to process it directly.\n", "\n", "Solution:\n", "\n", "You need to remove or encode the symbol column before training the model.\n", "\n", " If the symbol is the same for all rows (e.g., you're only training on \"BTC/USD\" data at this point): The easiest solution is to simply remove it from the feature set as it provides no predictive information within that single dataset.\n", "\n", " If you plan to train a model on multiple symbols later: You would need to encode it, for example, using One-Hot Encoding. However, for a single symbol model, it's redundant.\n", "\n", "Let's go with the first, simpler option for now.\n", "\n", "How to fix the code:\n", "\n", "Modify the cols_to_exclude list in your prepare_data_for_model function to explicitly include 'symbol'.\n", "\n", "Here's the relevant part of the prepare_data_for_model function with the fix:" ] }, { "cell_type": "markdown", "id": "8fa91bce-bd87-466b-b0c5-ce25a7b18967", "metadata": {}, "source": [ "Explanation of the fix:\n", "\n", "By adding 'symbol' to the cols_to_exclude list, the feature_columns list comprehension will automatically exclude it when creating the X DataFrame that is fed into the XGBoost model.\n", "\n", "After making this change, your feature_cols will no longer include 'symbol', and the XGBoost training should proceed without this specific error.\n", "\n", "The rest of your \"Step 4\" code (the part that calls prepare_data_for_model and trains the model) doesn't need to change. Just update the prepare_data_for_model function.\n", "\n", "Run the script again, and it should get past this error. You'll then see the model training and evaluation output." ] }, { "cell_type": "code", "execution_count": 7, "id": "80c87557-0064-467c-8a24-5d2b51bf9b85", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Training data shape: X_train: (4446, 17), y_train: (4446,)\n", "Test data shape: X_test: (1112, 17), y_test: (1112,)\n", "Number of features: 17\n", "First 5 features: ['SMA_10', 'SMA_30', 'EMA_10', 'RSI_14', 'MACD_12_26_9']\n", "\n", "Training the XGBoost model...\n", "\n", "Model Evaluation on Test Set:\n", "Accuracy: 0.5153\n", " precision recall f1-score support\n", "\n", " 0 0.53 0.69 0.60 588\n", " 1 0.48 0.32 0.39 524\n", "\n", " accuracy 0.52 1112\n", " macro avg 0.51 0.50 0.49 1112\n", "weighted avg 0.51 0.52 0.50 1112\n", "\n", "\n", "Top 10 Feature Importances:\n", "BBU_20_2.0 0.076261\n", "SMA_30 0.072104\n", "SMA_10 0.071693\n", "BBM_20_2.0 0.064772\n", "MACD_12_26_9 0.064698\n", "ATR_14 0.064123\n", "BBL_20_2.0 0.063519\n", "EMA_10 0.062803\n", "return_10m 0.060938\n", "BBB_20_2.0 0.059963\n", "dtype: float32\n" ] } ], "source": [ "import pandas as pd\n", "from sklearn.model_selection import train_test_split, TimeSeriesSplit # TimeSeriesSplit for more advanced CV\n", "from sklearn.ensemble import RandomForestClassifier # Example alternative model\n", "from sklearn.metrics import accuracy_score, classification_report\n", "import xgboost as xgb\n", "import numpy as np # For np.nan and other numpy operations if needed\n", "\n", "def prepare_data_for_model(df_input, target_horizon=5, test_size=0.2):\n", " \"\"\"\n", " Prepares X (features) and y (target) for the ML model.\n", " Target: 1 if price increases after target_horizon periods, 0 otherwise.\n", " \"\"\"\n", " df = df_input.copy() # Work on a copy to avoid modifying the original DataFrame\n", "\n", " if df.empty or 'close' not in df.columns:\n", " print(\"DataFrame is empty or 'close' column is missing for target creation.\")\n", " return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), []\n", "\n", " df['target'] = (df['close'].shift(-target_horizon) > df['close']).astype(int)\n", " df.dropna(inplace=True)\n", "\n", " # Identify feature columns:\n", " # ----- THIS IS THE KEY CHANGE -----\n", " cols_to_exclude = ['open', 'high', 'low', 'close', 'volume', # Raw data\n", " 'trade_count', 'vwap', # Other raw data (if present)\n", " 'future_price', 'price_change_pct', 'target', # Target-related\n", " 'symbol'] # <<< ADD 'symbol' HERE\n", "\n", " # Filter out columns that might not exist in df to avoid errors, then exclude\n", " actual_cols_to_exclude = [col for col in cols_to_exclude if col in df.columns]\n", " \n", " feature_columns = [col for col in df.columns if col not in actual_cols_to_exclude]\n", "\n", " if not feature_columns:\n", " print(\"No feature columns found after exclusion. Check exclusion list and DataFrame contents.\")\n", " return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), []\n", " \n", " X = df[feature_columns]\n", " y = df['target']\n", "\n", " if X.empty or y.empty:\n", " print(\"X or y is empty after processing. Not enough data.\")\n", " return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), feature_columns\n", "\n", " split_index = int(len(X) * (1 - test_size))\n", " X_train, X_test = X.iloc[:split_index], X.iloc[split_index:]\n", " y_train, y_test = y.iloc[:split_index], y.iloc[split_index:]\n", "\n", " return X_train, X_test, y_train, y_test, feature_columns\n", "\n", "\n", "# --- Main script execution for Step 4 ---\n", "# This assumes 'featured_data' DataFrame is available from the previous step\n", "model = None # Initialize model variable\n", "\n", "if 'featured_data' in locals() and not featured_data.empty:\n", " # Ensure 'close' column exists if not already checked by create_features\n", " if 'close' not in featured_data.columns:\n", " print(\"Error: 'close' column is missing in featured_data. Cannot proceed with model training.\")\n", " else:\n", " X_train, X_test, y_train, y_test, feature_cols = prepare_data_for_model(\n", " featured_data, # No .copy() needed here as prepare_data_for_model does it\n", " target_horizon=5, # Predict 5 minutes into the future\n", " test_size=0.2 # 20% of data for testing\n", " )\n", "\n", " if not X_train.empty and not y_train.empty and not X_test.empty and not y_test.empty:\n", " print(f\"\\nTraining data shape: X_train: {X_train.shape}, y_train: {y_train.shape}\")\n", " print(f\"Test data shape: X_test: {X_test.shape}, y_test: {y_test.shape}\")\n", " print(f\"Number of features: {len(feature_cols)}\")\n", " # print(f\"Features used: {feature_cols}\") # Can be very long, print first few if needed\n", " print(f\"First 5 features: {feature_cols[:5]}\")\n", "\n", "\n", " # Example Model: XGBoost (often performs well)\n", " # For XGBoost versions < 1.6, you might need use_label_encoder=False if y_train is a Pandas Series\n", " # For XGBoost versions >= 1.6, use_label_encoder is deprecated/removed.\n", " # If you encounter issues, ensure y_train is a NumPy array: y_train.to_numpy()\n", " model = xgb.XGBClassifier(\n", " objective='binary:logistic', # For binary up/down prediction\n", " n_estimators=100,\n", " learning_rate=0.1,\n", " max_depth=3,\n", " # use_label_encoder=False, # Removed for better compatibility with XGBoost >= 1.6\n", " eval_metric='logloss' # Evaluation metric for binary classification\n", " )\n", "\n", " print(\"\\nTraining the XGBoost model...\")\n", " try:\n", " model.fit(X_train, y_train) # XGBoost can often handle pandas DataFrames/Series directly\n", "\n", " # Evaluate on test set\n", " y_pred = model.predict(X_test)\n", " y_pred_proba = model.predict_proba(X_test)[:, 1] # Probabilities for the positive class\n", "\n", " print(\"\\nModel Evaluation on Test Set:\")\n", " print(f\"Accuracy: {accuracy_score(y_test, y_pred):.4f}\")\n", " \n", " # Check unique classes in y_test and y_pred for classification_report\n", " # This helps avoid errors if one class is missing in predictions or test set after split/dropna\n", " report = classification_report(y_test, y_pred, zero_division=0)\n", " print(report)\n", "\n", " # Feature importance (for tree-based models like XGBoost)\n", " if hasattr(model, 'feature_importances_'):\n", " importances = pd.Series(model.feature_importances_, index=X_train.columns).sort_values(ascending=False)\n", " print(\"\\nTop 10 Feature Importances:\")\n", " print(importances.head(10))\n", "\n", " except Exception as e:\n", " print(f\"An error occurred during model training or evaluation: {e}\")\n", " model = None # Ensure model is None if training failed\n", "\n", " else:\n", " print(\"Not enough data to create training/test sets after feature engineering and target creation.\")\n", " if 'feature_cols' in locals() and feature_cols: # Check if feature_cols was populated\n", " print(f\"Identified feature columns: {feature_cols[:5]}...\")\n", " else:\n", " print(\"Could not identify feature columns.\")\n", "\n", "else:\n", " print(\"Cannot prepare data for model, 'featured_data' is empty or not defined.\")\n", " # X_train, X_test, y_train, y_test, feature_cols will not be defined here" ] }, { "cell_type": "markdown", "id": "383fc250-5cb1-4d6d-b81c-fd7e4a7cede1", "metadata": {}, "source": [ "### PROMPT\n", "\n", "It gave me this. What does it mean? " ] }, { "cell_type": "markdown", "id": "c887e155-d370-4d3c-9daa-c54576ffa37b", "metadata": {}, "source": [ "### GEMINI:\n", "\n", "This output is excellent! It means the model trained successfully after fixing the 'symbol' column issue. Let's break down what this output tells you:\n", "\n", " Data Shapes:\n", "\n", " Training data shape: X_train: (4446, 17), y_train: (4446,)\n", "\n", " You have 4446 data points (rows) for training.\n", "\n", " Each data point has 17 features (columns) after removing 'symbol' and other excluded columns.\n", "\n", " y_train has 4446 corresponding target labels.\n", "\n", " Test data shape: X_test: (1112, 17), y_test: (1112,)\n", "\n", " You have 1112 data points for testing, also with 17 features each.\n", "\n", " Number of features: 17\n", "\n", " Confirms the count of features used.\n", "\n", " First 5 features: ['SMA_10', 'SMA_30', 'EMA_10', 'RSI_14', 'MACD_12_26_9']\n", "\n", " Shows the names of the first few features, confirming 'symbol' is gone and your technical indicators are present.\n", "\n", " Model Training:\n", "\n", " Training the XGBoost model...\n", "\n", " This indicates the model.fit(X_train, y_train) command executed without crashing.\n", "\n", " Model Evaluation on Test Set:\n", "\n", " Accuracy: 0.5153\n", "\n", " The model correctly predicted whether the price would go up or not up 51.53% of the time on the unseen test data.\n", "\n", " For a binary classification problem on financial markets (which are notoriously noisy), an accuracy slightly above 50% is not uncommon for a first attempt. It's better than random guessing (50%), but there's definitely room for improvement.\n", "\n", " Classification Report: This provides a more detailed breakdown of performance for each class (0 and 1).\n", "\n", " Target: 1 if price increases after target_horizon periods, 0 otherwise.\n", "\n", " Class 0 (Price did NOT go up / went down or stayed same):\n", "\n", " precision: 0.53: When the model predicted \"0\", it was correct 53% of the time.\n", "\n", " recall: 0.69: The model correctly identified 69% of all actual \"0\" instances.\n", "\n", " f1-score: 0.60: The harmonic mean of precision and recall for class \"0\".\n", "\n", " support: 588: There were 588 actual instances of \"0\" in the test set.\n", "\n", " Class 1 (Price DID go up):\n", "\n", " precision: 0.48: When the model predicted \"1\", it was correct 48% of the time. (This means when it signals \"UP\", it's wrong slightly more often than it's right).\n", "\n", " recall: 0.32: The model only identified 32% of all actual \"1\" (price up) instances. It's missing a lot of opportunities.\n", "\n", " f1-score: 0.39: The harmonic mean for class \"1\".\n", "\n", " support: 524: There were 524 actual instances of \"1\" in the test set.\n", "\n", " Overall Averages:\n", "\n", " accuracy: 0.52 (rounded from 0.5153)\n", "\n", " macro avg: Averages the metrics for each class without considering class imbalance.\n", "\n", " weighted avg: Averages the metrics, weighted by the number of true instances for each class (support). This is often more informative if you have class imbalance.\n", "\n", " Interpretation of Classification Report:\n", "\n", " The model is better at identifying instances where the price doesn't go up (higher recall for class 0) than when it does go up (lower recall for class 1).\n", "\n", " The precision for predicting an \"up\" movement (class 1) is below 0.5, which is not ideal for a trading signal you'd act upon directly without further refinement, as it means you'd be wrong more often than right when it predicts \"up\".\n", "\n", " This performance is typical for an initial model in a complex domain like financial markets. It's a starting point.\n", "\n", " Top 10 Feature Importances:\n", "\n", " This list shows which features the XGBoost model found most influential in making its predictions.\n", "\n", " BBU_20_2.0 0.076261 (Bollinger Bands Upper)\n", "\n", " SMA_30 0.072104\n", "\n", " SMA_10 0.071693\n", "\n", " BBM_20_2.0 0.064772 (Bollinger Bands Middle)\n", "\n", " MACD_12_26_9 0.064698\n", "\n", " ATR_14 0.064123\n", "\n", " BBL_20_2.0 0.063519 (Bollinger Bands Lower)\n", "\n", " EMA_10 0.062803\n", "\n", " return_10m 0.060938\n", "\n", " BBB_20_2.0 0.059963 (Bollinger Bands Width)\n", "\n", " Interpretation:\n", "\n", " It seems various Bollinger Band components, moving averages, MACD, ATR, and a 10-minute lagged return are all contributing.\n", "\n", " No single feature overwhelmingly dominates, which is common.\n", "\n", " This information can be useful for future feature engineering or selection (e.g., focusing on refining these types of indicators or trying variations).\n", "\n", "What does this mean for your trading bot?\n", "\n", " It's a Start: You have a working pipeline from data fetching, feature engineering, to model training and evaluation. This is a significant achievement.\n", "\n", " Performance is Modest: An accuracy of ~51-52% and a precision of <50% for buy signals means this model, as is, is likely not consistently profitable if used directly for trading. Transaction costs would likely eat any tiny edge.\n", "\n", " Next Steps (Crucial):\n", "\n", " Refine Target Variable:\n", "\n", " The current target (df['close'].shift(-target_horizon) > df['close']) is very noisy.\n", "\n", " Consider using a threshold: (future_price - current_price) / current_price > 0.001 (e.g., price must go up by at least 0.1%). This filters out tiny, insignificant movements.\n", "\n", " Consider a 3-class problem (up, down, hold/sideways) if you want to trade both long and short, or avoid trading in uncertain conditions.\n", "\n", " Feature Engineering:\n", "\n", " Experiment with different indicator periods.\n", "\n", " Add more diverse features: volatility measures (e.g., standard deviation of returns), other momentum indicators, interaction terms between features.\n", "\n", " Consider features from different timeframes (e.g., 5-min RSI, 15-min SMA).\n", "\n", " Hyperparameter Tuning:\n", "\n", " The XGBoost parameters (n_estimators, learning_rate, max_depth) are just defaults. Use techniques like GridSearchCV or RandomizedSearchCV to find better parameters.\n", "\n", " Address Class Imbalance (if it becomes an issue):\n", "\n", " If your target variable becomes imbalanced (e.g., far fewer \"up\" signals than \"not up\"), use techniques like scale_pos_weight in XGBoost, or over/undersampling methods (SMOTE). The current support (588 vs 524) is fairly balanced.\n", "\n", " More Sophisticated Validation:\n", "\n", " Use TimeSeriesSplit for cross-validation to get a more robust estimate of performance, as simple train-test splits can be misleading with time-series data.\n", "\n", " Backtesting Engine: The ultimate test is a proper backtest. Scikit-learn metrics are a good first step, but a backtesting engine will simulate trades, include transaction costs, slippage (if possible), and give you a better idea of real-world P&L. Libraries like backtesting.py, Zipline, or bt can be used.\n", "\n", " Longer Data Period: 7 days of 1-minute data is a good start for debugging, but for robust model training, you'll want much more data (months or even years, if feasible, though market regimes change).\n", "\n", "In summary, you've successfully built the core mechanics. Now the iterative process of improving the model's predictive power begins! The current results are a baseline to improve upon." ] }, { "cell_type": "markdown", "id": "0396098c-89ac-46db-a011-8c179cb7be5f", "metadata": {}, "source": [ "### Step 5: Backtesting (Simplified Vectorized Example)\n", "\n", "A proper backtest is event-driven and considers transaction costs, slippage, etc. This is a very simplified version." ] }, { "cell_type": "markdown", "id": "b92fb501-06d6-4f38-bba0-a4f39f7c49e5", "metadata": {}, "source": [ "### Prompt\n", "\n", "Ok. Now you proposed backtesting with the code below. Can you adjust the code once again to work with the one we have working already. Step 5: Backtesting (Simplified Vectorized Example)" ] }, { "cell_type": "markdown", "id": "ce6e3c3f-ed04-499c-bade-6d1bae6df834", "metadata": {}, "source": [ "### GEMINI\n", "\n", "Okay, let's integrate this simplified backtesting step (Step 5) into your existing workflow.\n", "\n", "The core idea is:\n", "\n", " Take the X_test data (which your model hasn't seen during training).\n", "\n", " Use your trained model to make predictions on X_test. These predictions will be our trading signals.\n", "\n", " Merge these predictions back with the original featured_data (specifically, the rows corresponding to X_test.index) so we have the 'close' prices needed for the backtest simulation.\n", "\n", " Run the run_simple_backtest function.\n", "\n", "Prerequisites:\n", "\n", " You'll need matplotlib if you want to uncomment the plotting part:" ] }, { "cell_type": "code", "execution_count": 8, "id": "2d9b9ee9-0a31-45a0-8097-75c33edcc2b0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: matplotlib in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (3.10.0)\n", "Requirement already satisfied: contourpy>=1.0.1 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from matplotlib) (1.3.1)\n", "Requirement already satisfied: cycler>=0.10 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from matplotlib) (0.12.1)\n", "Requirement already satisfied: fonttools>=4.22.0 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from matplotlib) (4.55.6)\n", "Requirement already satisfied: kiwisolver>=1.3.1 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from matplotlib) (1.4.8)\n", "Requirement already satisfied: numpy>=1.23 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from matplotlib) (2.2.5)\n", "Requirement already satisfied: packaging>=20.0 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from matplotlib) (24.2)\n", "Requirement already satisfied: pillow>=8 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from matplotlib) (11.1.0)\n", "Requirement already satisfied: pyparsing>=2.3.1 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from matplotlib) (3.2.1)\n", "Requirement already satisfied: python-dateutil>=2.7 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from matplotlib) (2.9.0.post0)\n", "Requirement already satisfied: six>=1.5 in c:\\users\\micro\\anaconda3\\envs\\alpaca_ai_env\\lib\\site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)\n", "Note: you may need to restart the kernel to use updated packages.\n" ] } ], "source": [ "pip install matplotlib" ] }, { "cell_type": "markdown", "id": "87e4d10d-0d12-48d3-b4ee-9f32b31ef536", "metadata": {}, "source": [ "Adjusted Step 5 Code (to be appended to your script):\n", "\n", "This code assumes model, X_test, and featured_data are available from the previous steps." ] }, { "cell_type": "markdown", "id": "e2744cb3-57f4-4e21-8437-52b6e2161cc9", "metadata": {}, "source": [ "Key Changes and Considerations:\n", "\n", " run_simple_backtest Function:\n", "\n", " Takes df_with_predictions_input and makes a copy to avoid modifying the original.\n", "\n", " Transaction Costs: The transaction_cost_pct is now applied to both buy and sell sides. For Alpaca crypto, the taker fee is typically between 0.1% and 0.3% (e.g., 0.001 to 0.003). I've set it to 0.001 (0.1%) as an example. This is a critical parameter.\n", "\n", " Cost/Proceeds Calculation: Explicitly calculates cost of purchase + fee for buys, and sale value - fee for sells.\n", "\n", " Trade Logging: Added a trades_log list to store details of each trade, which can be useful for analysis.\n", "\n", " Descriptive Naming: portfolio_value list renamed to portfolio_value_over_time.\n", "\n", " DatetimeIndex Check: Added a check and attempted conversion for the DataFrame index to ensure it's a pd.DatetimeIndex for better time-series handling and plotting.\n", "\n", " Improved Print Output: Formatted numbers with commas for readability.\n", "\n", " Plotting Markers: The plotting section now tries to add markers for actual buy/sell trades using the trades_log.\n", "\n", " Return Value: The function now returns both the DataFrame with portfolio values and the trades_log.\n", "\n", " Calling the Backtest:\n", "\n", " Data Alignment: The crucial part is data_for_backtest = featured_data.loc[X_test.index].copy(). This takes the featured_data (which includes 'open', 'high', 'low', 'close', 'symbol', and all engineered features) and filters it to only the rows that were part of your X_test set. This ensures you have the 'close' prices for the backtest period.\n", "\n", " Adding Predictions: data_for_backtest['predicted_signal'] = model.predict(X_test) adds the model's predictions to this aligned DataFrame. X_test (the features) is used for prediction, and the result is assigned to data_for_backtest.\n", "\n", " Error Handling: Added more robust checks for variable existence (model, X_test, featured_data) and more detailed error messages, including a try-except around the prediction and backtest call. A ValueError catch is included as this is common if feature sets mismatch.\n", "\n", " Printing Trade Log: Added a snippet to print the last few trades from the log.\n", "\n", "To Use This:\n", "\n", " Append the run_simple_backtest function and the calling code block to the end of your existing script (after Step 4 where model, X_test, and featured_data are created).\n", "\n", " Ensure matplotlib is installed if you want the plot.\n", "\n", " Run your entire script.\n", "\n", "Important Notes on this Simplified Backtest:\n", "\n", " No Look-Ahead Bias (in this version): Because we're predicting on X_test (which was held out) and then using those predictions to simulate trades on the same X_test period's prices, we are generally okay regarding look-ahead bias for this specific evaluation.\n", "\n", " Assumptions:\n", "\n", " Trades execute at the 'close' price of the bar where the signal is generated.\n", "\n", " No slippage (you always get the price you expect).\n", "\n", " trade_size_usd is fixed. You might later want to trade a percentage of your capital.\n", "\n", " Only long positions are taken.\n", "\n", " Strategy: The strategy is very basic:\n", "\n", " If signal is 1 (model predicts \"up\") and not in position: BUY.\n", "\n", " If signal is 0 (model predicts \"not up\") and in position: SELL.\n", "\n", " This means you exit a position if the model no longer predicts an \"up\" move for the next target_horizon candles.\n", "\n", " Realistic Fees: Ensure transaction_cost_pct reflects your actual trading fees. For Alpaca, check their latest fee schedule for crypto.\n", "\n", " This is NOT a substitute for a rigorous backtesting library like backtesting.py, bt, or Zipline for serious strategy evaluation. Those libraries handle many more complexities (order types, portfolio management, more detailed stats, event-driven logic). However, this vectorized backtest is a good first step to see if your model has any potential edge before investing time in a more complex backtester.\n", "\n", "You should now get a printout of the backtest performance and a plot showing your portfolio value over the test period. Pay close attention to the \"Total Return %\" after transaction costs!" ] }, { "cell_type": "code", "execution_count": 9, "id": "dffd7b1f-e01a-4c17-9835-43f1c8afeedb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Preparing data for backtesting...\n", "Data for backtest (based on X_test period) shape: (1112, 25)\n", "Successfully added 'predicted_signal' to backtesting data.\n", "\n", "--- Simplified Backtest Results ---\n", "Initial Capital: $10,000.00\n", "Final Portfolio Value: $9,792.06\n", "Total Return: -2.08%\n", "Number of trades executed: 214\n" ] }, { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAABW0AAAKyCAYAAACuWPzHAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAngBJREFUeJzs3QecI3XZB/D/9QZ39I70l16kSO8dFFFsCNJBeEEEFARBOiIgvVkBUZrwKiJd6tF7b4ICR5Ei5Q6ul7yfZ44s2VzqbvZ27vb7/Xxye5tMJpPJf2aT3zx5plehUCgkAAAAAAByoXd3LwAAAAAAAJ8T2gIAAAAA5IjQFgAAAAAgR4S2AAAAAAA5IrQFAAAAAMgRoS0AAAAAQI4IbQEAAAAAckRoCwAAAACQI0JbAAAAAIAcEdoCAHTQI488ktZZZ500ZMiQ1KtXr/Tkk082fN9LLrkku89rr73Wdt1GG22UXfLq2GOPzZaZ1lp00UXTbrvtlvLmf//3f9Pmm2/e3YtBNzr88MPTmmuu2d2LAQA9ktAWAJjhFAPP4mXgwIHpf/7nf9IBBxyQ3n333ZY+1s9//vN07bXXTnP9xIkT0ze/+c304YcfpjPPPDP98Y9/TIssskjKg/feey/17ds37bzzzlWn+eSTT9KgQYPS17/+9TSjiHV+zjnnpDXWWCPNOuusaZZZZsn+H9fFbXlx1113tRuftS559eqrr6bf/e536ac//WnbdXGAoXTZe/funeaYY4609dZbpwceeKBDjzNmzJjsYECssxndBx98kE477bS0wQYbpLnnnjvNNttsaa211kpXXXVVU/P5/e9/n5Zddtlsv7bUUkulc889t+J0t912W9p4443TXHPNlT3Wl770pWw/VGr8+PHpBz/4QbY8Cy20UDrxxBOnmc+bb76ZbUv33XffNLcddNBB6amnnkrXXXddU88BAOi8vi2YBwBAtzj++OPTYostlsaNG5fuvffedOGFF6Ybb7wxPfvss2nw4MEtC22/8Y1vpO23377d9f/617/S66+/nn7729+mvfbaqyWPdeutt7ZkPvPMM09WIfm3v/0tC8UqrYu//OUv2XqrFezmyejRo9O2226b7r777vTlL385q0yN0PDmm29OP/zhD7Pnc8MNN2RVz90tArfy8OyII47IgrEjjzxymulfeuml7Lnkydlnn51tWxEKlttxxx3TNttskyZPnpz++c9/pgsuuCCbLirPV1xxxaYeJ8bncccdl/0/z1XmjYjgOl7fWDdHHXVUduDk//7v/9J3vvOd9Pzzz7c9z1p+/etfp3333TftsMMO6ZBDDkn33HNPOvDAA7P19JOf/KRtughRY5+09tprt1XA//nPf0677LJL+u9//5sOPvjgbLoIkS+99NJsueJATewzl1hiiew1LDr00EPTdtttl9Zdd91plme++eZLX/3qV9Mvf/nLbBoAYDoqAADMYC6++OJCvI155JFH2l1/yCGHZNdffvnlnZr/lClTCmPGjMn+P2TIkMKuu+46zTR333139lhXX311p57Dq6++WugKf/zjH7P5X3HFFRVv32KLLQrDhg0rjBs3ruF5HnPMMdk8u8M+++yTPfa55547zW3nnXdedtu+++47XZepdJzUs/zyyxc23HDDwoxgwoQJhbnmmqtw1FFHtbs+xmqs59NOO63d9TfddFN2/X777df0Y73//vvZfWNstdLEiRML48ePL0xP//73vwuvvfbaNGNkk002KQwYMKDw6aef1rx/jKU555yzsO2227a7fqeddsr2Qx9++GHbdZtvvnlhgQUWaLf9xnNeYoklCiuttFLbdWuuuWbhuOOOa/s99mXf+c532n6/5557snm/8cYbVZfrmmuuKfTq1avwr3/9q+46AABaJ1+H9AEAOmGTTTZp+2p3mDRpUjrhhBOyyrIBAwZkvUPj697xleFScX1Ub95yyy1p9dVXz9oGRMVbVK9Fhecf/vCHtq+ER4VnXDbccMPsvtEiIa4vrRK844470vrrr59VfcbXlqNS7YUXXqi7/JV62kargz333DPNO++82delV1555Wx56vna176WPf7ll18+zW0xz9tvvz2rII71EtV88Ty+8IUvZL8vvPDCWaXe2LFjaz5G8evy0a6iXFwfFYCl3nrrrbTHHntkzyUeZ/nll08XXXRR3ecSX9+Or4zH6xstMMrtv//+WaVnfJ0/pg0rrLBCxSrRKVOmpAUXXDB77qXXnXXWWdnyxDqO5fv+97+fPvroo4bGSat72hbbf0T1eFRZFr9qH8s0YcKE9PHHH2cVlbPPPnt2OeywwyJJn+Z5NvKcKonHjWrNzTbbrKHlj7FerD4vFcsZX6+P8RSv95JLLplOOeWUbNmK4yeeW4gq1OI2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FtGu49JLL816UdcT86kUmIZY16UHV6LFQYSzcbCjuH+IFhDRwzjapkQrmNIDPXEAoyj69sY+IdpVxHPZdttt0+GHH571gI52GieddFIW6paPh9IDCABA1xPaAgAzrOg1W97/sVyjQUNpIDIzicq76NUZ1Xg//vGPs5/LLbdcW4gTYVtUZEZ485Of/CQts8wyWagcQWdUDBdPGtXMui0P8IrziGWJ/puV1OpjWgypIwQsDZ9KxW0hnltRhLPRQzWqPqNnbvT6jHBtq622ardsEW5edtllFedbHjpOz3ESVbSNXl8asjb7nMpFX+NaJyyLAwnFk1PFidlieYqhX3F7jGWIcRUnuaokDrbUE+OrUnhcPr4aeW2qrcta4XQ10Xu3tMo9xnT5yfgi9Ixg9Be/+EX63ve+19B8559//uy5RaBdGrhHkBtVwQsssEDb79GPNtZt6QGdCGejn3Zs7zFNtard6Esb2+Kqq66a9e2ObwXEdhJinpVC2xgP9XryAgCtJbQFAGZKcRb3CI6i4q8Y+hVPiBVVn+Vnea+mmeqy4jzjZFXlorotQo9Gq2yL84swMp5HaTgT8yp9vFria89xEqGosI0QLb4iHaFM0TPPPJP++c9/ZidsKz2R1T/+8Y+68y5WK8b6LFVeARwhYVTARiBVDPuaEUFUhG4RMFU7GVlUM0ZlcWkgG5WXEexHi4SoTPzLX/6Stt9+++yr5UWxbuLkcXGiuJkluO/sc4rgPgLfqNatVEFa7sgjj0y//e1v01FHHZVVlhaXIVp41Hu9a21fMb5K2z4U1asw72rRCqI01C6GqaXfAIhWC3GgIA6ENKp4QCJOJrbNNtu0XR+/xz6geHsEuJMmTaoYXk+cODGbtlqwff3112cVtrFfDHFSvgiLS59LHLApF21VGqkWBgBaR09bAGCmVAw94iz1pc4444zsZ3wluBERspaHktVE+BHBSgSgpfd59tln06233touiGlETB9nni/tyxphTfTHjL6UG264YUPziVYI0dMyepFGSPbd7353mgrE0orD+P/ZZ59dd77xtfIIosv7k0aFYal4jOg3Gn1tY11Uap9QS7QDiK9tRxB54YUXTnP7r371q+xr+tHrNXp4lopq2/ga+UUXXZT15CxtjVD8SnkEXCeccMI084113ehrnyedfU5rr712NgYee+yxhh4v2o18//vfz9p/RE/n4jI88MAD2XXl4vFjOcLgwYPbrisXwW8coCgdH0899VS67777UleI8RGPN2bMmJrTRW/gCKOLl9Lq7thWoy9sbHPFfU0l8RjxWKV9YjfZZJOs6rV8jMfvsZ6K+6yowo11Hv27S9spREj+97//PQvdK4X1MW30+I5wvVjJG21lXnnllbbXI/oTRwuZUhHe/+tf/8r6XAMA049KWwBgphRVYfG15eihGoFQBJwPP/xwFqhGtWV8lbsREdBEWBgBTFShRfVmVK9Wc9ppp2WVoRF8RYgYJw+KkDUqFqP6rhnRO/TXv/511qYgArRFF100O5FWhFYRRtc6MVep+Cr08ccfn/72t79l1Zcxn6IIeCIci9YJUWEXQWyEq7W+Hl9qr732yr4CHj/jq/ER4EblbrmYJk6AFetu7733zoKuaMkQPTlj/cb/aznzzDOzkOt///d/s2rOYkVthILxvOL1jQrIchEexnOLSwRi5ZWfcb8IHE8++eQscIyTt8XXzKMSMdoqRHhdeoKtGUFnn9N6662XtUiI1yWCxEZEH9UYk/E6X3nllenQQw/NeiRH+4QYv7EdRT/nqOyOMfzaa69lgX+EizEWIuyMlgnxGq2wwgrZZY899si2uzjxW2xL0TYgAvro21p6ArFWibYC0RYgxmlHTkYW+5eoBI91t+mmm07TniJCz8UXX7xt2tgHxYGU4n4h1kUE7fvvv3/WMzie9z333JOdZCyq44snN4yDIDGeI3xda621sseMkD5aJrz55pvZ9JUUD8TEa1V6YCgeLw7kxPLF48e2XCrGQYT4X/3qV5teJwBAJxQAAGYwF198cZSFFh555JGa002cOLFw3HHHFRZbbLFCv379CgsvvHDhiCOOKIwbN67ddIssskhh2223rTiPF198sbDBBhsUBg0alD3mrrvuml1/5513Zr9fffXV09zntttuK6y77rrZfYYOHVr4yle+Unj++ecrPodXX3217boNN9wwu5R69913C7vvvnthrrnmKvTv37+w4oorZvdt1hprrJE93gUXXDDNbbFsm222WWGWWWbJHmfvvfcuPPXUU9n0pY91zDHHZNeVGjNmTGHPPfcsDBs2rDDrrLMWvvWtbxXee++9bLqYvvy57L///tnrEK/HfPPNV9h0000Lv/nNbxp6DuPHjy+ceeaZhdVWW60wZMiQwuDB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N7/5Tds0999/f9pxxx2zwPeJJ55I22+/fXZ59tlnp+MaAQAAAABmdrkObSMo/epXv5q23XbbtOiii6ZvfOMbWbj68MMPt1XZnnXWWemoo47KpltppZXSpZdemt5+++107bXXZtNE2HvzzTen3/3ud2nNNddM6623Xhb6Xnnlldl04bLLLksTJkxIF110UVp++eXTd77znXTggQemM844o21Zzj777LTVVlulQw89NC277LJZxe+qq66aBcoAAAAAAD0itF1nnXXS7bffnv75z39mvz/11FPp3nvvTVtvvXX2+6uvvpreeeedrCVC0bBhw7Jw9oEHHsh+j5/REmH11Vdvmyam7927d1aZW5xmgw02SP3792+bJqp1X3rppfTRRx+1TVP6OMVpio8DAAAAANAKfVOOHX744Vnrgugj26dPn6zH7UknnZS1OwgR2IZ555233f3i9+Jt8XOeeeZpd3vfvn3THHPM0W6a6JtbPo/ibbPPPnv2s9bjVDJ+/PjsUhTPJUycODG7QFFxPBgXVGOMUI8xQj3GCLUYH9RjjFCPMUI9xgj19JQxMrHB55fr0PbPf/5z1rrg8ssvz9oWPPnkk+mggw5KCyywQNp1111T3p188snpuOOOm+b6W2+9NQ0ePLhblol8i77LUIsxQj3GCPUYI9RifFCPMUI9xgj1GCP09DEyZsyYGT+0jf6xUW0bPWbDiiuumF5//fUsDI3Qdr755suuf/fdd9P888/fdr/4fZVVVsn+H9O899577eY7adKk9OGHH7bdP37GfUoVf683TfH2So444oh0yCGHtKu0XXjhhbO+vEOHDu3gWmFmPcoSO6XNN9889evXr7sXhxwyRqjHGKEeY4RajA/qMUaoxxihHmOEenrKGBn12TfxZ+jQNpLn6D1bKtokTJkyJft/tDSI0DT63hZD2nji0at2v/32y35fe+2108cff5wee+yxtNpqq2XX3XHHHdk8ovdtcZojjzwyGxzFQRGDZOmll85aIxSniceJSt+imCaur2bAgAHZpVw8xsw8+Og4Y4N6jBHqMUaoxxihFuODeowR6jFGqMcYoaePkX4NPrdcn4jsK1/5StbD9oYbbkivvfZa+utf/5rOOOOM9LWvfS27vVevXlmIeuKJJ6brrrsuPfPMM2mXXXbJ2idsv/322TTLLrts2mqrrdLee++dHn744XTfffelAw44IKvejenCd7/73ewkZHvuuWd67rnn0lVXXZXOPvvsdlWyP/zhD9PNN9+cTj/99PTiiy+mY489Nj366KPZvAAAAAAAWiXXlbbnnntu+tnPfpb+93//N2txECHr97///XT00Ue3TXPYYYel0aNHp3322SerqF1vvfWycHXgwIFt00Rf3AhXN91006xyd4cddkjnnHNO2+3Dhg3L+szuv//+WTXuXHPNlT1GzLNonXXWyXrrHnXUUemnP/1pWmqppdK1116bVlhhhem4RgAAAACAmV2uQ9tZZ501nXXWWdmlmqi2Pf7447NLNXPMMUcWuNay0korpXvuuafmNN/85jezCwAAAABAV8l1ewQAAAAAgJ5GaAsAAAAAkCNCWwAAAACAHBHaAgAAAADkiNAWAAAAACBHhLYAAAAAADkitAUAAAAAyBGhLQAAAABAjghtAQAAAAByRGgLAAAAAJAjQlsAAAAAgBwR2gIAAAAA5IjQFgAAAAAgR4S2AAAAAAA5IrQFAAAAAMgRoS0AAAAAQI4IbQEAAAAAckRoCwAAAACQI0JbAAAAAIAcEdoCAAAAAOSI0BYAAAAAIEeEtgAAAAAAOSK0BQAAAADIEaEtAAAAAECOCG0BAAAAAHJEaAsAAAAAkCNCWwAAAACAHBHaAgAAAADkiNAWAAAAACBHhLYAAAAAADkitAUAAAAAyBGhLQAAAABAjghtAQAAAAByRGgLAAAAAJAjQlsAAAAAgBwR2gIAAAAA5IjQFgAAAAAgR4S2AAAAAAA5IrQFAAAAAMgRoS0AAAAAQI4IbQEAAAAAckRoCwAAAACQI0JbAAAAAIAcEdoCAAAAAOSI0BYAAAAAIEeEtgAAAAAAOSK0BQAAAADIEaEtAAAAAECOCG0BAAAAAHJEaAsAAAAAkCNCWwAAAACAHBHaAgAAAADkiNAWAAAAACBHhLYAAAAAADkitAUAAAAAyBGhLQAAAABAjghtAQAAAABypG8zE3/88cfpr3/9a7rnnnvS66+/nsaMGZPmnnvu9MUvfjFtueWWaZ111um6JQUAAAAA6AEaqrR9++2301577ZXmn3/+dOKJJ6axY8emVVZZJW266aZpoYUWSnfeeWfafPPN03LLLZeuuuqqrl9qAAAAAICeXGkblbS77rpreuyxx7JgtpIIcq+99tp01llnpTfeeCP9+Mc/bvWyAgAAAADM9BoKbZ9//vk055xz1pxm0KBBaccdd8wuH3zwQauWDwAAAACgR2moPUK9wLaz0wMAAAAA0ERoW8sLL7yQLr744vTkk092dlYAAAAAAD1eQ+0Rio4//visDcKhhx6a/R4nINtqq63SrLPOmkaOHJkuueSStNNOO3XVsgIAAAAAzPSaqrS95ppr2p2I7KSTTkoHHnhg+u9//5vOO++89POf/7wrlhEAAAAAoMdoqNL20ksvTYVCIb322mtZG4Q40Vj8ft9996X1118/u33KlCnp3//+d/b/sMsuu3T1sgMAAAAA9MzQdpFFFsl+9u/fP80777zZ7xHeDh06NG288cZZgDt+/PjUq1evtOiii2a/AwAAAADQRaHthhtumP1cddVV0/XXX59+8pOfpJtvvjlts802aYMNNshue+aZZ9LCCy/c9jsAAAAAAF3c0/a0007LKmzXXXfd9Prrr2cnJiuKk5DFSckAAAAAAOjiStuilVdeOetrGz1t55xzzna3/fjHP87aJQAAAAAAMJ1C26LywDbMP//8nVgMAAAAAAAabo9w5ZVXNry23njjjXTfffdZuwAAAAAAXRXaXnjhhWnZZZdNp556anrhhRemuX3kyJHpxhtvTN/97nezk5VF+wQAAAAAALqoPcLdd9+drrvuunTuueemI444Ig0ZMiTNO++8aeDAgemjjz5K77zzTpprrrnSbrvtlp599tnsNgAAAAAAurCn7XbbbZdd/vvf/6Z77703vf7662ns2LFZWPvFL34xu/Tu3VDhLgAAAAAArToRWYS022+/fbN3AwAAAACgAUpjAQAAAAByRGgLAAAAAJAjQlsAAAAAgBwR2gIAAAAAzAyh7YQJE9JLL72UJk2a1NolAgAAAADowZoObceMGZP23HPPNHjw4LT88sunESNGZNf/4Ac/SL/4xS+6YhkBAAAAAHqMvs3e4YgjjkhPPfVUuuuuu9JWW23Vdv1mm22Wjj322HT44Ye3ehkBAAAAoEtNnjw5TZw4sbsXo8eKdd+3b980bty47LWYUfXr1y/16dNn+oe21157bbrqqqvSWmutlXr16tV2fVTd/utf/+r0AgEAAADA9FIoFNI777yTPv744+5elNTTX4f55psvvfHGG+0yxxnRbLPNlj2XzjyPpkPb999/P80zzzzTXD969OgZfoUCAAAA0LMUA9vIu6IdqHyre0yZMiV9+umnaZZZZkm9e3f4NFzdHjxHa9n33nsv+33++eeffqHt6quvnm644Yash20oDuTf/e53ae211+7wggAAAADA9BRfwy8GtnPOOWd3L07q6aHthAkT0sCBA2fY0DYMGjQo+xnBbYyrjrZKaDq0/fnPf5623nrr9Pzzz6dJkyals88+O/v//fffn+6+++4OLQQAAAAATG/FHrZRYQutUhxPMb46Gto2HVuvt9566cknn8wC2xVXXDHdeuutWWr8wAMPpNVWW61DCwEAAAAA3UVLBPI2npqutA1LLLFE+u1vf9vpBwcAAAAAoJOVtiNGjKh5AQAAAABmLi+++GJaa621sp6zq6yySkP32W233dL222/f9vtGG22UDjrooJSHSthrr702zVSh7aKLLpoWW2yxqhcAAAAAoGtFIBrhY1z69++fllxyyXT88cdnLU07O9/SoLXomGOOSUOGDEkvvfRSuv322zs077/85S/phBNO6NB9J0yYkOaaa670i1/8ouLtMd955523rU/xjK7p9ghPPPFEu99jRcR1Z5xxRjrppJNauWwAAAAAQBVbbbVVuvjii9P48ePTjTfemPbff//Ur1+/dMQRRzQ9r8mTJ9fsxfqvf/0rbbvttmmRRRbp8PLOMcccHb5v//79084775w938MPP7zdbYVCIV1yySVpl112yZ7/zKDpStuVV1653WX11VdPe++9d/rlL3+ZzjnnnK5ZSgAAAACgnQEDBqT55psvC1L322+/tNlmm6Xrrrsuu+2jjz7KQszZZ589DR48OG299dbp5ZdfbrtvhJyzzTZbNv1yyy2XzWuPPfZIf/jDH9Lf/va3tireu+66K/v52GOPZZW88f9jjz02m8czzzyTNtlkkzRo0KA055xzpn322Sd9+umnVZe3vD1C6TLOMsss6Rvf+Ea7ZSy35557pn/+85/p3nvvbXf93Xffnf79739ntz/yyCNp8803z6pyhw0bljbccMP0+OOPp2qKz+/jjz9uu+7JJ5/MrnvttdfarovHXH/99bPnuvDCC6cDDzwwjR49OuUmtK1m6aWXzlYKAAAAAMyoompzzIRJ3XKJx+6MCBSjjUCxzcGjjz6ahbIPPPBANu9tttmmXfuAMWPGpFNOOSX97ne/S88991xWkPmtb30rq+D9z3/+k13WWWed7Ofyyy+ffvSjH2X///GPf5wFlltuuWUWuEYmePXVV6fbbrstHXDAAQ0vb+ky3nfffdl1X/7yl6u2OFhxxRXTGmuskS666KJ210f1bSznMssskz755JO06667ZiHrgw8+mJZaaqnsecf1HRVVxrFOdthhh/T000+nq666Kpt/M8+1y9sjjBo1qt3v8YLHixUJe6wEAAAAAJhRjZ04OS139C3d8tjPH79lGty/6bguy+eiz+wtt9ySfvCDH2TVqsUgNMLMcNlll2UVonECrm9+85vZdRGOXnDBBdm36UuD32i3EBW8RfH/vn37ZtWwxet/+9vfpnHjxqVLL70063UbzjvvvPSVr3wlC4Kjv2wt5cs4ZcqU9Jvf/CatsMIK7ZaxXFTTRmgcAXMsT4Sx11xzTVsHgKj8LRXzjIriqMaNQLgjTj755LTTTju1VQlHBhqPF1W8F154YXZytm6vtI0nGQl68RK9KKKEOhL7WEgAAAAAoOtdf/31WXAZoWG0P/j2t7+dFVa+8MILWci65pprtk0b7Qvim/JxW2mf2JVWWqlDjx3zibC3GNiGddddNwtf42Rljdy/fBkjZyxfxnI77rhj1n/3z3/+c/Z7VL327t07e+7h3XffzVq5RrAa7RGGDh2atWwYMWJE6qinnnoqaycR67p4iSrjeK6vvvpq6gpNR/d33nlnu99jpcw999zZGepiRQMAAADAjGpQvz5ZxWt3PXYzNt5446yIMsLXBRZYoOlsLqpqa518LI+GDh2a9b6NlgjRgzd+RkuHCFJDtEb44IMP0tlnn531+o1evWuvvXZb24hykW2G0tYU5e0ZIvT9/ve/n/WxLfeFL3whdYWmU9Yo+wUAAACAmVGEmB1pUdAdoso1CinLLbvssmnSpEnpoYceamuPEEFmVMDGN+ZriQA4KlnriceI6tPobVusto1WBxGCRrVsI/cvX8YPP/ywoWXcc889s5OaRaXx/fffn0477bS222IZouVD9LENb7zxRvrvf/9bdV5RjBqi/Wt0FSieiKzUqquump5//vmK67qrNDQCi2eda8R2223XmeUBAAAAADohWgN89atfzdoE/PrXv06zzjprOvzww9OCCy6YXV/LoosumvXGjfA0WipEi4F+/fpNM130eD3mmGOyytZoyfD+++9n/XS/973v1e1nW2kZI/g99NBDG1rGDTbYIAtQd9lll+zkY8XQtzjfP/7xj2n11VfPzs0V84yK4mpiPtHrN57DSSedlP75z3+m008/vd00P/nJT9Jaa62VnXhsr732ypY1Qtx//OMfWR/fbgttt99++4aPRDSSxAMAAAAAXSfaBvzwhz/MTr4VrQEi6LzxxhsrBrClIkS96667stAz2gJEq9Soai03ePDgLNyNx1hjjTWy33fYYYd0xhlndHgZI3yN6tl+dZYxMshojfDTn/40HXHEEe1u+/3vf5/22WefrDo2wtif//zn2YnLqonHuuKKK9J+++2X9feN53LiiSe2OxFaXB8nMjvyyCPT+uuvn7VSWGKJJdr66HaFXoXShg10qUj34+jEyJEjs/4bUNorJXacUbpfb8dEz2SMUI8xQj3GCLUYH9RjjFCPMcKMOkbGjRuXnUhqscUWy07mRfeJk3pFdhaZWe/P+szOqGqNq0bzwRl7DQAAAAAAzGQ6FNpGg+E4OvKrX/0qnXPOOe0urfbWW2+lnXfeOeuhEf0nVlxxxfToo4+23R6FwkcffXSaf/75s9s322yz9PLLL7ebRzQxjj4bkV7PNttsWbPiKO8u9fTTT2flzZF+R+n0qaeeOs2yXH311VmfjJgmliPWAQAAAABAKzV9KrwnnngiK2UfM2ZMFt7OMccc2RnYom/FPPPMkw488MCWLdxHH32U1l133bTxxhunm266KTubWwSyxTO5hQhXIyz+wx/+kJUc/+xnP0tbbrll1gy4WH4cgW2cAS6aA0c5/u677571trj88svbypK32GKLLPCNIPqZZ57J+mJEwBvThTgT3Y477phOPvnkrM9G3Dd6/T7++ONphRVWaNlzBgAAAAB6tqYrbQ8++OD0la98JQtUo7L1wQcfTK+//npabbXV0i9/+cuWLtwpp5ySVb1GU+IvfelLWSgb4Wo0+i1W2Z511lnpqKOOys4qF02BL7300vT222+na6+9NpvmhRdeSDfffHP63e9+l9Zcc8203nrrpXPPPTddeeWV2XThsssuy5odX3TRRWn55ZdP3/nOd7LwubRx8tlnn5222mqr7Ixzyy67bDrhhBOyhsZddYY4AAAAAKBnajq0ffLJJ9OPfvSjrCFwnz590vjx49vaCcQZ21rpuuuuy85UF2driyreL37xi+m3v/1t2+3R0Pedd97JKmSLopFvhLMPPPBA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e++9dOONN6bbbrstzcgaXgOnnnpq1rd2/fXXT88880w67bTTspYIf/jDHwS2AAAAAPRI5VW2RV1dbfvSSy+lhx9+OO24447Z73379k3f/va3syC3tJXApptu2vLHfvfdd9Pee++dVdhGe4RoURCVss32ot14442zZYxLVOxGGBxtHl5//fWG5xGVw8svv3yWUYY//elPaZFFFpnh88qGQ9sIaSOxf+qpp9JDDz2U9tlnnxm6xBgAAAAAWtnLtlxX9raNcDa+BR8nHovANi4XXnhh+r//+7+s4jYMGjSo6flGlWtpi4UwceLEdr9Ha4QIWs8+++zsW/nx/2hpMGHChKYeK9obRDuEuETf3XPOOSeruP3tb3/btiyhdHnKl6VYbVvs5xutEXbfffe2Hr0zfWgbZ3A788wz0worrNC1SwQAAAAAM3CVbVdX20ZYe+mll6bTTz+9rVI1LlFsGSHuFVdckU230korZX1sq+nfv3+aPLn9ss8999zZt+tLg9KYd6n77rsvHXjggVkf26hyjROK/fe//+3084qgNYLasWPHti1LKO23W74sIVpERHVuhL7PP/98FirP6BoObfv169e1SwIAAAAAM0mVbVdW20Yv2I8++ijtueeeWYFl6WWHHXZoa5FwzDHHZAFu/HzhhReylqennHJK23wWXXTRNHz48PTWW2+1ha7RD/f999/PWqX+61//Sueff3666aab2j1+tEX44x//mM0zvpG/0047daiqd/z48VlAHJeY12GHHZa1WfjKV76S3R4VuAsvvHA69thj08svv5ydWC2C6nKzzz57+vrXv54OPfTQtMUWW2QnWJvRNd7VFwAAAABoqMq2K6ttI5TdbLPNsn6y5SK0ffTRR9PTTz+dBbBXX311uu6669Iqq6yS9YyNPrhFxx9/fHrttdfSEkss0VbVGue0uuCCC7KwNk5aFtP/+Mc/nubxIzReddVV0/e+972s6naeeeZp+nncfPPN2cnD4rL22munJ554Il111VVtJ1KLItIInV988cWsajgC5xNPPLHivCLAjvYMe+yxR5oZ9O3uBQAAAACAGbXKtl5oW1ptO3z34S157L///e81T8xV2togKlDjUslaa62VtVQot++++2aXUj/96U/b/v/FL34xPfLII+1u/8Y3vtHu9/K+uOWiB22xD22YMmVKGjVqVHZSs1LrrrtuFkDXm3dUC0df3a9+9atpZqDSFgAAAAC6oMq2q3vbktKYMWOyNg6/+MUv0ve///2sT2+PDW2jQXGciS7KkePy17/+dZqmxQAAAAAws1bZ9kq9mrpPTN/q3rakrPfuMsssk+abb750xBFHpJlF0+0RXnnllbTtttumN998My299NLZdSeffHLWFDiaAUcPDAAAAACYGY2eMDo99OZDqZBqf/2/XEz/4JsPpjETx6TB/QZ32fL1NMcee2x2mdk0HdpGY+HFF188PfDAA2mOOebIrvvggw/SzjvvnN0WwS0AAAAAzIyG9B+S3jrkrTRy/Mim7ztswDCBLV0T2t59993pwQcfbAtsQzT5jb4R0RgYAAAAAGZmcw+ZO7tAbnraDhgwIH3yySfTXP/pp5/ONI1+AQAAAABmmND2y1/+ctpnn33SQw89lAqFQnaJytt99903bbfddl2zlAAAAAAAPUTToe0555yTnWxs7bXXTgMHDswu0RZhySWXTGeffXbXLCUAAAAAQA/RdE/b2WabLf3tb39LL7/8cnrxxRez65ZddtkstAUAAACAmd7776c0svkTkaVhw1Kau2f0wu3Vq1f661//mrbffvsue4xjjz02XXvttenJJ5/Mft9tt93Sxx9/nF3X40LboqWWWiq7AAAAAECPMXp0SgsumNLEic3ft1+/lD7+OKXBg1u2OA888EBab7310lZbbZVuuOGGpu676KKLpoMOOii7TG/vv/9+Ovroo7Nlfvfdd9Pss8+ell9++SyIXX/99VNP11Boe8ghh6QTTjghDRkyJPt/LWeccUarlg0AAAAA8mXIkJTWXDOl++5LqVBo/H69eqW01lotDWzD73//+/SDH/wg+/n222+nBRZYIM0IdthhhzRhwoT0hz/8IS2++OLpP//5T7rxxhvTBx980N2LNuP0tH3iiSfSxM+OHsT/q12KpcgAAAAAMNM64YTmAtsQ0x9/fEsX49NPP01XXXVV2m+//dK2226bLrnkkmmm+fvf/57WWGON7LxUc801V/ra176WXb/RRhul119/PR188MFZK4O4hKh0XWWVVdrN46yzzsqqcoseeeSRtPnmm2fzGzZsWNpwww3T448/3vByRwuDe+65J51yyilp4403Tossskj60pe+lBWLbrfddu2m22uvvdLcc8+dhg4dmjbZZJP01FNPpZ6godD2zjvvzHrZFv9f7XLHHXd09fICAAAAQPfaaKOU1lsvpT59Gps+pouv/Mf9WujPf/5zWmaZZdLSSy+ddt5553TRRRelQkmYHK0HIqTdZpttsoLL22+/PQtHw1/+8pe00EILpeOPPz6rco1Loz755JO06667pnvvvTc9+OCDWQvVeIy4vhGzzDJLdones+PHj6863Te/+c303nvvpZtuuik99thjadVVV02bbrpp+vDDD9PMrsM9bQEAAACgR1fbbrxxY9NOntzyKtsQLREirA3R03bkyJHp7rvvzqpow0knnZS+853vpOOOO67tPiuvvHL2c4455kh9+vRJs846a5pvvvmaetyoeC31m9/8Jiv4jMf+8pe/XPf+ffv2zaqC99577/SrX/0qC2M32GCDrFp4nXXWyaaJQPjhhx/OQtsBAwZk1/3yl7/Mgt5rrrkm7bPPPin19ND261//esMzjJQeAAAAAHpEte0DD0wNZWtV2UYQ2eIq25deeikLNf/617+2BaHf/va3syC3GNpGK9MIRlstThx21FFHpbvuuisLVSdPnpzGjBmTRowY0VRP2whpo01CVOtGNe1pp52WBcB77LFH1gYh2j/MOeec7e43duzY9K9//SvN7BoKbaM3BQAAAADQZLVtF1bZTpo0qd2Jx6I1QlSlnnfeeVmeN2jQoKbn27t373YtFkLxXFdF0RohThh29tlnZ/1o4zHXXnvt7MRizYg+u9EbNy5HHnlk2m233bKq4D322CMLbOeff/4sGC5XbOOaenpoe/HFF3f9kgAAAADAzFRt20VVthHWXnrppen0009PW2yxRbvbtt9++3TFFVekfffdN6200kpZH9vdd9+94nz69++fVcmWipN+vfPOO1lwWzw5WVTslrrvvvvSBRdckPWxDW+88Ub673//2+nnFb15b7zxxuz/0TIhliMqiEtPgtZTNHQiskref//9rLdEXOL/AAAAANAjq22rtUfooirb66+/Pn300Udpzz33TCussEK7S7QdiCrccMwxx2QBbvx84YUX0jPPPJNOOeWUtvlEGDp8+PD01ltvtYWu0Vohsr5TTz01a0Nw/vnnZ60LSsWJx/74xz9m83zooYfSTjvt1FRVb1TpRl/cP/3pT+npp59Or776arr66qvTOeeck7bbbrtsms022yyr3o0Q+tZbb02vvfZauv/++7OK3EcffTTN7JoObUePHp2VKEd5cjQIjkuUYccgid4VAAAAANDjqm2jqrZU/L7++i2vsg0RykaoWamlaYS2EWpGGBoBbISh1113XVpllVWyoDT64BYdf/zxWRi6xBJLZBW2Ydlll82qaCOsjZOWxfQ//vGPp3n8CI2jGvZ73/teOvDAA9M888zT8PLPMsssac0110xnnnlmli1G2BzB8i677JLOPffcbJqo8o2q27g9KoX/53/+Jzup2uuvv57mnXfeNLPrVShvUlHH97///XTbbbdlvTHWXXfd7Lqoto0XJ/pPXHjhhV21rDO8UaNGZRtTnMlv6NCh3b045Ej0hokdUXytoF+/ft29OOSQMUI9xgj1GCPUYnxQjzFCPcYIM+oYGTduXFbludhii2X9VTss+q5W6m17551dEtrOjKZMmZJlZ5GZ9e7d4eYAuR9XjeaDDfW0LfV///d/6Zprrmk7C12IDS5KoL/1rW8JbQEAAADo2b1tu6iXLT1H07F1tECoVIIcJdDaIwAAAACQenpv2y7qZUvP0XRoGw2Ao8dElPkWjR07Nh133HHZbQAAAADQY6ttQxf1sqXnaLo9wllnnZW22mqrtNBCC2XNiMNTTz2V9We45ZZbumIZAQAAACD/Tjoppa23nvoTpmdou+KKK6aXX345XXbZZenFF1/Mrttxxx3TTjvtlPW1BQAAAIAeaYMNUvrvf1OSkTE9QttVV1013X777Wn22WdPxx9/fPrxj3+c9t57784+NgAAAAB0u0Kh0LqZCWx7vEILxlNDPW1feOGFNHr06Oz/0bv2008/7fQDAwAAAEB36tevX/ZzzJgx3b0ozETGfDaeiuOryyptV1lllbT77run9dZbL0uKf/nLX6ZZZpml4rRHH310hxcGAAAAAKaXPn36pNlmmy2999572e+DBw9OvXr16u7F6pGmTJmSJkyYkMaNG5d6926ozjR3IjeNwDbGU4yrGF9dGtpecskl6ZhjjknXX399NnBvuumm1LfvtHeN24S2AAAAAMwo5ptvvuxnMbil+wLPsWPHZufM6jWDB+cR2BbHVZeGtksvvXS68sors/9H0h39beeZZ55OPTAAAAAAdLcICOeff/4s65o4cWJ3L06PFet++PDhaYMNNuhUW4HuFsvemQrbpkLbUnfeeWeaY445prl+0qRJ6f77789WLAAAAADMSCJoa0XYRsfEuo98ceDAgTN0aNsqTTeI2GSTTdKHH344zfUjR45MG2+8cauWCwAAAACgR+rdkf4SlfpKfPDBB2nIkCGtWi4AAAAAgB6p4fYIX//617OfEdjutttuacCAAW23TZ48OT399NNpnXXW6ZqlBAAAAADoIRoObYcNG9ZWaTvrrLNmZ3Ir6t+/f1prrbXS3nvv3TVLCQAAAADQQzQc2l588cVZYBvOPffcNMsss3TlcgEAAAAA9EhN9bSN0Payyy5L//nPf7puiQAAAAAAerCmQtvevXunpZZaKjvpGAAAAAAA3Rzahl/84hfp0EMPTc8++2wXLA4AAAAAQM/WcE/bol122SWNGTMmrbzyytkJyEpPSBY+/PDDVi4fAAAAAECP0nRoe9ZZZ3XNkgAAAAAA0Hxou+uuu3bNkgAAAAAA0HxoGyZPnpyuvfba9MILL2S/L7/88mm77bZLffr0afXyAQAAAAD0KE2Htq+88kraZptt0ltvvZWWXnrp7LqTTz45LbzwwumGG25ISyyxRFcsJwAAAABAj9C72TsceOCBWTD7xhtvpMcffzy7jBgxIi222GLZbQAAAAAATMdK27vvvjs9+OCDaY455mi7bs4550y/+MUv0rrrrtuJRQEAAAAAoOlK2wEDBqRPPvlkmus//fTT1L9//1YtFwAAAABAj9R0aPvlL3857bPPPumhhx5KhUIhu0Tl7b777pudjAwAAAAAgOkY2p5zzjlZT9u11147DRw4MLtEW4Qll1wynX322Z1YFAAAAAAAmu5pO9tss6W//e1v6ZVXXkkvvPBCdt2yyy6bhbYAAAAAAEyn0HbKlCnptNNOS9ddd12aMGFC2nTTTdMxxxyTBg0a1MlFAAAAAACg6fYIJ510UvrpT3+aZplllrTgggtmrRD233//Ru8OAAAAAEArQ9tLL700XXDBBemWW25J1157bfr73/+eLrvssqwCFwAAAACA6RzajhgxIm2zzTZtv2+22WapV69e6e23327RogAAAAAA0HBoO2nSpDRw4MB21/Xr1y9NnDixK5YLAAAAAKBHavhEZIVCIe22225pwIABbdeNGzcu7bvvvmnIkCFt1/3lL39p/VICAAAAAPQQDYe2u+666zTX7bzzzq1eHgAAAACAHq3h0Pbiiy/u2iUBAAAAAKDxnrYAAAAAAHQ9oS0AAAAAQI4IbQEAAAAAckRoCwAAAACQI0JbAAAAAIAcEdoCAAAAAOSI0BYAAAAAIEeEtgAAAAAAOSK0BQAAAADIEaEtAAAAAECOCG0BAAAAAHJEaAsAAAAAkCNCWwAAAACAHBHaAgAAAADkiNAWAAAAACBHhLYAAAAAADkitAUAAAAAyBGhLQAAAABAjghtAQAAAAByRGgLAAAAAJAjQlsAAAAAgBwR2gIAAAAA5IjQFgAAAAAgR4S2AAAAAAA5IrQFAAAAAMgRoS0AAAAAQI4IbQEAAAAAckRoCwAAAACQI0JbAAAAAIAcEdoCAAAAAOSI0BYAAAAAIEeEtgAAAAAAOSK0BQAAAADIEaEtAAAAAECOCG0BAAAAAHJEaAsAAAAAkCMzVGj7i1/8IvXq1SsddNBBbdeNGzcu7b///mnOOedMs8wyS9phhx3Su+++2+5+I0aMSNtuu20aPHhwmmeeedKhhx6aJk2a1G6au+66K6266qppwIABackll0yXXHLJNI9//vnnp0UXXTQNHDgwrbnmmunhhx/uwmcLAAAAAPREM0xo+8gjj6Rf//rXaaWVVmp3/cEHH5z+/ve/p6uvvjrdfffd6e23305f//rX226fPHlyFthOmDAh3X///ekPf/hDFsgeffTRbdO8+uqr2TQbb7xxevLJJ7NQeK+99kq33HJL2zRXXXVVOuSQQ9IxxxyTHn/88bTyyiunLbfcMr333nvTaQ0AAAAAAD3BDBHafvrpp2mnnXZKv/3tb9Pss8/edv3IkSPT73//+3TGGWekTTbZJK222mrp4osvzsLZBx98MJvm1ltvTc8//3z605/+lFZZZZW09dZbpxNOOCGrmo0gN/zqV79Kiy22WDr99NPTsssumw444ID0jW98I5155pltjxWPsffee6fdd989Lbfcctl9onL3oosu6oY1AgAAAADMrPqmGUC0P4hK2M022yydeOKJbdc/9thjaeLEidn1Rcsss0z6whe+kB544IG01lprZT9XXHHFNO+887ZNExWy++23X3ruuefSF7/4xWya0nkUpym2YYhwNx7riCOOaLu9d+/e2X3ivtWMHz8+uxSNGjUq+xnLHBcoKo4H44JqjBHqMUaoxxihFuODeowR6jFGqMcYoZ6eMkYmNvj8ch/aXnnllVk7gmiPUO6dd95J/fv3T7PNNlu76yOgjduK05QGtsXbi7fVmiZC1rFjx6aPPvooa7NQaZoXX3yx6rKffPLJ6bjjjpvm+qj+jSpdKPePf/yjuxeBnDNGqMcYoR5jhFqMD+oxRqjHGKEeY4SePkbGjBkz44e2b7zxRvrhD3+YvVhx8q8ZTVTmRh/cogiBF1544bTFFlukoUOHduuykb+jLDHON99889SvX7/uXhxyyBihHmOEeowRajE+qMcYoR5jhHqMEerpKWNk1GffxJ+hQ9toSRAn+lp11VXbrouK1+HDh6fzzjsvO1FYtC74+OOP21Xbvvvuu2m++ebL/h8/H3744XbzjduLtxV/Fq8rnSaC1UGDBqU+ffpkl0rTFOdRyYABA7JLuRh4M/Pgo+OMDeoxRqjHGKEeY4RajA/qMUaoxxihHmOEnj5G+jX43HJ9IrJNN900PfPMM+nJJ59su6y++urZScmK/48nevvtt7fd56WXXkojRoxIa6+9dvZ7/Ix5RPhbFKl9BLJxQrHiNKXzKE5TnEe0YIiTnJVOM2XKlOz34jQAAAAAAK2Q60rbWWedNa2wwgrtrhsyZEiac845267fc889sxYEc8wxRxbE/uAHP8iC1DgJWYhWBBHOfu9730unnnpq1r/2qKOOyk5uVqyC3XfffbPK3cMOOyztscce6Y477kh//vOf0w033ND2uPEYu+66axYUf+lLX0pnnXVWGj16dNp9992n6zoBAAAAAGZuuQ5tG3HmmWem3r17px122CGNHz8+bbnllumCCy5ouz3aGlx//fVpv/32y8LcCH0jfD3++OPbpllsscWygPbggw9OZ599dlpooYXS7373u2xeRd/+9rfT+++/n44++ugs+F1llVXSzTffPM3JyQAAAAAAelRoe9ddd7X7PU5Qdv7552eXahZZZJF044031pzvRhttlJ544oma0xxwwAHZBQAAAACgq+S6py0AAAAAQE8jtAUAAAAAyBGhLQAAAABAjghtAQAAAAByRGgLAAAAAJAjQlsAAAAAgBwR2gIAAAAA5IjQFgAAAAAgR4S2AAAAAAA5IrQFAAAAAMgRoS0AAAAAQI4IbQEAAAAAckRoCwAAAACQI0JbAAAAAIAcEdoCAAAAAOSI0BYAAAAAIEeEtgAAAAAAOSK0BQAAAADIEaEtAAAAAECOCG0BAAAAAHJEaAsAAAAAkCNCWwAAAACAHBHaAgAAAADkiNAWAAAAACBHhLYAAAAAADkitAUAAAAAyBGhLQAAAABAjghtAQAAAAByRGgLAAAAAJAjQlsAAAAAgBwR2gIAAAAA5IjQFgAAAAAgR4S2AAAAAAA5IrQFAAAAAMgRoS0AAAAAQI4IbQEAAAAAckRoCwAAAACQI0JbAAAAAIAcEdoCAAAAAOSI0BYAAAAAIEeEtgAAAAAAOSK0BQAAAADIEaEtAAAAAECOCG0BAAAAAHJEaAsAAAAAkCNCWwAAAACAHBHaAgAAAADkiNAWAAAAACBHhLYAAAAAADkitAUAAAAAyBGhLQAAAABAjghtAQAAAAByRGgLAAAAAJAjQlsAAAAAgBwR2gIAAAAA5IjQFgAAAAAgR4S2AAAAAAA5IrQFAAAAAMgRoS0AAAAAQI4IbQEAAAAAckRoCwAAAACQI0JbAAAAAIAcEdoCAAAAAOSI0BYAAAAAIEeEtgAAAAAAOSK0BQAAAADIEaEtAAAAAECOCG0BAAAAAHJEaAsAAAAAkCNCWwAAAACAHBHaAgAAAADkiNAWAAAAACBHhLYAAAAAADkitAUAAAAAyBGhLQAAAABAjghtAQAAAAByRGgLAAAAAJAjQlsAAAAAgBwR2gIAAAAA5IjQFgAAAAAgR4S2AAAAAAA5IrQFAAAAAMgRoS0AAAAAQI4IbQEAAAAAckRoCwAAAACQI0JbAAAAAIAcEdoCAAAAAOSI0BYAAAAAIEeEtgAAAAAAOSK0BQAAAADIEaEtAAAAAECOCG0BAAAAAHJEaAsAAAAAkCNCWwAAAACAHBHaAgAAAADkiNAWAAAAACBHhLYAAAAAADkitAUAAAAAyBGhLQAAAABAjghtAQAAAAByRGgLAAAAAJAjQlsAAAAAgBzJdWh78sknpzXWWCPNOuusaZ555knbb799eumll9pNM27cuLT//vunOeecM80yyyxphx12SO+++267aUaMGJG23XbbNHjw4Gw+hx56aJo0aVK7ae6666606qqrpgEDBqQll1wyXXLJJdMsz/nnn58WXXTRNHDgwLTmmmumhx9+uIueOQAAAADQU+U6tL377ruzQPbBBx9M//jHP9LEiRPTFltskUaPHt02zcEHH5z+/ve/p6uvvjqb/u23305f//rX226fPHlyFthOmDAh3X///ekPf/hDFsgeffTRbdO8+uqr2TQbb7xxevLJJ9NBBx2U9tprr3TLLbe0TXPVVVelQw45JB1zzDHp8ccfTyuvvHLacsst03vvvTcd1wgAAAAAMLPrm3Ls5ptvbvd7hK1RKfvYY4+lDTbYII0cOTL9/ve/T5dffnnaZJNNsmkuvvjitOyyy2ZB71prrZVuvfXW9Pzzz6fbbrstzTvvvGmVVVZJJ5xwQvrJT36Sjj322NS/f//0q1/9Ki222GLp9NNPz+YR97/33nvTmWeemQWz4Ywzzkh777132n333bPf4z433HBDuuiii9Lhhx8+3dcNAAAAADBzynVoWy5C2jDHHHNkPyO8jerbzTbbrG2aZZZZJn3hC19IDzzwQBbaxs8VV1wxC2yLIojdb7/90nPPPZe++MUvZtOUzqM4TVTchqjSjcc64ogj2m7v3bt3dp+4bzXjx4/PLkWjRo3KfsYyxwWKiuPBuKAaY4R6jBHqMUaoxfigHmOEeowR6jFGqKenjJGJDT6/GSa0nTJlShairrvuummFFVbIrnvnnXeyStnZZput3bQR0MZtxWlKA9vi7cXbak0TIevYsWPTRx99lLVZqDTNiy++WLMn73HHHTfN9VH9G/11oVy0AYFajBHqMUaoxxihFuODeowR6jFGqMcYoaePkTFjxsxcoW30tn322WeztgUziqjMjT64RRECL7zwwllf3qFDh3brspG/oyyxU9p8881Tv379untxyCFjhHqMEeoxRqjF+KAeY4R6jBHqMUaop6eMkVGffRN/pghtDzjggHT99den4cOHp4UWWqjt+vnmmy9rXfDxxx+3q7Z99913s9uK0zz88MPt5he3F28r/ixeVzpNBKuDBg1Kffr0yS6VpinOo5IBAwZkl3Ix8GbmwUfHGRvUY4xQjzFCPcYItRgf1GOMUI8xQj3GCD19jPRr8Ln1TjlWKBSywPavf/1ruuOOO7KThZVabbXVsid6++23t1330ksvpREjRqS11147+z1+PvPMM+m9995rmyZS+whkl1tuubZpSudRnKY4j2jBEI9VOk20a4jfi9MAAAAAALRC37y3RLj88svT3/72tzTrrLO29aAdNmxYVgEbP/fcc8+sBUGcnCyC2B/84AdZkBonIQvRiiDC2e9973vp1FNPzeZx1FFHZfMuVsHuu+++6bzzzkuHHXZY2mOPPbKA+M9//nO64YYb2pYlHmPXXXdNq6++evrSl76U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c88V9t5778Jss81WePfdd9umuf/++wtDhw4tnHzyyYVnn3228OKLLxauuuqqwrhx46rOd9KkSYUVVlihsNlmmxWeeOKJwo033liYa665CkcccUTbNI8//njh8ssvz+b56quvFv74xz8WBg8eXPj1r39ddb6ffvppYd999y385je/KWy55ZaFr371qxWn22677Qprrrlm4R//+Ec273gO9957b4fXU082o42Rk046qXDUUUcV7rvvvsIrr7xSOOusswq9e/cu/P3vf2+bJm7r06dP4dRTTy08//zz2fT9+vUrPPPMMy1ZZz1Nd46Roo8++qiw+OKLF7bYYovCyiuvXHN5R44cWZh33nkLO+20U7YsV1xxRWHQoEHtxpUx0loz4xgpin3TggsuWFh//fWr/k2i542Pyy67rDBgwIDsZ4yRW265pTD//PMXDj744A6vp55sRhsjjbxf/eEPf1g45ZRTCg8//HDhn//8Z/aY8Xcm3vvQvJlxjASfaXruGPGZpmeNkTvvvLMQkeVLL71U+M9//tN2mTx5co/6TCO0hTJf+tKXCvvvv3/b77FTWGCBBbKdUFG8UYiNuxmxI4o/Ku+8807bdRdeeGG2gxs/fnzV+33ta18r7Lzzzg09xq677lrxDc5NN91UGDZsWOGDDz5oapmZ+cZI0TbbbFPYfffd237/1re+Vdh2223bTRPP4fvf/35T8yU/Y+Tb3/52Nv9jjjmm7pvgCy64oDD77LO3m8dPfvKTwtJLL932uzHSWjPjGCm+CV9nnXUKv/vd76r+TaJnjo94Pptsskm7+x1yyCGFddddt6nnwIw5Rko1s29YbrnlCscdd1wTz4CZeYz4TNNaM/IYKfKZZuYdI8XQ9qOPPmp4vjPjZxrtEaDEhAkT0mOPPZY222yztut69+6d/R4l/+G9995LDz30UFbCv84666R55503bbjhhtnX1muJ+0fpfUxftOWWW6ZRo0al5557ruJ9nnjiiXT//fdn8++M6667Lq2++urZVwMWXHDB9D//8z/pxz/+cd2vLDDzjpGRI0emOeaYo91jlz6n4mMXnxMz1hi5+OKL07///e90zDHHNLTMMd/4GlP//v3bzfell/6/vTuPjaJ8Azj+UChHRS5LQArlLggIKSYlYI2gYDgs4Q+QKzQ0lBYKCSrGIIakaARrsJqiBIJcCUdTQYGE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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "Sample of backtest results (last 5 rows):\n", " close predicted_signal portfolio_value\n", "timestamp \n", "2025-06-04 23:54:00+00:00 104807.747 0 9792.056818\n", "2025-06-04 23:55:00+00:00 104816.255 0 9792.056818\n", "2025-06-04 23:57:00+00:00 104744.600 0 9792.056818\n", "2025-06-04 23:58:00+00:00 104732.050 0 9792.056818\n", "2025-06-04 23:59:00+00:00 104748.560 0 9792.056818\n", "\n", "Last 5 trades:\n", "{'timestamp': Timestamp('2025-06-04 23:07:00+0000', tz='UTC'), 'type': 'SELL', 'price': np.float64(104919.53), 'quantity_btc': np.float64(0.009546703427839334), 'proceeds_usd': np.float64(1000.6340010615935), 'fee_usd': np.float64(1.001635636698292)}\n", "{'timestamp': Timestamp('2025-06-04 23:09:00+0000', tz='UTC'), 'type': 'BUY', 'price': np.float64(104846.337), 'quantity_btc': np.float64(0.009537767637986246), 'cost_usd': np.float64(1001.0), 'fee_usd': np.float64(1.0)}\n", "{'timestamp': Timestamp('2025-06-04 23:18:00+0000', tz='UTC'), 'type': 'SELL', 'price': np.float64(104769.275), 'quantity_btc': np.float64(0.009537767637986246), 'proceeds_usd': np.float64(998.2657355497312), 'fee_usd': np.float64(0.9992650005502814)}\n", "{'timestamp': Timestamp('2025-06-04 23:20:00+0000', tz='UTC'), 'type': 'BUY', 'price': np.float64(104687.1415), 'quantity_btc': np.float64(0.009552271517510104), 'cost_usd': np.float64(1000.9999999999999), 'fee_usd': np.float64(0.9999999999999999)}\n", "{'timestamp': Timestamp('2025-06-04 23:37:00+0000', tz='UTC'), 'type': 'SELL', 'price': np.float64(104760.2965), 'quantity_btc': np.float64(0.009552271517510104), 'proceeds_usd': np.float64(999.6980976264405), 'fee_usd': np.float64(1.0006987964228633)}\n" ] } ], "source": [ "import matplotlib.pyplot as plt # Optional, for plotting\n", "\n", "# --- Step 5: Simplified Backtesting ---\n", "\n", "def run_simple_backtest(df_with_predictions_input, initial_capital=10000, trade_size_usd=1000, transaction_cost_pct=0.001): \n", " # Alpaca crypto fee for taker is 0.1% to 0.3%\n", " \"\"\"\n", " A very simplified vectorized backtest.\n", " Assumes df_with_predictions has a 'predicted_signal' column (1 for buy, 0 for hold/nothing).\n", " For this example, our model's prediction (0 or 1) is the signal.\n", " 1 = Go Long (Buy), 0 = Exit Long (Sell if in position) or Do Nothing\n", " \"\"\"\n", " df_with_predictions = df_with_predictions_input.copy()\n", "\n", " if df_with_predictions.empty or 'predicted_signal' not in df_with_predictions.columns or 'close' not in df_with_predictions.columns:\n", " print(\"DataFrame for backtest is empty or 'predicted_signal'/'close' column missing.\")\n", " if 'predicted_signal' not in df_with_predictions.columns:\n", " print(\"'predicted_signal' column is missing.\")\n", " if 'close' not in df_with_predictions.columns:\n", " print(\"'close' column is missing.\")\n", " return pd.DataFrame() # Return empty dataframe on failure\n", "\n", " capital = initial_capital\n", " position_btc = 0 # Amount of BTC held\n", " portfolio_value_over_time = [] # Use a more descriptive name\n", " trades_log = [] # To log trades\n", "\n", " # Ensure the DataFrame index is a DatetimeIndex for proper time-series operations and logging\n", " if not isinstance(df_with_predictions.index, pd.DatetimeIndex):\n", " print(\"Warning: Index is not a DatetimeIndex. Converting...\")\n", " try:\n", " df_with_predictions.index = pd.to_datetime(df_with_predictions.index)\n", " except Exception as e:\n", " print(f\"Could not convert index to DatetimeIndex: {e}. Backtest might behave unexpectedly.\")\n", " return pd.DataFrame()\n", "\n", "\n", " for i in range(len(df_with_predictions)):\n", " current_timestamp = df_with_predictions.index[i]\n", " current_price = df_with_predictions['close'].iloc[i]\n", " signal = df_with_predictions['predicted_signal'].iloc[i]\n", "\n", " # Decision logic\n", " # Signal 1: Model predicts price will go UP\n", " if signal == 1 and position_btc == 0: # Buy signal and no current position\n", " # Buy BTC\n", " amount_to_buy_btc = trade_size_usd / current_price\n", " cost_of_purchase = amount_to_buy_btc * current_price # Cost before fees\n", " fee = cost_of_purchase * transaction_cost_pct\n", " total_cost = cost_of_purchase + fee\n", "\n", " if capital >= total_cost:\n", " capital -= total_cost\n", " position_btc += amount_to_buy_btc\n", " trades_log.append({\n", " 'timestamp': current_timestamp, 'type': 'BUY',\n", " 'price': current_price, 'quantity_btc': amount_to_buy_btc,\n", " 'cost_usd': total_cost, 'fee_usd': fee\n", " })\n", " # print(f\"{current_timestamp}: BUY {amount_to_buy_btc:.6f} BTC @ {current_price:.2f}, Cost: {total_cost:.2f}\")\n", "\n", " # Signal 0: Model predicts price will NOT go up (or go down/stay neutral)\n", " elif signal == 0 and position_btc > 0: # Sell signal (or neutral) and have a position\n", " # Sell all BTC\n", " value_of_sale = position_btc * current_price # Value before fees\n", " fee = value_of_sale * transaction_cost_pct\n", " proceeds = value_of_sale - fee\n", "\n", " capital += proceeds\n", " trades_log.append({\n", " 'timestamp': current_timestamp, 'type': 'SELL',\n", " 'price': current_price, 'quantity_btc': position_btc,\n", " 'proceeds_usd': proceeds, 'fee_usd': fee\n", " })\n", " # print(f\"{current_timestamp}: SELL {position_btc:.6f} BTC @ {current_price:.2f}, Proceeds: {proceeds:.2f}\")\n", " position_btc = 0\n", " \n", " # For signals where no action is taken (e.g., signal=1 but already in position, or signal=0 and no position)\n", " # we just calculate the current portfolio value.\n", "\n", " current_portfolio_value = capital + (position_btc * current_price)\n", " portfolio_value_over_time.append(current_portfolio_value)\n", "\n", " df_with_predictions['portfolio_value'] = portfolio_value_over_time\n", " \n", " print(\"\\n--- Simplified Backtest Results ---\")\n", " print(f\"Initial Capital: ${initial_capital:,.2f}\")\n", " if not df_with_predictions.empty:\n", " final_portfolio_value = df_with_predictions['portfolio_value'].iloc[-1]\n", " print(f\"Final Portfolio Value: ${final_portfolio_value:,.2f}\")\n", " total_return_pct = (final_portfolio_value / initial_capital - 1) * 100\n", " print(f\"Total Return: {total_return_pct:.2f}%\")\n", " print(f\"Number of trades executed: {len(trades_log)}\")\n", " else:\n", " print(\"Could not calculate final portfolio value as DataFrame is empty.\")\n", " total_return_pct = 0\n", "\n", " # Plotting\n", " if not df_with_predictions.empty and 'portfolio_value' in df_with_predictions.columns:\n", " plt.figure(figsize=(14, 7))\n", " plt.plot(df_with_predictions.index, df_with_predictions['portfolio_value'], label='Portfolio Value')\n", " \n", " # Optional: Plot buy/sell markers\n", " buy_signals = df_with_predictions[df_with_predictions['predicted_signal'] == 1].copy()\n", " sell_signals = df_with_predictions[df_with_predictions['predicted_signal'] == 0].copy() # This is more like 'exit'\n", " \n", " # Filter to actual trades from log for plotting markers\n", " actual_buy_times = [trade['timestamp'] for trade in trades_log if trade['type'] == 'BUY']\n", " actual_sell_times = [trade['timestamp'] for trade in trades_log if trade['type'] == 'SELL']\n", "\n", " if actual_buy_times:\n", " buy_prices = [df_with_predictions.loc[t, 'close'] for t in actual_buy_times if t in df_with_predictions.index]\n", " plt.scatter([t for t in actual_buy_times if t in df_with_predictions.index], buy_prices, marker='^', color='green', \n", " label='Actual Buy', alpha=1, s=100)\n", " if actual_sell_times:\n", " sell_prices = [df_with_predictions.loc[t, 'close'] for t in actual_sell_times if t in df_with_predictions.index]\n", " plt.scatter([t for t in actual_sell_times if t in df_with_predictions.index], sell_prices, marker='v', color='red', \n", " label='Actual Sell', alpha=1, s=100)\n", " \n", " plt.title(f'Portfolio Value Over Time (Return: {total_return_pct:.2f}%)')\n", " plt.xlabel('Date')\n", " plt.ylabel('Portfolio Value ($)')\n", " plt.legend()\n", " plt.grid(True)\n", " plt.tight_layout()\n", " plt.show()\n", " \n", " return df_with_predictions, trades_log\n", "\n", "\n", "# --- Code to call the backtest (after Step 4 Model Training) ---\n", "# This assumes 'model', 'X_test', and 'featured_data' are defined and populated from previous steps.\n", "\n", "backtest_results_df = pd.DataFrame() # Initialize\n", "trades_log_list = []\n", "\n", "if 'model' in locals() and model is not None and \\\n", " 'X_test' in locals() and not X_test.empty and \\\n", " 'featured_data' in locals() and not featured_data.empty:\n", "\n", " print(\"\\nPreparing data for backtesting...\")\n", " # Get the original rows from 'featured_data' that correspond to the 'X_test' indices.\n", " # This ensures we have 'close' prices and other original columns for the backtest period.\n", " # It's important that X_test.index directly maps to featured_data.index for these rows.\n", " \n", " # Verify X_test.index is a subset of featured_data.index\n", " if not X_test.index.isin(featured_data.index).all():\n", " print(\"Error: X_test.index contains values not present in featured_data.index. Cannot align for backtest.\")\n", " else:\n", " # Select the portion of featured_data that corresponds to X_test\n", " data_for_backtest = featured_data.loc[X_test.index].copy()\n", "\n", " if data_for_backtest.empty:\n", " print(\"Error: Sliced data_for_backtest is empty. Check indices.\")\n", " else:\n", " print(f\"Data for backtest (based on X_test period) shape: {data_for_backtest.shape}\")\n", " \n", " # Add model predictions as the 'predicted_signal'\n", " # X_test should contain the same features (and in the same order) as X_train\n", " # Ensure X_test has the correct columns if any transformations were applied only to X_train\n", " try:\n", " # Re-check columns of X_test against model's expected features if issues arise\n", " # print(f\"X_test columns: {X_test.columns.tolist()}\")\n", " # if hasattr(model, 'feature_names_in_'):\n", " # print(f\"Model expected features: {model.feature_names_in_.tolist()}\")\n", "\n", " data_for_backtest['predicted_signal'] = model.predict(X_test)\n", " print(\"Successfully added 'predicted_signal' to backtesting data.\")\n", " \n", " # Run the backtest\n", " # Using Alpaca's taker fee for crypto, which can be around 0.1% to 0.3%\n", " # Let's use 0.1% (0.001) as an example. Adjust as needed.\n", " backtest_results_df, trades_log_list = run_simple_backtest(\n", " data_for_backtest,\n", " initial_capital=10000,\n", " trade_size_usd=1000, # Amount to invest per trade\n", " transaction_cost_pct=0.001 # e.g., 0.1% per trade side\n", " )\n", "\n", " if not backtest_results_df.empty:\n", " print(\"\\nSample of backtest results (last 5 rows):\")\n", " print(backtest_results_df[['close', 'predicted_signal', 'portfolio_value']].tail())\n", " \n", " if trades_log_list:\n", " print(\"\\nLast 5 trades:\")\n", " for trade in trades_log_list[-5:]:\n", " print(trade)\n", "\n", " except ValueError as ve:\n", " print(f\"ValueError during prediction or backtest setup: {ve}\")\n", " print(\"This might be due to a mismatch in feature names or number of features expected by the model.\")\n", " print(f\"X_test columns: {list(X_test.columns)}\")\n", " if hasattr(model, 'n_features_in_'):\n", " print(f\"Model was trained with {model.n_features_in_} features.\")\n", " if hasattr(model, 'feature_names_in_'):\n", " print(f\"Model feature names: {list(model.feature_names_in_)}\")\n", "\n", " except Exception as e:\n", " print(f\"An error occurred during backtest preparation or execution: {e}\")\n", "\n", "else:\n", " print(\"\\nSkipping backtest: Model, X_test, or featured_data is not available or empty.\")\n", " if 'model' not in locals() or model is None:\n", " print(\"- Model is not trained or available.\")\n", " if 'X_test' not in locals() or X_test.empty:\n", " print(\"- X_test data is not available.\")\n", " if 'featured_data' not in locals() or featured_data.empty:\n", " print(\"- featured_data is not available.\")" ] }, { "cell_type": "markdown", "id": "dc704773-08be-4465-8614-e3152b9b5ef0", "metadata": {}, "source": [ "### PROMPT\n", "\n", "Ok. I got this. What does it mean?" ] }, { "cell_type": "markdown", "id": "16b682cc-e3b0-4fa5-8414-2683a78fb97d", "metadata": {}, "source": [ "### GEMINI\n", "\n", "This output gives you the results of your simplified backtest. Let's break it down:\n", "\n", " Preparation:\n", "\n", " Preparing data for backtesting...\n", "\n", " Data for backtest (based on X_test period) shape: (1112, 25): This means the 1112 rows from your test set were successfully selected, and they now have 25 columns. These columns include your original features, the 'close' price, and the newly added 'predicted_signal'.\n", "\n", " Successfully added 'predicted_signal' to backtesting data.: Confirms your model's predictions are now part of the dataset used for the backtest.\n", "\n", " Backtest Summary Results:\n", "\n", " --- Simplified Backtest Results ---\n", "\n", " Initial Capital: $10,000.00: You started the simulation with $10,000.\n", "\n", " Final Portfolio Value: $9,792.06: After all the simulated trades over the test period, your portfolio ended up with $9,792.06.\n", "\n", " Total Return: -2.08%: Your strategy lost 2.08% of the initial capital over this test period. This is a negative return, meaning the strategy was not profitable.\n", "\n", " Number of trades executed: 214: The strategy made 214 trades (a buy or a sell counts as one trade in this context, so roughly 107 round trips if every buy was followed by a sell). For 1112 one-minute bars (which is about 18.5 hours of data), this is a relatively high trading frequency (average of about 1 trade every 5 minutes or ~11.5 trades per hour).\n", "\n", " Sample of Backtest Data (Last 5 Rows):\n", "\n", " This shows the 'close' price, the model's 'predicted_signal', and the 'portfolio_value' at each of the last 5 timestamps in your test data.\n", "\n", " timestamp: The date and time of the data point.\n", "\n", " close: The BTC/USD closing price for that minute.\n", "\n", " predicted_signal: The signal generated by your model (0 means \"not up\", 1 means \"up\"). In these last 5 rows, the signal is consistently 0.\n", "\n", " portfolio_value: The value of your simulated portfolio at that timestamp. Notice it remains constant at $9792.056818 for these last 5 rows. This is because the signal is 0, and presumably, the strategy was already out of a position (or never entered one if the signal was 0 from the start of these 5 rows and was also 0 before).\n", "\n", " Last 5 Trades Log:\n", "\n", " This gives you a glimpse into the actual trading activity. Each dictionary represents one trade.\n", "\n", " 'timestamp': When the trade occurred.\n", "\n", " 'type': 'BUY' or 'SELL'.\n", "\n", " 'price': The BTC price at which the trade was simulated.\n", "\n", " 'quantity_btc': How much BTC was bought or sold.\n", "\n", " 'cost_usd' (for BUYs): Total USD spent, including the fee.\n", "\n", " 'proceeds_usd' (for SELLs): Total USD received, after deducting the fee.\n", "\n", " 'fee_usd': The transaction fee paid for that trade (calculated as price * quantity * transaction_cost_pct).\n", "\n", " Observations from the trade log:\n", "\n", " The trades are happening frequently (e.g., a SELL at 23:07, then a BUY at 23:09, then a SELL at 23:18).\n", "\n", " The fee_usd is around $1.00 per trade, which is consistent with a trade_size_usd of\n", "\n", " \n", " 1000anda‘transactioncostpct‘of0.001(0.11000anda‘transactionc​ostp​ct‘of0.001(0.1\n", "\n", " \n", "\n", " 1000 * 0.001 = $1`.\n", "\n", " Looking at the BUY at 23:09 and SELL at 23:18:\n", "\n", " Bought at ~104846, cost $1001.00.\n", "\n", " Sold at ~104769, proceeds $998.27.\n", "\n", " This individual round trip was a small loss, exacerbated by fees.\n", "\n", " This high frequency of trades, where each trade incurs a fee, can quickly erode profits or magnify losses if the gains per trade are not significantly larger than the fees.\n", "\n", "What does this mean for your trading bot and what to do next?\n", "\n", " The Current Strategy is Not Profitable (on this test set with these parameters): A -2.08% return clearly indicates that, in its current state, the bot would lose money. The primary culprit is likely a combination of:\n", "\n", " Modest Predictive Accuracy: Your model's accuracy was ~51-52%. While slightly better than random, it might not be strong enough to overcome transaction costs, especially with frequent trading. The precision for \"up\" signals was below 50%.\n", "\n", " Transaction Costs: 214 trades, each costing around $1 (0.1% of $1000), means roughly $214 in fees were paid on an initial capital of $10,000. This is 214 / 10000 = 2.14% of your capital just in fees. This alone explains the negative return. Your gross profit (before fees) was likely very close to zero or slightly positive, but fees wiped it out and created a loss.\n", "\n", " High Trading Frequency: The model seems to be generating signals quite often. This might be due to noisy features or a target variable that flips frequently.\n", "\n", " Next Steps - Focus on Improving Profitability:\n", "\n", " Reduce Trading Frequency / Improve Signal Quality:\n", "\n", " Refine the Target Variable: This is often the most impactful. Instead of just price > prev_price, require a more significant move: (future_price - current_price) / current_price > (transaction_cost_pct * 2 + desired_profit_margin). For example, if your round-trip cost is 0.2% (0.1% buy + 0.1% sell), you might want the price to move at least 0.3% or 0.4% in your favor before considering it a valid \"up\" signal for training. This will make \"1\" signals rarer but hopefully higher quality.\n", "\n", " Longer target_horizon: Predicting 5 minutes ahead is short. Try 15m, 30m, or 1h. This might smooth out noise.\n", "\n", " Add a \"Hold\" or \"No Signal\" State: Your current strategy is always either trying to get in (signal 1) or get out (signal 0). Perhaps only trade on very confident signals.\n", "\n", " Smoothing Features/Signals: Apply smoothing (e.g., moving average) to your raw predictions or some of your more jittery features if they are causing frequent signal changes.\n", "\n", " Improve Model Accuracy/Precision:\n", "\n", " Continue with feature engineering.\n", "\n", " Hyperparameter tune your XGBoost model (e.g., using GridSearchCV or RandomizedSearchCV with TimeSeriesSplit for cross-validation). Pay attention to precision for the class \"1\" (up-signal).\n", "\n", " Try different model types (e.g., LightGBM, Logistic Regression, an LSTM if you have a lot more data and want to capture sequences).\n", "\n", " Decrease Impact of Transaction Costs:\n", "\n", " The most direct way is to trade less frequently (see point 1).\n", "\n", " Ensure your trade_size_usd is appropriate. If it's too small, fixed fees (if any) can have a larger percentage impact. (Alpaca is percentage-based for crypto, so this is less of an issue than with fixed fees per share).\n", "\n", " Analyze Losing Trades:\n", "\n", " If you log more details (like the features at the time of the trade), you can analyze why the model made a wrong prediction. Were there specific market conditions or feature values that consistently led to bad trades?\n", "\n", " Increase Data for Training/Testing:\n", "\n", " 1112 minutes (less than a day) for testing is a very short period. Market behavior can vary significantly. Try to get a much longer period for more robust backtesting.\n", "\n", " Consider a Different Strategy Logic:\n", "\n", " Maybe don't sell immediately when the signal turns to 0. Implement a trailing stop loss, or a take profit target, or hold for a fixed duration after a buy signal.\n", "\n", "The key takeaway is that your system is working end-to-end, but the strategy/model isn't yet profitable. The backtest results are invaluable for guiding your next iteration of improvements. The fact that transaction costs alone account for the current loss is a strong hint that reducing trade frequency by improving signal quality is a high-priority area." ] }, { "cell_type": "markdown", "id": "409a5f55-69bf-4442-b3af-154f84337341", "metadata": {}, "source": [ "### Step 6: Signal Generation & Order Execution (Live/Paper Trading)\n", "\n", "This is where you'd run the bot periodically (e.g., every minute)." ] }, { "cell_type": "markdown", "id": "d839a691-458b-4cf4-9c30-8be554ab2b08", "metadata": {}, "source": [ "### PROMPT\n", "\n", "Ok. Now, previously you prepared this for the next step. Can you adjust it now to work with our working code: Step 6: Signal Generation & Order Execution (Live/Paper Trading)" ] }, { "cell_type": "markdown", "id": "3db00f5f-e663-4f75-8b71-d32d2ac17dd0", "metadata": {}, "source": [ "### GEMINI\n", "\n", "Okay, this \"Step 6: Signal Generation & Order Execution\" is where your bot starts interacting with the live (or paper) market. It's a critical step and needs careful handling.\n", "\n", "Let's adjust it to fit with the code structure we've built, particularly ensuring it uses the globally available model and feature_cols that were defined/created in Step 4 (Model Training).\n", "\n", "Key Assumptions & Globals from Previous Steps:\n", "\n", " api: The Alpaca tradeapi.REST object, initialized and connected.\n", "\n", " create_features: The function using finta to calculate technical indicators.\n", "\n", " model: Your trained XGBoost model (or whatever model you used). This should be a global variable after Step 4.\n", "\n", " feature_cols: A list of feature column names that the model was trained on. This should also be a global variable defined in Step 4.\n", "\n", "Adjusted Step 6 Code:" ] }, { "cell_type": "markdown", "id": "9e45bdd9-cac4-4c49-998b-c0da19217bc6", "metadata": {}, "source": [ "Key Changes and Enhancements:\n", "\n", " Global Variables: Explicitly uses global api, create_features, model, feature_cols, SYMBOL_TO_TRADE, TRADE_QTY_USD within functions where needed, assuming these are set up in the main script body.\n", "\n", " get_latest_bar_features Function:\n", "\n", " Takes symbol_to_fetch, timeframe_to_use, and max_lookback_period as arguments.\n", "\n", " Uses datetime.now(tz=timezone.utc) for timezone-aware start time. (You'll need from datetime import timezone).\n", "\n", " Explicitly lowercases column names from Alpaca before passing to create_features.\n", "\n", " Added more robust error handling and checks for sufficient data length.\n", "\n", " The logic to select the features for the most recent complete bar is handled by featured_bars[feature_cols].iloc[-1:] after create_features (which includes dropna).\n", "\n", " Added a check to ensure all feature_cols are present in the featured_bars DataFrame before selection to prevent KeyError.\n", "\n", " check_and_place_trade Function:\n", "\n", " Retrieves model, feature_cols, api, etc., via global.\n", "\n", " Confidence Threshold: Added proba = model.predict_proba(current_features_df)[0] to get prediction probabilities and introduced a CONFIDENCE_THRESHOLD. The bot will only act on a \"BUY\" signal if the model's confidence for that prediction is above this threshold. This is a common technique to filter out weaker signals.\n", "\n", " Price Fetching: Uses api.get_latest_crypto_quote(SYMBOL_TO_TRADE, exchange=\"CBSE\") to get a more current bid/ask price. You need to specify an exchange. Common ones for BTC/USD on Alpaca are CBSE (Coinbase), ERSX, FTXU. Check Alpaca's documentation for available crypto exchanges.\n", "\n", " Quantity Calculation and Rounding: Explicitly rounds qty_to_buy and qty_to_sell to a suitable number of decimal places (e.g., 7 for BTC) and converts to string for the API order. Added a check to ensure qty_to_buy_rounded > 0.\n", "\n", " Improved print statements for clarity.\n", "\n", " More detailed error handling, including traceback.print_exc() for debugging.\n", "\n", " Main Loop:\n", "\n", " The if __name__ == \"__main__\": block now includes checks to ensure model and feature_cols are actually populated before starting the loop.\n", "\n", " The timing loop is slightly improved to wait until the next minute starts, but it's still basic. For serious live trading, use APScheduler or a similar library for precise execution at the start of each new bar.\n", "\n", " Added traceback.print_exc() in the main loop's exception handler.\n", "\n", "Before Running (CRITICAL):\n", "\n", " PAPER TRADING ONLY: Start with paper trading. Ensure your Alpaca API keys are for your paper account.\n", "\n", " Global Scope: Verify that model (your trained XGBoost model) and feature_cols (the list of feature names) are correctly populated and accessible in the global scope of your script before this Step 6 code is reached.\n", "\n", " create_features Function: Ensure your create_features function (the one using finta) is defined and works correctly.\n", "\n", " API Object: Ensure api is your initialized Alpaca tradeapi.REST object.\n", "\n", " Constants: Adjust SYMBOL_TO_TRADE, TRADE_QTY_USD, and CONFIDENCE_THRESHOLD as needed.\n", "\n", " Exchange for Quotes: In get_latest_crypto_quote, make sure exchange=\"CBSE\" (or another valid exchange from Alpaca for BTC/USD) is appropriate.\n", "\n", " Precision for Orders: The round(qty_to_buy, 7) (and for sell) uses 7 decimal places. This is typical for BTC. Verify Alpaca's requirements for BTC/USD order quantity precision if you encounter issues.\n", "\n", " Error Handling: This code includes basic error handling. Monitor logs carefully. Real trading bots need much more robust error management, retry logic, and state management.\n", "\n", " Scheduler: For a real bot, replace the time.sleep() loop with a proper scheduler (like APScheduler) to run check_and_place_trade() precisely when new bars form (e.g., at HH:MM:00 for 1-minute bars).\n", "\n", "How to Integrate:\n", "\n", " Place the get_latest_bar_features and check_and_place_trade functions in your script.\n", "\n", " Define the constants (SYMBOL_TO_TRADE, etc.) after your model training section.\n", "\n", " The if __name__ == \"__main__\": block containing the trading loop should be at the very end of your script, so it only runs when the script is executed directly and after all setup/training is complete.\n", "\n", "This provides a more robust foundation for your bot's execution logic. Test thoroughly in paper trading!" ] }, { "cell_type": "code", "execution_count": null, "id": "0e08f195-a9f3-4ccd-91e7-8b99656a886d", "metadata": {}, "outputs": [], "source": [ "import os # Already imported, but good to note for this section\n", "import alpaca_trade_api as tradeapi # Already imported\n", "import pandas as pd # Already imported\n", "from datetime import datetime, timedelta # Already imported\n", "import time # Already imported\n", "\n", "# --- Constants for Step 6 ---\n", "# These should be defined after your API setup and model training\n", "SYMBOL_TO_TRADE = \"BTC/USD\" # Use the same symbol format as Alpaca uses\n", "TRADE_QTY_USD = 100 # Amount in USD to trade per signal. Adjust based on risk.\n", "# TARGET_HORIZON_MINUTES is implicitly handled by the model's training\n", "# but not directly used in this execution logic, other than fetching enough data.\n", "\n", "# Ensure necessary globals are defined from previous steps\n", "# For example, at the end of Step 4, you should have:\n", "# model = your_trained_xgboost_model\n", "# feature_cols = list_of_your_feature_column_names\n", "\n", "# And from the beginning of your script:\n", "# api = tradeapi.REST(...)\n", "# create_features = your_finta_based_feature_creation_function\n", "\n", "\n", "def get_latest_bar_features(symbol_to_fetch, timeframe_to_use, max_lookback_period=150):\n", " \"\"\"\n", " Fetches latest bars, calculates features for the most recent *complete* bar.\n", " max_lookback_period: Number of bars to fetch to ensure enough data for all indicators.\n", " e.g., if longest MA is 30, and RSI is 14, fetch at least 30+ buffer.\n", " 150 minutes should cover most common 1-min indicators.\n", " \"\"\"\n", " global api, create_features, feature_cols # Access global api, create_features, and feature_cols\n", "\n", " # Calculate start time for fetching data\n", " # We need enough bars for feature calculation, so go back further than just max_lookback_period\n", " # to account for potential gaps or incomplete last bar from API.\n", " # For 1-minute bars, max_lookback_period minutes. Let's add a buffer.\n", " now_utc = datetime.now(tz=timezone.utc) # Ensure timezone awareness for Alpaca\n", " start_fetch_dt = now_utc - timedelta(minutes=max_lookback_period + 60) # Extra 60 min buffer\n", "\n", " print(f\"Fetching latest {max_lookback_period} bars for {symbol_to_fetch} from {start_fetch_dt.isoformat()}...\")\n", "\n", " try:\n", " latest_bars_df = api.get_crypto_bars(\n", " symbol_to_fetch,\n", " timeframe_to_use, # e.g., tradeapi.TimeFrame.Minute\n", " start=start_fetch_dt.isoformat(),\n", " # end defaults to now, limit helps control amount\n", " limit=max_lookback_period + 5 # Fetch a few extra just in case\n", " ).df\n", " \n", " # Ensure columns are lowercase as expected by 'create_features'\n", " latest_bars_df.columns = [col.lower() for col in latest_bars_df.columns]\n", "\n", " except Exception as e:\n", " print(f\"Error fetching latest bars: {e}\")\n", " return None\n", "\n", " if latest_bars_df.empty:\n", " print(\"No recent bars data fetched.\")\n", " return None\n", "\n", " # The last bar from Alpaca might be incomplete if fetched mid-minute.\n", " # We want to make predictions based on the *last fully completed* bar.\n", " # For 1-minute data, if we fetch at XX:XX:30, the bar for XX:XX:00 is complete.\n", " # Let's assume the second to last bar is definitely complete, or if only one bar, it's what we have.\n", " # More robust: filter out bars with timestamp > current_minute_start\n", " \n", " # A simple approach: ensure enough bars for features\n", " # Longest feature (e.g., SMA_30) needs 30 periods. RSI needs 14.\n", " # Let's say your longest feature (like SMA_30) needs 'N' periods.\n", " # `create_features` will drop N-1 rows.\n", " required_bars_for_features = 35 # Example: if SMA(30) is longest, need at least 30 + buffer\n", " if len(latest_bars_df) < required_bars_for_features:\n", " print(f\"Not enough recent bars ({len(latest_bars_df)}) to calculate all features (need ~{required_bars_for_features}).\")\n", " return None\n", "\n", " print(f\"Calculating features for {len(latest_bars_df)} bars...\")\n", " # The create_features function was defined in your earlier code (using finta)\n", " try:\n", " featured_bars = create_features(latest_bars_df) # .copy() is handled inside create_features\n", " except Exception as e:\n", " print(f\"Error during feature creation for latest bars: {e}\")\n", " return None\n", " \n", " if featured_bars.empty:\n", " print(\"Failed to create features (resulted in empty DataFrame).\")\n", " return None\n", "\n", " # Return only the features for the most recent bar for which features could be calculated\n", " # `dropna()` in `create_features` removes initial NaNs. The last row of `featured_bars` is what we need.\n", " # The features must match `feature_cols` used during training.\n", " try:\n", " # Ensure all expected feature_cols are present in featured_bars before selecting\n", " missing_cols = [col for col in feature_cols if col not in featured_bars.columns]\n", " if missing_cols:\n", " print(f\"Error: The following feature columns are missing after calculation: {missing_cols}\")\n", " print(f\"Available columns: {featured_bars.columns.tolist()}\")\n", " return None\n", " \n", " # Select only the feature_cols and take the last row\n", " final_features_for_prediction = featured_bars[feature_cols].iloc[-1:]\n", " return final_features_for_prediction # Return as a DataFrame (single row)\n", " except Exception as e:\n", " print(f\"Error selecting final features for prediction: {e}\")\n", " print(f\"Feature_cols expected: {feature_cols}\")\n", " print(f\"Columns in featured_bars: {featured_bars.columns.tolist()}\")\n", " return None\n", "\n", "\n", "def check_and_place_trade():\n", " global model, feature_cols, api, SYMBOL_TO_TRADE, TRADE_QTY_USD # Ensure these are accessible\n", "\n", " if model is None: # Check if model is loaded/trained\n", " print(\"Model is not available. Skipping trade check.\")\n", " return\n", " if not feature_cols: # Check if feature_cols is defined\n", " print(\"Feature columns not defined. Skipping trade check.\")\n", " return\n", "\n", " print(f\"\\n[{datetime.now()} UTC] Checking for trading signal for {SYMBOL_TO_TRADE}...\")\n", " \n", " # Get features for the latest complete bar\n", " # Assuming tradeapi.TimeFrame.Minute was used for training and data fetching\n", " current_features_df = get_latest_bar_features(SYMBOL_TO_TRADE, tradeapi.TimeFrame.Minute)\n", "\n", " if current_features_df is None or current_features_df.empty:\n", " print(\"Could not get features for the latest bar. Skipping trade.\")\n", " return\n", "\n", " # The get_latest_bar_features should already return data with columns in feature_cols order\n", " # But an explicit reorder is a good safeguard if the selection logic changes.\n", " # current_features_df = current_features_df[feature_cols] # This was already done in get_latest_bar_features\n", "\n", " try:\n", " prediction = model.predict(current_features_df) # `current_features_df` is already a DataFrame\n", " signal = prediction[0] # Prediction for the single latest bar\n", " proba = model.predict_proba(current_features_df)[0] # Get probabilities [prob_class_0, prob_class_1]\n", "\n", " print(f\"Features for prediction: {current_features_df.iloc[0].to_dict()}\")\n", " print(f\"Model Prediction (0=NotUp, 1=Up): {signal}, Confidence (Prob for Up): {proba[1]:.4f}\")\n", "\n", " except Exception as e:\n", " print(f\"Error during model prediction: {e}\")\n", " print(f\"Features passed to model: {current_features_df}\")\n", " return\n", "\n", " # --- Alpaca API Interaction ---\n", " try:\n", " positions = api.list_positions()\n", " btc_position = next((p for p in positions if p.symbol == SYMBOL_TO_TRADE), None)\n", " \n", " # Use get_latest_trade for a more \"real-time\" price if available, or quote\n", " # latest_trade = api.get_latest_crypto_trade(SYMBOL_TO_TRADE, exchange=\"CBSE\") # Example exchange\n", " # current_price = latest_trade.p\n", " # Or use quotes:\n", " latest_quote = api.get_latest_crypto_quote(SYMBOL_TO_TRADE, exchange=\"CBSE\") # Coinbase is a common exchange for BTCUSD\n", " current_price = (latest_quote.ap + latest_quote.bp) / 2.0 # Mid-price from ask/bid\n", "\n", " if not current_price or current_price <= 0:\n", " print(f\"Could not get a valid current price for {SYMBOL_TO_TRADE}. Price: {current_price}\")\n", " return\n", "\n", " print(f\"Current mid-price for {SYMBOL_TO_TRADE}: ${current_price:.2f}\")\n", "\n", " # --- Trade Logic ---\n", " # Simple strategy: Buy on signal 1, Sell (if holding) on signal 0\n", " # Add a confidence threshold:\n", " CONFIDENCE_THRESHOLD = 0.55 # Only trade if probability of \"UP\" is > 55% (example)\n", "\n", " if signal == 1 and proba[1] >= CONFIDENCE_THRESHOLD: # Predicted UP with enough confidence\n", " if btc_position is None or float(btc_position.qty) == 0:\n", " qty_to_buy = TRADE_QTY_USD / current_price\n", " # Alpaca has precision requirements for crypto orders.\n", " # For BTC, usually 6-8 decimal places for quantity.\n", " qty_to_buy_rounded = round(qty_to_buy, 7) # Adjust precision as needed for BTC\n", "\n", " if qty_to_buy_rounded > 0: # Ensure non-zero quantity\n", " print(f\"BUY signal (confidence {proba[1]:.2f}). Attempting to buy {qty_to_buy_rounded:.7f} {SYMBOL_TO_TRADE} at ~${current_price:.2f}\")\n", " api.submit_order(\n", " symbol=SYMBOL_TO_TRADE,\n", " qty=str(qty_to_buy_rounded), # Qty as string\n", " side='buy',\n", " type='market',\n", " time_in_force='gtc' # Good 'til Canceled\n", " )\n", " print(f\"BUY order for {qty_to_buy_rounded:.7f} {SYMBOL_TO_TRADE} submitted.\")\n", " else:\n", " print(f\"Calculated buy quantity {qty_to_buy_rounded:.7f} is too small. No order placed.\")\n", " else:\n", " print(f\"BUY signal (confidence {proba[1]:.2f}), but already have a position of {btc_position.qty} {SYMBOL_TO_TRADE}. Holding.\")\n", "\n", " elif signal == 0: # Predicted NOT UP (or low confidence for UP) - Potential SELL\n", " if btc_position and float(btc_position.qty) > 0:\n", " qty_to_sell = float(btc_position.qty) # Sell entire position\n", " qty_to_sell_rounded = round(qty_to_sell, 7) # Match precision\n", "\n", " if qty_to_sell_rounded > 0:\n", " print(f\"SELL signal (or low UP confidence). Attempting to sell {qty_to_sell_rounded:.7f} {SYMBOL_TO_TRADE} at ~${current_price:.2f}\")\n", " api.submit_order(\n", " symbol=SYMBOL_TO_TRADE,\n", " qty=str(qty_to_sell_rounded), # Qty as string\n", " side='sell',\n", " type='market',\n", " time_in_force='gtc'\n", " )\n", " print(f\"SELL order for {qty_to_sell_rounded:.7f} {SYMBOL_TO_TRADE} submitted.\")\n", " else:\n", " print(f\"Sell quantity {qty_to_sell_rounded:.7f} is zero or negative. No order placed.\")\n", " else:\n", " print(\"SELL signal (or low UP confidence), but no open position to sell. Doing nothing.\")\n", " \n", " else: # Signal is 1 but confidence is too low\n", " print(f\"UP signal (signal={signal}) but confidence {proba[1]:.2f} is below threshold {CONFIDENCE_THRESHOLD}. No action.\")\n", "\n", "\n", " except Exception as e:\n", " print(f\"Error during trade execution or API interaction: {e}\")\n", " import traceback\n", " traceback.print_exc()\n", "\n", "\n", "# --- Main Execution Loop (for demonstration) ---\n", "# This should be run after your model is trained and `model` and `feature_cols` are populated.\n", "\n", "# Example of how to make `model` and `feature_cols` global if they are defined in a function:\n", "# This is just illustrative; in your script, they should be in the global scope after training.\n", "# if 'model' not in globals(): model = None\n", "# if 'feature_cols' not in globals(): feature_cols = []\n", "\n", "if __name__ == \"__main__\":\n", " # --- This block is usually at the very end of your script ---\n", " # --- It assumes all previous steps (API init, data fetch, feature creation, model training) have run ---\n", "\n", " # Check if the model and feature_cols are actually populated from Step 4\n", " if 'model' not in globals() or model is None:\n", " print(\"Model is not trained or not found in global scope. Exiting live trading part.\")\n", " # exit() # Or handle appropriately\n", " elif 'feature_cols' not in globals() or not feature_cols:\n", " print(\"Feature_cols not defined or empty. Exiting live trading part.\")\n", " # exit()\n", " else:\n", " print(\"\\n--- Starting Live/Paper Trading Bot (Simplified Loop) ---\")\n", " print(\"IMPORTANT: THIS IS FOR PAPER TRADING. VERIFY ALL SETTINGS.\")\n", " print(f\"Trading Symbol: {SYMBOL_TO_TRADE}, USD per Trade: {TRADE_QTY_USD}\")\n", " print(f\"Using Model: {type(model).__name__}, Trained Features: {len(feature_cols)}\")\n", " \n", " # You would typically use a robust scheduler like APScheduler for live trading\n", " # to run check_and_place_trade() precisely at the start of each new bar (e.g., every minute).\n", " # This is a very simplified loop for demonstration.\n", " \n", " try:\n", " # Initial check\n", " check_and_place_trade()\n", " \n", " # Loop for a few more checks\n", " for i in range(5): # Run for a few iterations (e.g., 5 minutes)\n", " # Wait for the next minute (approximately).\n", " # This is NOT a precise way to align with market bars.\n", " current_seconds = datetime.now().second\n", " wait_time = 60 - current_seconds\n", " if wait_time < 0: wait_time = 60 + wait_time # if somehow it's past the minute\n", " print(f\"Waiting for {wait_time} seconds until the next minute...\")\n", " time.sleep(wait_time) \n", " check_and_place_trade()\n", " \n", " except KeyboardInterrupt:\n", " print(\"\\nTrading loop stopped by user.\")\n", " except Exception as e:\n", " print(f\"An unhandled error occurred in the trading loop: {e}\")\n", " import traceback\n", " traceback.print_exc()\n", " finally:\n", " print(\"Trading loop finished.\")\n", "\n", "# else:\n", "# if 'model' not in globals() or model is None:\n", "# print(\"Script not run as main, or model not yet trained. Live trading functions defined but not executed.\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.12.3" } }, "nbformat": 4, "nbformat_minor": 5 }