(Remember: At the moment Alpaca uses different symbols for executing orders such as “BTC/USD” and checking portfolio positions such as “BTCUSD”.)
KEY_ID = "your API KEY" #replace it with your own KEY_ID from Alpaca: https://alpaca.markets/
SECRET_KEY = "your SECRET KEY" #replace it with your own SECRET_KEY from Alpaca
import warnings
warnings.filterwarnings('ignore')
Step 1: Alpaca Setup & Python Environment
Sign up for an Alpaca account: https://alpaca.markets/
Get your API Key ID and Secret Key from your paper trading account dashboard.
Install necessary Python libraries:
pip install alpaca-trade-api pandas scikit-learn xgboost numpy pandas-ta
Set up API credentials (environment variables are best):
import os
import alpaca_trade_api as tradeapi
import pandas as pd
from datetime import datetime, timedelta
import time
# For Paper Trading
os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets'
# Replace with your actual keys or set them as environment variables
os.environ['APCA_API_KEY_ID'] = 'your API KEY'
os.environ['APCA_API_SECRET_KEY'] = 'your SECRET KEY'
API_KEY = os.getenv('APCA_API_KEY_ID')
API_SECRET = os.getenv('APCA_API_SECRET_KEY')
BASE_URL = os.getenv('APCA_API_BASE_URL')
api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')
print("Connected to Alpaca Paper Trading.")
# Check account
# account = api.get_account()
# print(f"Account status: {account.status}")
Connected to Alpaca Paper Trading.
Step 2: Data Acquisition
def fetch_data(symbol, timeframe, start_date_str, end_date_str=None):
"""Fetches historical crypto data from Alpaca."""
if end_date_str is None:
end_date_str = datetime.now().strftime('%Y-%m-%d')
# Alpaca API expects ISO 8601 format for start/end times
# And it has a limit on how many bars can be fetched per request (e.g., 10000 for crypto)
# So we may need to fetch in chunks if requesting a long period.
all_bars = []
start_dt = pd.to_datetime(start_date_str, utc=True)
end_dt = pd.to_datetime(end_date_str, utc=True)
# Fetch data in chunks to avoid hitting API limits for very long periods
# For 1-minute data, 10000 bars is about 7 days.
# Let's fetch data in smaller chunks, e.g., 5 days at a time.
current_start = start_dt
while current_start < end_dt:
chunk_end = min(current_start + timedelta(days=5), end_dt) # Adjust chunk size as needed
print(f"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}")
# Alpaca's get_crypto_bars expects start and end in ISO format
bars = api.get_crypto_bars(
symbol,
timeframe,
start=current_start.isoformat(),
end=chunk_end.isoformat(),
limit=10000 # Max limit per request
).df
if bars.empty:
print(f"No data found for chunk starting {current_start.isoformat()}")
if current_start + timedelta(days=5) > end_dt and not all_bars: # if first chunk and no data
break
current_start += timedelta(days=5) # Move to next chunk period
time.sleep(1) # Be nice to the API
continue
all_bars.append(bars)
# Update current_start to the timestamp of the last bar fetched + 1 minute
# to avoid overlap and to ensure we move forward.
if not bars.index.empty:
current_start = bars.index[-1] + pd.Timedelta(minutes=1)
else: # Should not happen if bars is not empty, but as a safeguard
current_start += timedelta(days=5)
time.sleep(1) # Respect API rate limits
if not all_bars:
print("No data fetched. Check your date range or symbol.")
return pd.DataFrame()
df = pd.concat(all_bars)
df = df[~df.index.duplicated(keep='first')] # Remove potential duplicates from chunking
df = df.sort_index() # Ensure chronological order
df = df[df.index <= end_dt] # Ensure we don't go past the requested end_dt
return df
# Example usage:
symbol = "BTC/USD" # Alpaca uses "BTC/USD" for crypto pairs
timeframe = tradeapi. টাইমফ্রেম.মিনিট # or "1Min" for older SDK versions
# Fetch last 30 days of data for example
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
# end_date = datetime.now().strftime('%Y-%m-%d') # Fetches up to now
btc_data = fetch_data(symbol, timeframe, start_date)
if not btc_data.empty:
print(f"Fetched {len(btc_data)} rows of {symbol} data.")
print(btc_data.head())
print(btc_data.tail())
else:
print("Failed to fetch data.")
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[5], line 61 59 # Example usage: 60 symbol = "BTC/USD" # Alpaca uses "BTC/USD" for crypto pairs ---> 61 timeframe = tradeapi. টাইমফ্রেম.মিনিট # or "1Min" for older SDK versions 62 # Fetch last 30 days of data for example 63 start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') AttributeError: module 'alpaca_trade_api' has no attribute 'টাইমফ্রেম'
Step 3: Feature Engineering
This is where you create signals for your model. pandas-ta is a great library for this.
import pandas_ta as ta
def create_features(df):
if df.empty or 'close' not in df.columns:
print("DataFrame is empty or 'close' column is missing.")
return df
df.ta.sma(length=10, append=True, col_names=('SMA_10'))
df.ta.sma(length=30, append=True, col_names=('SMA_30'))
df.ta.ema(length=10, append=True, col_names=('EMA_10'))
df.ta.rsi(length=14, append=True, col_names=('RSI_14'))
df.ta.macd(append=True, col_names=('MACD_12_26_9', 'MACDh_12_26_9', 'MACDs_12_26_9'))
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'))
df.ta.atr(length=14, append=True, col_names=('ATR_14'))
# Lagged returns
for lag in [1, 3, 5, 10]:
df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)
# Add more features: volatility, momentum, volume-based if available, etc.
# e.g., log returns, price relative to moving average, etc.
df.dropna(inplace=True) # Remove rows with NaNs created by indicators
return df
if not btc_data.empty:
featured_data = create_features(btc_data.copy()) # Use .copy() to avoid modifying original
print("\nData with features:")
print(featured_data.head())
else:
print("Cannot create features, btc_data is empty.")
featured_data = pd.DataFrame() # ensure it's a DataFrame
Step 4: Model Training - Defining Target & Training
Let's define a simple target: will the price be higher or lower in N minutes?
from sklearn.model_selection import train_test_split, TimeSeriesSplit
from sklearn.ensemble import RandomForestClassifier # Example model
from sklearn.metrics import accuracy_score, classification_report
import xgboost as xgb
def prepare_data_for_model(df, target_horizon=5, test_size=0.2):
"""
Prepares X (features) and y (target) for the ML model.
Target: 1 if price increases by more than a small threshold after target_horizon periods, 0 otherwise.
-1 if price decreases by more than a small threshold. (Optional: make it 3 classes)
"""
if df.empty or 'close' not in df.columns:
print("DataFrame is empty or 'close' column is missing.")
return pd.DataFrame(), pd.Series(dtype='float64'), pd.DataFrame(), pd.Series(dtype='float64')
# Define target: 1 if price goes up in `target_horizon` minutes, 0 otherwise
# A small threshold can help avoid noise around 0% change
# price_threshold = 0.0005 # e.g., 0.05% change
# df['future_price'] = df['close'].shift(-target_horizon)
# df['price_change'] = (df['future_price'] - df['close']) / df['close']
# df['target'] = 0 # Hold
# df.loc[df['price_change'] > price_threshold, 'target'] = 1 # Buy
# df.loc[df['price_change'] < -price_threshold, 'target'] = -1 # Sell (for 3-class)
# For 2-class (Up/Not Up):
df['target'] = (df['close'].shift(-target_horizon) > df['close']).astype(int)
df.dropna(inplace=True) # Remove rows with NaN target (due to shift)
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']]
X = df[feature_columns]
y = df['target']
# Time series split is crucial: DO NOT shuffle time series data for training
# For a simple split:
split_index = int(len(X) * (1 - test_size))
X_train, X_test = X[:split_index], X[split_index:]
y_train, y_test = y[:split_index], y[split_index:]
# For more robust cross-validation, use TimeSeriesSplit
# tscv = TimeSeriesSplit(n_splits=5)
# for train_index, test_index in tscv.split(X):
# X_train, X_test = X.iloc[train_index], X.iloc[test_index]
# y_train, y_test = y.iloc[train_index], y.iloc[test_index]
# Train and evaluate your model here
return X_train, X_test, y_train, y_test, feature_columns
if not featured_data.empty:
X_train, X_test, y_train, y_test, feature_cols = prepare_data_for_model(featured_data.copy(), target_horizon=5)
if not X_train.empty:
print(f"\nTraining data shape: X_train: {X_train.shape}, y_train: {y_train.shape}")
print(f"Test data shape: X_test: {X_test.shape}, y_test: {y_test.shape}")
print(f"Features used: {feature_cols}")
# Example Model: Random Forest
# model = RandomForestClassifier(n_estimators=100, random_state=42, class_weight='balanced')
# Example Model: XGBoost (often performs well)
model = xgb.XGBClassifier(
objective='binary:logistic', # or 'multi:softprob' for multi-class
n_estimators=100,
learning_rate=0.1,
max_depth=3,
use_label_encoder=False, # Suppress a warning
eval_metric='logloss' # or 'mlogloss' for multi-class
)
model.fit(X_train, y_train)
# Evaluate on test set
y_pred = model.predict(X_test)
print("\nModel Evaluation on Test Set:")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred, zero_division=0))
# Feature importance (for tree-based models)
if hasattr(model, 'feature_importances_'):
importances = pd.Series(model.feature_importances_, index=X_train.columns).sort_values(ascending=False)
print("\nFeature Importances:")
print(importances.head(10))
else:
print("Not enough data to create training/test sets after feature engineering and target creation.")
else:
print("Cannot prepare data for model, featured_data is empty.")
model = None # Ensure model is defined even if training fails
Important Considerations for Modeling:
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.
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).
Stationarity: Price series are generally non-stationary. Features like returns or indicators often help.
Overfitting: Models can learn noise from historical data. Robust cross-validation (like TimeSeriesSplit) and regularization are key.
Step 5: Backtesting (Simplified Vectorized Example)
A proper backtest is event-driven and considers transaction costs, slippage, etc. This is a very simplified version.
def run_simple_backtest(df_with_predictions, initial_capital=10000, trade_size_usd=1000, transaction_cost_pct=0.003): # Alpaca crypto fee
"""
A very simplified vectorized backtest.
Assumes df_with_predictions has a 'signal' column (1 for buy, 0 for hold/nothing, -1 for sell if implementing).
For this example, we'll assume our model's prediction (0 or 1) is the signal.
1 = Go Long, 0 = Exit Long (or do nothing if not in position)
"""
if df_with_predictions.empty or 'predicted_signal' not in df_with_predictions.columns:
print("DataFrame for backtest is empty or 'predicted_signal' column missing.")
return
capital = initial_capital
position_btc = 0 # Amount of BTC held
portfolio_value = []
# Assume 'predicted_signal' comes from your model (1 for predicted up, 0 for predicted down/neutral)
# Let's assume a simple strategy: if signal is 1, buy. If signal is 0 and we have a position, sell.
for i in range(len(df_with_predictions)):
current_price = df_with_predictions['close'].iloc[i]
signal = df_with_predictions['predicted_signal'].iloc[i]
# Decision logic
if signal == 1 and position_btc == 0: # Buy signal and no current position
# Buy
amount_to_buy_btc = trade_size_usd / current_price
cost = amount_to_buy_btc * current_price * (1 + transaction_cost_pct)
if capital >= cost:
capital -= cost
position_btc += amount_to_buy_btc
# print(f"{df_with_predictions.index[i]}: BUY {amount_to_buy_btc:.6f} BTC @ {current_price:.2f}")
elif signal == 0 and position_btc > 0: # Sell signal (or neutral) and have a position
# Sell
proceeds = position_btc * current_price * (1 - transaction_cost_pct)
capital += proceeds
# print(f"{df_with_predictions.index[i]}: SELL {position_btc:.6f} BTC @ {current_price:.2f}")
position_btc = 0
current_portfolio_value = capital + (position_btc * current_price)
portfolio_value.append(current_portfolio_value)
df_with_predictions['portfolio_value'] = portfolio_value
print("\nBacktest Results:")
print(f"Initial Capital: ${initial_capital:.2f}")
print(f"Final Portfolio Value: ${df_with_predictions['portfolio_value'].iloc[-1]:.2f}")
returns = (df_with_predictions['portfolio_value'].iloc[-1] / initial_capital - 1) * 100
print(f"Total Return: {returns:.2f}%")
# Plotting (optional)
# import matplotlib.pyplot as plt
# plt.figure(figsize=(12,6))
# plt.plot(df_with_predictions.index, df_with_predictions['portfolio_value'])
# plt.title('Portfolio Value Over Time')
# plt.xlabel('Date')
# plt.ylabel('Portfolio Value ($)')
# plt.show()
if model and not X_test.empty:
# Use the model to predict on the entire test set for backtesting
# For a more realistic backtest, you'd re-train periodically or use a walk-forward approach.
# Here, we're just using the single model trained on X_train.
all_featured_data_for_backtest = featured_data.loc[X_test.index].copy() # Get original data rows for X_test
all_featured_data_for_backtest['predicted_signal'] = model.predict(X_test) # Use the trained model
run_simple_backtest(all_featured_data_for_backtest)
else:
print("Skipping backtest as model or test data is not available.")
Backtesting Libraries: For more serious backtesting, consider backtrader or zipline-reloaded. They handle many complexities.
Step 6: Signal Generation & Order Execution (Live/Paper Trading)
This is where you'd run the bot periodically (e.g., every minute).
SYMBOL = "BTC/USD"
TRADE_QTY_USD = 100 # Amount in USD to trade per signal. Adjust based on risk tolerance.
TARGET_HORIZON_MINUTES = 5 # Same as used in training
# Global model and feature_cols (assuming they are trained and available)
# model = ... (your trained model)
# feature_cols = ... (list of feature column names used for training)
def get_latest_bar_features():
"""Fetches latest bars, calculates features for the most recent one."""
# Fetch enough data to calculate all features (e.g., max lookback of your indicators)
# If SMA_30 is longest, need at least 30 + target_horizon previous bars
# Let's fetch more to be safe, e.g., 100 bars
now = datetime.now()
start_fetch_dt = (now - timedelta(minutes=150)).strftime('%Y-%m-%d %H:%M:%S') # fetch last 150 mins
latest_bars_df = api.get_crypto_bars(
SYMBOL,
tradeapi. টাইমফ্রেম.মিনিট,
start=start_fetch_dt, # Alpaca needs ISO format with T
# end defaults to now
limit=150 # fetch a bit more than needed for features
).df
if latest_bars_df.empty or len(latest_bars_df) < 35: # Min needed for SMA_30 + some buffer
print("Not enough recent bars to calculate features.")
return None
featured_bars = create_features(latest_bars_df.copy())
if featured_bars.empty:
print("Failed to create features for latest bars.")
return None
# Return only the features for the most recent complete bar
return featured_bars[feature_cols].iloc[-1:] # Return as DataFrame
def check_and_place_trade():
global model, feature_cols # Ensure these are accessible
if model is None or feature_cols is None:
print("Model not trained or feature columns not defined. Skipping trade check.")
return
print(f"\n{datetime.now()}: Checking for trading signal...")
current_features_df = get_latest_bar_features()
if current_features_df is None or current_features_df.empty:
print("Could not get features for the latest bar.")
return
# Ensure columns are in the same order as during training
current_features_df = current_features_df[feature_cols]
prediction = model.predict(current_features_df)
signal = prediction[0] # 0 for down/neutral, 1 for up
print(f"Raw features for prediction: {current_features_df.iloc[0].to_dict()}")
print(f"Model prediction: {signal}")
try:
positions = api.list_positions()
btc_position = next((p for p in positions if p.symbol == SYMBOL), None)
current_price_info = api.get_latest_crypto_quote(SYMBOL) # Use quote for more current price
current_price = (current_price_info.ap + current_price_info.bp) / 2 # Mid price
if not current_price:
print("Could not get current price for BTC/USD.")
return
if signal == 1: # Predicted UP - Potential BUY
if btc_position is None or float(btc_position.qty) == 0:
qty_to_buy = TRADE_QTY_USD / current_price
print(f"BUY signal. Attempting to buy {qty_to_buy:.6f} {SYMBOL} at ~${current_price:.2f}")
api.submit_order(
symbol=SYMBOL,
qty=round(qty_to_buy, 6), # Alpaca crypto needs precision
side='buy',
type='market',
time_in_force='gtc' # Good 'til canceled
)
print("BUY order submitted.")
else:
print(f"BUY signal, but already have a position of {btc_position.qty} {SYMBOL}. Holding.")
elif signal == 0: # Predicted DOWN/NEUTRAL - Potential SELL
if btc_position and float(btc_position.qty) > 0:
qty_to_sell = float(btc_position.qty) # Sell entire position
print(f"SELL signal. Attempting to sell {qty_to_sell:.6f} {SYMBOL} at ~${current_price:.2f}")
api.submit_order(
symbol=SYMBOL,
qty=round(qty_to_sell, 6),
side='sell',
type='market',
time_in_force='gtc'
)
print("SELL order submitted.")
else:
print("SELL signal, but no open position to sell. Doing nothing.")
else:
print("Neutral signal or unrecognized signal. Doing nothing.")
except Exception as e:
print(f"Error during trade execution: {e}")
# Main loop (very basic scheduler)
# For a robust bot, use APScheduler or run it in a more managed environment (e.g., cloud server with cron)
if __name__ == "__main__" and model is not None: # Ensure model is trained
# This is a simplified loop. In a real bot, you'd schedule this.
# For example, using APScheduler to run exactly at the start of each minute.
print("Starting dummy trading loop (runs a few times for demo). Press Ctrl+C to stop.")
print("IMPORTANT: This is for PAPER TRADING ONLY.")
print(f"Will use model: {type(model).__name__} and features: {feature_cols}")
try:
# Initial run
check_and_place_trade()
for i in range(5): # Run for a few iterations for demo
# Wait for the next minute (approximately)
# A more precise scheduler (like APScheduler) is better for live trading
time.sleep(60)
check_and_place_trade()
except KeyboardInterrupt:
print("Trading loop stopped by user.")
except Exception as e:
print(f"An error occurred in the trading loop: {e}")
else:
if model is None:
print("Model is not trained. Cannot start trading loop.")
Step 7: Risk Management (Conceptual)
Position Sizing: Don't risk too much on a single trade (e.g., TRADE_QTY_USD should be a small % of your paper capital).
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.
# Example of a market buy order with a trailing stop-loss
# api.submit_order(
# symbol=SYMBOL,
# qty=qty_to_buy,
# side='buy',
# type='market',
# time_in_force='day',
# trail_percent='1.5' # Trail stop loss 1.5% below high water mark
# )
Take-Profit: Automatically sell if the price moves in your favor by a certain amount.
Max Drawdown: If your total capital drops by X%, stop trading and re-evaluate.
Step 8: Deployment & Monitoring
Server: Run your bot on a reliable machine or cloud server (AWS EC2, Google Cloud, etc.) that's always on.
Scheduling: Use cron (Linux) or APScheduler (Python library) to run your check_and_place_trade function every minute precisely.
Logging: Log every decision, trade, error, and API response.
Monitoring: Track P&L, number of trades, win rate, errors, API connectivity, etc. Set up alerts.
Very Important Next Steps & Considerations:
PAPER TRADE EXTENSIVELY. I cannot stress this enough.
Improve Feature Engineering: This is key. Explore more advanced features, time-series properties, etc.
Hyperparameter Tuning: Use GridSearchCV or RandomizedSearchCV (or Optuna/Hyperopt) to find optimal model parameters.
Robust Backtesting: Implement a proper event-driven backtester that accounts for slippage and realistic transaction costs.
Walk-Forward Optimization: Markets change. Periodically re-train your model on newer data and test on out-of-sample data that follows.
Understand Alpaca's Crypto Trading Rules: Fees (currently 0.3% for taker/maker on basic accounts), minimum order sizes, precision for quantities.
Error Handling & Resilience: Your bot needs to handle API errors, network issues, unexpected data, etc.
The 1-Minute Problem:
Noise: Extremely high noise-to-signal ratio.
Latency: Your reaction time vs. market speed.
Transaction Costs: Can quickly kill profitability on small moves. Your average win needs to be significantly larger than costs.
Alternative ML Approaches:
Reinforcement Learning: Can learn trading policies directly. Very complex.
LSTMs/Transformers: For sequence modeling, but require significant data and expertise.
Simplicity First: Start with a very simple model and strategy. Complexity doesn't always mean better results and makes debugging harder.
This is a massive undertaking. Be patient, learn continuously, and manage your risk (even paper money risk, as it builds habits). Good luck!
AttributeError Traceback (most recent call last) Cell In[5], line 61 59 # Example usage: 60 symbol = "BTC/USD" # Alpaca uses "BTC/USD" for crypto pairs ---> 61 timeframe = tradeapi. টাইমফ্রেম.মিনিট # or "1Min" for older SDK versions 62 # Fetch last 30 days of data for example 63 start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
AttributeError: module 'alpaca_trade_api' has no attribute 'টাইমফ্রেম'. Can you fix it?
import warnings
warnings.filterwarnings('ignore')
import os
import alpaca_trade_api as tradeapi
import pandas as pd
from datetime import datetime, timedelta
import time
# For Paper Trading
os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets'
# Replace with your actual keys or set them as environment variables
# Make sure these are set correctly if you haven't already
os.environ['APCA_API_KEY_ID'] = 'your API KEY'
os.environ['APCA_API_SECRET_KEY'] = 'your SECRET KEY'
# os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID'
# os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'
API_KEY = os.getenv('APCA_API_KEY_ID')
API_SECRET = os.getenv('APCA_API_SECRET_KEY')
BASE_URL = os.getenv('APCA_API_BASE_URL')
# Ensure API_KEY and API_SECRET are not None
if not API_KEY or not API_SECRET:
raise ValueError("API_KEY or API_SECRET not set. Please set them as environment variables or directly in the script.")
api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')
print("Attempting to connect to Alpaca Paper Trading...")
try:
account = api.get_account()
print(f"Successfully connected. Account status: {account.status}")
except Exception as e:
print(f"Failed to connect or get account info: {e}")
# Exit or handle if connection fails
exit()
def fetch_data(symbol, timeframe_enum, start_date_str, end_date_str=None): # Changed timeframe to timeframe_enum
"""Fetches historical crypto data from Alpaca."""
if end_date_str is None:
end_date_str = datetime.now().strftime('%Y-%m-%d')
all_bars = []
start_dt = pd.to_datetime(start_date_str, utc=True)
end_dt = pd.to_datetime(end_date_str, utc=True)
current_start = start_dt
while current_start < end_dt:
# Calculate chunk_end, ensuring it doesn't exceed end_dt
# For 1-minute data, 10000 bars is approx 6.94 days. Let's use 6 days to be safe.
chunk_end_candidate = current_start + timedelta(days=6)
chunk_end = min(chunk_end_candidate, end_dt)
print(f"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}")
try:
bars = api.get_crypto_bars(
symbol,
timeframe_enum, # Use the passed enum
start=current_start.isoformat(),
end=chunk_end.isoformat(), # Ensure end is also passed
limit=10000
).df
except Exception as e:
print(f"Error fetching data chunk: {e}")
# Decide how to handle: break, retry, or skip chunk
current_start = chunk_end # Move to next potential period
time.sleep(5) # Wait longer if an error occurred
continue
if bars.empty:
print(f"No data found for chunk starting {current_start.isoformat()}")
if current_start >= end_dt and not all_bars: # if first chunk and no data
break
current_start = chunk_end # Move to next chunk period
time.sleep(1) # Be nice to the API
continue
all_bars.append(bars)
if not bars.index.empty:
# Move current_start to the timestamp of the last bar fetched + 1 unit of timeframe
# For TimeFrame.Minute, this is +1 minute
if timeframe_enum == tradeapi.TimeFrame.Minute:
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(minutes=1)
elif timeframe_enum == tradeapi.TimeFrame.Hour:
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(hours=1)
# Add other timeframes if needed
else: # Default for Day or others
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(days=1)
# Ensure current_start does not go beyond end_dt excessively in the loop condition
if current_start > end_dt and chunk_end_candidate >= end_dt :
break
else: # Should not happen if bars is not empty, but as a safeguard
current_start = chunk_end
time.sleep(1) # Respect API rate limits
if not all_bars:
print("No data fetched. Check your date range, symbol, or API connection.")
return pd.DataFrame()
df = pd.concat(all_bars)
df = df[~df.index.duplicated(keep='first')] # Remove potential duplicates
df = df.sort_index() # Ensure chronological order
# Filter to ensure we are within the originally requested start_dt and end_dt precisely
df = df[(df.index >= start_dt) & (df.index <= end_dt)]
return df
# Example usage:
symbol = "BTC/USD" # Alpaca uses "BTC/USD" for crypto pairs
# CORRECTED TIMEFRAME:
timeframe_to_use = tradeapi.TimeFrame.Minute # Corrected
# Alternative: timeframe_to_use = "1Min" (as a string, for some SDK versions or if TimeFrame enum is problematic)
# Fetch last 7 days of data for example (a smaller range for quicker testing)
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
# end_date = datetime.now().strftime('%Y-%m-%d') # Fetches up to now by default if None
btc_data = fetch_data(symbol, timeframe_to_use, start_date)
if not btc_data.empty:
print(f"\nFetched {len(btc_data)} rows of {symbol} data.")
print("Sample data (first 5 rows):")
print(btc_data.head())
print("\nSample data (last 5 rows):")
print(btc_data.tail())
else:
print("Failed to fetch data or no data available for the period.")
Attempting to connect to Alpaca Paper Trading...
Successfully connected. Account status: ACTIVE
Fetching data from 2025-05-02T00:00:00+00:00 to 2025-05-08T00:00:00+00:00
Fetching data from 2025-05-08T00:01:00+00:00 to 2025-05-09T00:00:00+00:00
Fetched 4349 rows of BTC/USD data.
Sample data (first 5 rows):
close high low trade_count \
timestamp
2025-05-02 00:00:00+00:00 96599.4770 96599.4770 96599.4770 0
2025-05-02 00:02:00+00:00 96650.8100 96650.8100 96650.8100 0
2025-05-02 00:03:00+00:00 96588.0115 96588.0115 96588.0115 0
2025-05-02 00:05:00+00:00 96639.6100 96639.6100 96639.6100 0
2025-05-02 00:08:00+00:00 96521.6100 96584.7550 96521.6100 0
open volume vwap symbol
timestamp
2025-05-02 00:00:00+00:00 96599.4770 0.0 96599.4770 BTC/USD
2025-05-02 00:02:00+00:00 96650.8100 0.0 96650.8100 BTC/USD
2025-05-02 00:03:00+00:00 96588.0115 0.0 96588.0115 BTC/USD
2025-05-02 00:05:00+00:00 96639.6100 0.0 96639.6100 BTC/USD
2025-05-02 00:08:00+00:00 96584.7550 0.0 96553.1825 BTC/USD
Sample data (last 5 rows):
close high low trade_count \
timestamp
2025-05-08 23:55:00+00:00 103121.6815 103121.6815 103121.6815 0
2025-05-08 23:56:00+00:00 103170.6190 103170.6190 103170.6190 0
2025-05-08 23:57:00+00:00 103204.4250 103204.4250 103204.4250 0
2025-05-08 23:58:00+00:00 103284.7150 103284.7150 103284.7150 0
2025-05-09 00:00:00+00:00 103206.1315 103267.4635 103196.4550 1
open volume vwap symbol
timestamp
2025-05-08 23:55:00+00:00 103121.6815 0.000000 103121.6815 BTC/USD
2025-05-08 23:56:00+00:00 103170.6190 0.000000 103170.6190 BTC/USD
2025-05-08 23:57:00+00:00 103204.4250 0.000000 103204.4250 BTC/USD
2025-05-08 23:58:00+00:00 103284.7150 0.000000 103284.7150 BTC/USD
2025-05-09 00:00:00+00:00 103230.7000 0.000012 103230.7000 BTC/USD
Key changes:
timeframe = tradeapi.TimeFrame.Minute: This is the crucial fix.
I renamed the timeframe parameter in fetch_data to timeframe_enum to avoid confusion if you were to pass the string version.
Added a check for API_KEY and API_SECRET to ensure they are loaded.
Improved the connection check with a try-except block.
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.
Made the advancement of current_start adaptive to the timeframe_enum.
Final filtering of the concatenated DataFrame to strictly adhere to start_dt and end_dt.
Reduced the default fetch range in the example to 7 days for faster initial testing.
Important: You will also need to make the same correction in Step 6 (Signal Generation & Order Execution) inside the get_latest_bar_features function:
Step 3: Feature Engineering
This is where you create signals for your model. pandas-ta is a great library for this.
import pandas_ta as ta
def create_features(df):
if df.empty or 'close' not in df.columns:
print("DataFrame is empty or 'close' column is missing.")
return df
df.ta.sma(length=10, append=True, col_names=('SMA_10'))
df.ta.sma(length=30, append=True, col_names=('SMA_30'))
df.ta.ema(length=10, append=True, col_names=('EMA_10'))
df.ta.rsi(length=14, append=True, col_names=('RSI_14'))
df.ta.macd(append=True, col_names=('MACD_12_26_9', 'MACDh_12_26_9', 'MACDs_12_26_9'))
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'))
df.ta.atr(length=14, append=True, col_names=('ATR_14'))
# Lagged returns
for lag in [1, 3, 5, 10]:
df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)
# Add more features: volatility, momentum, volume-based if available, etc.
# e.g., log returns, price relative to moving average, etc.
df.dropna(inplace=True) # Remove rows with NaNs created by indicators
return df
if not btc_data.empty:
featured_data = create_features(btc_data.copy()) # Use .copy() to avoid modifying original
print("\nData with features:")
print(featured_data.head())
else:
print("Cannot create features, btc_data is empty.")
featured_data = pd.DataFrame() # ensure it's a DataFrame
--------------------------------------------------------------------------- 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: 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 * 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 * 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 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 ImportError: cannot import name 'NaN' from 'numpy' (C:\Users\micro\anaconda3\envs\alpaca_AI_env\Lib\site-packages\numpy\__init__.py)
So, we are working on a trading bot. It should be using machine learning to trade BTCUSD in one minute intervals and Alpaca Markets. 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
Step 2 was OK, but in Step 3 I got this an error (check below). 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.
GOOD CODE: import os import alpaca_trade_api as tradeapi import pandas as pd from datetime import datetime, timedelta import time
# For Paper Trading os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets' # Replace with your actual keys or set them as environment variables # Make sure these are set correctly if you haven't already os.environ['APCA_API_KEY_ID'] = 'your API KEY' os.environ['APCA_API_SECRET_KEY'] = 'your SECRET KEY' # os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID' # os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'
API_KEY = os.getenv('APCA_API_KEY_ID') API_SECRET = os.getenv('APCA_API_SECRET_KEY') BASE_URL = os.getenv('APCA_API_BASE_URL')
# Ensure API_KEY and API_SECRET are not None if not API_KEY or not API_SECRET: raise ValueError("API_KEY or API_SECRET not set. Please set them as environment variables or directly in the script.")
api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')
print("Attempting to connect to Alpaca Paper Trading...") try: account = api.get_account() print(f"Successfully connected. Account status: {account.status}") except Exception as e: print(f"Failed to connect or get account info: {e}") # Exit or handle if connection fails exit()
def fetch_data(symbol, timeframe_enum, start_date_str, end_date_str=None): # Changed timeframe to timeframe_enum """Fetches historical crypto data from Alpaca.""" if end_date_str is None: end_date_str = datetime.now().strftime('%Y-%m-%d')
all_bars = []
start_dt = pd.to_datetime(start_date_str, utc=True)
end_dt = pd.to_datetime(end_date_str, utc=True)
current_start = start_dt
while current_start < end_dt:
# Calculate chunk_end, ensuring it doesn't exceed end_dt
# For 1-minute data, 10000 bars is approx 6.94 days. Let's use 6 days to be safe.
chunk_end_candidate = current_start + timedelta(days=6)
chunk_end = min(chunk_end_candidate, end_dt)
print(f"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}")
try:
bars = api.get_crypto_bars(
symbol,
timeframe_enum, # Use the passed enum
start=current_start.isoformat(),
end=chunk_end.isoformat(), # Ensure end is also passed
limit=10000
).df
except Exception as e:
print(f"Error fetching data chunk: {e}")
# Decide how to handle: break, retry, or skip chunk
current_start = chunk_end # Move to next potential period
time.sleep(5) # Wait longer if an error occurred
continue
if bars.empty:
print(f"No data found for chunk starting {current_start.isoformat()}")
if current_start >= end_dt and not all_bars: # if first chunk and no data
break
current_start = chunk_end # Move to next chunk period
time.sleep(1) # Be nice to the API
continue
all_bars.append(bars)
if not bars.index.empty:
# Move current_start to the timestamp of the last bar fetched + 1 unit of timeframe
# For TimeFrame.Minute, this is +1 minute
if timeframe_enum == tradeapi.TimeFrame.Minute:
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(minutes=1)
elif timeframe_enum == tradeapi.TimeFrame.Hour:
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(hours=1)
# Add other timeframes if needed
else: # Default for Day or others
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(days=1)
# Ensure current_start does not go beyond end_dt excessively in the loop condition
if current_start > end_dt and chunk_end_candidate >= end_dt :
break
else: # Should not happen if bars is not empty, but as a safeguard
current_start = chunk_end
time.sleep(1) # Respect API rate limits
if not all_bars:
print("No data fetched. Check your date range, symbol, or API connection.")
return pd.DataFrame()
df = pd.concat(all_bars)
df = df[~df.index.duplicated(keep='first')] # Remove potential duplicates
df = df.sort_index() # Ensure chronological order
# Filter to ensure we are within the originally requested start_dt and end_dt precisely
df = df[(df.index >= start_dt) & (df.index <= end_dt)]
return df
# Example usage: symbol = "BTC/USD" # Alpaca uses "BTC/USD" for crypto pairs # CORRECTED TIMEFRAME: timeframe_to_use = tradeapi.TimeFrame.Minute # Corrected # Alternative: timeframe_to_use = "1Min" (as a string, for some SDK versions or if TimeFrame enum is problematic)
# Fetch last 7 days of data for example (a smaller range for quicker testing) start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d') # end_date = datetime.now().strftime('%Y-%m-%d') # Fetches up to now by default if None
btc_data = fetch_data(symbol, timeframe_to_use, start_date)
if not btc_data.empty: print(f"\nFetched {len(btc_data)} rows of {symbol} data.") print("Sample data (first 5 rows):") print(btc_data.head()) print("\nSample data (last 5 rows):") print(btc_data.tail()) else: print("Failed to fetch data or no data available for the period.")
BAD CODE: import pandas_ta as ta
def create_features(df): if df.empty or 'close' not in df.columns: print("DataFrame is empty or 'close' column is missing.") return df
df.ta.sma(length=10, append=True, col_names=('SMA_10'))
df.ta.sma(length=30, append=True, col_names=('SMA_30'))
df.ta.ema(length=10, append=True, col_names=('EMA_10'))
df.ta.rsi(length=14, append=True, col_names=('RSI_14'))
df.ta.macd(append=True, col_names=('MACD_12_26_9', 'MACDh_12_26_9', 'MACDs_12_26_9'))
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'))
df.ta.atr(length=14, append=True, col_names=('ATR_14'))
# Lagged returns
for lag in [1, 3, 5, 10]:
df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)
# Add more features: volatility, momentum, volume-based if available, etc.
# e.g., log returns, price relative to moving average, etc.
df.dropna(inplace=True) # Remove rows with NaNs created by indicators
return df
if not btc_data.empty: featured_data = create_features(btc_data.copy()) # Use .copy() to avoid modifying original print("\nData with features:") print(featured_data.head()) else: print("Cannot create features, btc_data is empty.") featured_data = pd.DataFrame() # ensure it's a DataFrame
ERROR: 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:
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 *
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 *
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
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
ImportError: cannot import name 'NaN' from 'numpy' (C:\Users\micro\anaconda3\envs\alpaca_AI_env\Lib\site-packages\numpy_init_.py)
import warnings
warnings.filterwarnings('ignore')
import os
import alpaca_trade_api as tradeapi
import pandas as pd
from datetime import datetime, timedelta
import time
# For Paper Trading
os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets'
# Replace with your actual keys or set them as environment variables
# Make sure these are set correctly if you haven't already
os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID'
os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'
# os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID'
# os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'
API_KEY = os.getenv('APCA_API_KEY_ID')
API_SECRET = os.getenv('APCA_API_SECRET_KEY')
BASE_URL = os.getenv('APCA_API_BASE_URL')
# Ensure API_KEY and API_SECRET are not None
if not API_KEY or not API_SECRET:
raise ValueError("API_KEY or API_SECRET not set. Please set them as environment variables or directly in the script.")
api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')
print("Attempting to connect to Alpaca Paper Trading...")
try:
account = api.get_account()
print(f"Successfully connected. Account status: {account.status}")
except Exception as e:
print(f"Failed to connect or get account info: {e}")
# Exit or handle if connection fails
exit()
def fetch_data(symbol, timeframe_enum, start_date_str, end_date_str=None): # Changed timeframe to timeframe_enum
"""Fetches historical crypto data from Alpaca."""
if end_date_str is None:
end_date_str = datetime.now().strftime('%Y-%m-%d')
all_bars = []
start_dt = pd.to_datetime(start_date_str, utc=True)
end_dt = pd.to_datetime(end_date_str, utc=True)
current_start = start_dt
while current_start < end_dt:
# Calculate chunk_end, ensuring it doesn't exceed end_dt
# For 1-minute data, 10000 bars is approx 6.94 days. Let's use 6 days to be safe.
chunk_end_candidate = current_start + timedelta(days=6)
chunk_end = min(chunk_end_candidate, end_dt)
print(f"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}")
try:
bars = api.get_crypto_bars(
symbol,
timeframe_enum, # Use the passed enum
start=current_start.isoformat(),
end=chunk_end.isoformat(), # Ensure end is also passed
limit=10000
).df
except Exception as e:
print(f"Error fetching data chunk: {e}")
# Decide how to handle: break, retry, or skip chunk
current_start = chunk_end # Move to next potential period
time.sleep(5) # Wait longer if an error occurred
continue
if bars.empty:
print(f"No data found for chunk starting {current_start.isoformat()}")
if current_start >= end_dt and not all_bars: # if first chunk and no data
break
current_start = chunk_end # Move to next chunk period
time.sleep(1) # Be nice to the API
continue
all_bars.append(bars)
if not bars.index.empty:
# Move current_start to the timestamp of the last bar fetched + 1 unit of timeframe
# For TimeFrame.Minute, this is +1 minute
if timeframe_enum == tradeapi.TimeFrame.Minute:
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(minutes=1)
elif timeframe_enum == tradeapi.TimeFrame.Hour:
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(hours=1)
# Add other timeframes if needed
else: # Default for Day or others
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(days=1)
# Ensure current_start does not go beyond end_dt excessively in the loop condition
if current_start > end_dt and chunk_end_candidate >= end_dt :
break
else: # Should not happen if bars is not empty, but as a safeguard
current_start = chunk_end
time.sleep(1) # Respect API rate limits
if not all_bars:
print("No data fetched. Check your date range, symbol, or API connection.")
return pd.DataFrame()
df = pd.concat(all_bars)
df = df[~df.index.duplicated(keep='first')] # Remove potential duplicates
df = df.sort_index() # Ensure chronological order
# Filter to ensure we are within the originally requested start_dt and end_dt precisely
df = df[(df.index >= start_dt) & (df.index <= end_dt)]
return df
# Example usage:
symbol = "BTC/USD" # Alpaca uses "BTC/USD" for crypto pairs
# CORRECTED TIMEFRAME:
timeframe_to_use = tradeapi.TimeFrame.Minute # Corrected
# Alternative: timeframe_to_use = "1Min" (as a string, for some SDK versions or if TimeFrame enum is problematic)
# Fetch last 7 days of data for example (a smaller range for quicker testing)
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
# end_date = datetime.now().strftime('%Y-%m-%d') # Fetches up to now by default if None
btc_data = fetch_data(symbol, timeframe_to_use, start_date)
if not btc_data.empty:
print(f"\nFetched {len(btc_data)} rows of {symbol} data.")
print("Sample data (first 5 rows):")
print(btc_data.head())
print("\nSample data (last 5 rows):")
print(btc_data.tail())
else:
print("Failed to fetch data or no data available for the period.")
Attempting to connect to Alpaca Paper Trading...
Successfully connected. Account status: ACTIVE
Fetching data from 2025-05-29T00:00:00+00:00 to 2025-06-04T00:00:00+00:00
Fetching data from 2025-06-04T00:01:00+00:00 to 2025-06-05T00:00:00+00:00
Fetched 5587 rows of BTC/USD data.
Sample data (first 5 rows):
close high low trade_count \
timestamp
2025-05-29 00:00:00+00:00 107854.950 107854.9500 107797.350 0
2025-05-29 00:01:00+00:00 107730.050 107786.4545 107730.050 0
2025-05-29 00:02:00+00:00 107784.335 107784.3350 107784.335 0
2025-05-29 00:03:00+00:00 107749.815 107749.8150 107749.815 0
2025-05-29 00:04:00+00:00 107669.645 107680.6050 107524.300 0
open volume vwap symbol
timestamp
2025-05-29 00:00:00+00:00 107853.9450 0.0 107826.15000 BTC/USD
2025-05-29 00:01:00+00:00 107786.4545 0.0 107758.25225 BTC/USD
2025-05-29 00:02:00+00:00 107784.3350 0.0 107784.33500 BTC/USD
2025-05-29 00:03:00+00:00 107749.8150 0.0 107749.81500 BTC/USD
2025-05-29 00:04:00+00:00 107680.6050 0.0 107602.45250 BTC/USD
Sample data (last 5 rows):
close high low trade_count \
timestamp
2025-06-04 23:54:00+00:00 104807.747 104845.250 104807.747 0
2025-06-04 23:55:00+00:00 104816.255 104816.255 104782.185 0
2025-06-04 23:57:00+00:00 104744.600 104744.600 104744.600 0
2025-06-04 23:58:00+00:00 104732.050 104739.899 104732.050 0
2025-06-04 23:59:00+00:00 104748.560 104758.500 104657.600 1
open volume vwap symbol
timestamp
2025-06-04 23:54:00+00:00 104845.250 0.000000 104826.4985 BTC/USD
2025-06-04 23:55:00+00:00 104782.185 0.000000 104799.2200 BTC/USD
2025-06-04 23:57:00+00:00 104744.600 0.000000 104744.6000 BTC/USD
2025-06-04 23:58:00+00:00 104739.899 0.000000 104735.9745 BTC/USD
2025-06-04 23:59:00+00:00 104657.600 0.000072 104657.6000 BTC/USD
Step 3: Feature Engineering
This is where you create signals for your model. pandas-ta is a great library for this.
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).
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.
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.
Important Notes for finta:
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.
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.
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.
Here's how you can modify your create_features function using finta:
First, make sure to install finta:
pip install finta
Then, replace your BAD CODE section with the following:
pip install finta
Collecting finta Downloading finta-1.3-py3-none-any.whl.metadata (6.4 kB) Requirement already satisfied: numpy in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from finta) (2.2.5) Requirement already satisfied: pandas in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from finta) (2.2.3) 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) Requirement already satisfied: pytz>=2020.1 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from pandas->finta) (2024.1) Requirement already satisfied: tzdata>=2022.7 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from pandas->finta) (2024.2) 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) Downloading finta-1.3-py3-none-any.whl (29 kB) Installing collected packages: finta Successfully installed finta-1.3 Note: you may need to restart the kernel to use updated packages.
import pandas as pd
from finta import TA # Import the TA module from finta
import numpy as np # For np.nan if needed for BBP/BBB calculation edge cases
# This is your existing GOOD CODE (data fetching part)
# I'm including it here just for completeness of the script context
# ... (paste your GOOD CODE here, from 'import os' down to the btc_data fetching and printing)
# For brevity, I'll assume btc_data is already fetched as per your GOOD CODE.
# --- MODIFIED create_features FUNCTION USING finta ---
def create_features_finta(df):
if df.empty or 'close' not in df.columns:
print("DataFrame is empty or 'close' column is missing.")
return df.copy() # Return a copy to avoid modifying original if it's returned early
# Ensure required columns for some indicators are present
required_ohlc = ['open', 'high', 'low', 'close']
if not all(col in df.columns for col in required_ohlc):
print(f"DataFrame is missing one or more required columns: {required_ohlc}")
# Attempt to continue if only 'close' is needed for some indicators,
# but ATR and BBands might fail or need to be skipped.
# For now, let's be strict for this example.
# You might want to add more nuanced handling later.
# return df.copy() # Or raise an error
# SMA (Simple Moving Average)
df['SMA_10'] = TA.SMA(df, period=10, column='close')
df['SMA_30'] = TA.SMA(df, period=30, column='close')
# EMA (Exponential Moving Average)
df['EMA_10'] = TA.EMA(df, period=10, column='close') # finta's EMA defaults to 'close' if not specified
# RSI (Relative Strength Index)
df['RSI_14'] = TA.RSI(df, period=14, column='close')
# MACD (Moving Average Convergence Divergence)
# TA.MACD returns a DataFrame with 'MACD' and 'SIGNAL' columns
macd_df = TA.MACD(df, period_fast=12, period_slow=26, signal=9, column='close')
df['MACD_12_26_9'] = macd_df['MACD']
df['MACDs_12_26_9'] = macd_df['SIGNAL'] # This is the signal line
df['MACDh_12_26_9'] = df['MACD_12_26_9'] - df['MACDs_12_26_9'] # Histogram is MACD - Signal
# Bollinger Bands (BBANDS)
# TA.BBANDS returns a DataFrame with 'BB_UPPER', 'BB_MIDDLE', 'BB_LOWER'
# It uses 'high', 'low', 'close' columns by default.
if all(col in df.columns for col in ['high', 'low', 'close']):
bb_df = TA.BBANDS(df, period=20, std_multiplier=2.0) # std_multiplier is for standard deviations
df['BBL_20_2.0'] = bb_df['BB_LOWER']
df['BBM_20_2.0'] = bb_df['BB_MIDDLE'] # This is the SMA(20)
df['BBU_20_2.0'] = bb_df['BB_UPPER']
# Calculate BBB (Bollinger Band Width) and BBP (Bollinger Band Percent B) manually
# BBB = (Upper Band - Lower Band) / Middle Band
df['BBB_20_2.0'] = (df['BBU_20_2.0'] - df['BBL_20_2.0']) / df['BBM_20_2.0']
# BBP = (Price - Lower Band) / (Upper Band - Lower Band)
# Handle division by zero if Upper Band == Lower Band (can happen in flat markets or with very short periods)
band_range = df['BBU_20_2.0'] - df['BBL_20_2.0']
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
else:
print("Skipping Bollinger Bands calculation as 'high', 'low', 'close' columns are not all present.")
for col_name in ['BBL_20_2.0', 'BBM_20_2.0', 'BBU_20_2.0', 'BBB_20_2.0', 'BBP_20_2.0']:
df[col_name] = np.nan
# ATR (Average True Range)
# TA.ATR requires 'high', 'low', 'close' columns.
if all(col in df.columns for col in ['high', 'low', 'close']):
df['ATR_14'] = TA.ATR(df, period=14)
else:
print("Skipping ATR calculation as 'high', 'low', 'close' columns are not all present.")
df['ATR_14'] = np.nan
# Lagged returns (this part is standard pandas, no change needed)
for lag in [1, 3, 5, 10]:
df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)
df.dropna(inplace=True) # Remove rows with NaNs created by indicators
return df
# --- End of MODIFIED create_features FUNCTION ---
# Your existing code to call the function (make sure btc_data is defined from GOOD CODE)
if 'btc_data' in locals() and not btc_data.empty:
# It's good practice to lowercase column names if the library expects it,
# though Alpaca's .df usually does this. Let's be explicit.
btc_data.columns = [col.lower() for col in btc_data.columns]
featured_data = create_features_finta(btc_data.copy()) # Use .copy() to avoid modifying original
print("\nData with features (using finta):")
if not featured_data.empty:
print(featured_data.head())
print(f"\nNumber of rows in featured_data: {len(featured_data)}")
else:
print("Featured data is empty after processing.")
elif 'btc_data' in locals() and btc_data.empty:
print("Cannot create features, btc_data is empty.")
featured_data = pd.DataFrame() # ensure it's a DataFrame
else:
print("btc_data not found. Please run the data fetching part first.")
featured_data = pd.DataFrame()
Data with features (using finta):
close high low trade_count \
timestamp
2025-05-29 00:40:00+00:00 107891.3515 107891.3515 107891.3515 0
2025-05-29 00:42:00+00:00 107819.7350 107819.7350 107819.7350 0
2025-05-29 00:45:00+00:00 107887.0150 107887.0150 107887.0150 0
2025-05-29 00:47:00+00:00 107962.3700 107962.3700 107962.3700 0
2025-05-29 00:48:00+00:00 108009.4200 108009.4200 108009.4200 0
open volume vwap symbol \
timestamp
2025-05-29 00:40:00+00:00 107891.3515 0.0 107891.3515 BTC/USD
2025-05-29 00:42:00+00:00 107819.7350 0.0 107819.7350 BTC/USD
2025-05-29 00:45:00+00:00 107887.0150 0.0 107887.0150 BTC/USD
2025-05-29 00:47:00+00:00 107962.3700 0.0 107962.3700 BTC/USD
2025-05-29 00:48:00+00:00 108009.4200 0.0 108009.4200 BTC/USD
SMA_10 SMA_30 ... BBL_20_2.0 \
timestamp ...
2025-05-29 00:40:00+00:00 107940.20850 107797.621567 ... 107628.818315
2025-05-29 00:42:00+00:00 107922.53925 107796.447733 ... 107677.261002
2025-05-29 00:45:00+00:00 107904.22360 107801.679900 ... 107707.198595
2025-05-29 00:47:00+00:00 107901.82610 107807.614400 ... 107744.779959
2025-05-29 00:48:00+00:00 107911.29420 107816.267900 ... 107774.213800
BBM_20_2.0 BBU_20_2.0 BBB_20_2.0 \
timestamp
2025-05-29 00:40:00+00:00 107862.704675 108096.591035 0.004337
2025-05-29 00:42:00+00:00 107874.226550 108071.192098 0.003652
2025-05-29 00:45:00+00:00 107884.072300 108060.946005 0.003279
2025-05-29 00:47:00+00:00 107897.923525 108051.067091 0.002839
2025-05-29 00:48:00+00:00 107912.089525 108049.965250 0.002555
BBP_20_2.0 ATR_14 return_1m return_3m \
timestamp
2025-05-29 00:40:00+00:00 0.561241 76.081607 0.000338 -0.000014
2025-05-29 00:42:00+00:00 0.361672 76.737786 -0.000664 -0.001301
2025-05-29 00:45:00+00:00 0.508319 76.117071 0.000624 0.000297
2025-05-29 00:47:00+00:00 0.710412 78.004607 0.000698 0.000658
2025-05-29 00:48:00+00:00 0.852965 77.682679 0.000436 0.001759
return_5m return_10m
timestamp
2025-05-29 00:40:00+00:00 -0.000097 0.000023
2025-05-29 00:42:00+00:00 -0.001051 -0.001636
2025-05-29 00:45:00+00:00 -0.000054 -0.001695
2025-05-29 00:47:00+00:00 0.000020 -0.000222
2025-05-29 00:48:00+00:00 0.001432 0.000877
[5 rows x 25 columns]
Number of rows in featured_data: 5558
To make this a complete, runnable script, combine your "GOOD CODE" with the modified "BAD CODE" section:
import os
import alpaca_trade_api as tradeapi
import pandas as pd
from datetime import datetime, timedelta
import time
from finta import TA # Import for technical indicators
import numpy as np # For np.nan
# For Paper Trading
os.environ['APCA_API_BASE_URL'] = 'https://paper-api.alpaca.markets'
# Replace with your actual keys or set them as environment variables
# Make sure these are set correctly if you haven't already
# IMPORTANT: Replace with your actual keys for testing
#os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID' # <--- REPLACE
#os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY' # <--- REPLACE
os.environ['APCA_API_KEY_ID'] = 'YOUR_PAPER_API_KEY_ID'
os.environ['APCA_API_SECRET_KEY'] = 'YOUR_PAPER_API_SECRET_KEY'
API_KEY = os.getenv('APCA_API_KEY_ID')
API_SECRET = os.getenv('APCA_API_SECRET_KEY')
BASE_URL = os.getenv('APCA_API_BASE_URL')
# Ensure API_KEY and API_SECRET are not None
if not API_KEY or not API_SECRET or API_KEY == 'YOUR_PAPER_API_KEY_ID':
raise ValueError("API_KEY or API_SECRET not set or still default. Please set them as environment variables or directly in the script.")
api = tradeapi.REST(API_KEY, API_SECRET, BASE_URL, api_version='v2')
print("Attempting to connect to Alpaca Paper Trading...")
try:
account = api.get_account()
print(f"Successfully connected. Account status: {account.status}")
except Exception as e:
print(f"Failed to connect or get account info: {e}")
exit()
def fetch_data(symbol, timeframe_enum, start_date_str, end_date_str=None):
if end_date_str is None:
end_date_str = datetime.now().strftime('%Y-%m-%d')
all_bars = []
start_dt = pd.to_datetime(start_date_str, utc=True)
end_dt = pd.to_datetime(end_date_str, utc=True)
current_start = start_dt
while current_start < end_dt:
chunk_end_candidate = current_start + timedelta(days=6)
chunk_end = min(chunk_end_candidate, end_dt)
print(f"Fetching data from {current_start.isoformat()} to {chunk_end.isoformat()}")
try:
bars = api.get_crypto_bars(
symbol,
timeframe_enum,
start=current_start.isoformat(),
end=chunk_end.isoformat(),
limit=10000
).df
except Exception as e:
print(f"Error fetching data chunk: {e}")
current_start = chunk_end
time.sleep(5)
continue
if bars.empty:
print(f"No data found for chunk starting {current_start.isoformat()}")
if current_start >= end_dt and not all_bars:
break
current_start = chunk_end
time.sleep(1)
continue
all_bars.append(bars)
if not bars.index.empty:
if timeframe_enum == tradeapi.TimeFrame.Minute:
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(minutes=1)
elif timeframe_enum == tradeapi.TimeFrame.Hour:
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(hours=1)
else:
current_start = bars.index[-1].to_pydatetime() + pd.Timedelta(days=1)
if current_start > end_dt and chunk_end_candidate >= end_dt :
break
else:
current_start = chunk_end
time.sleep(1)
if not all_bars:
print("No data fetched. Check your date range, symbol, or API connection.")
return pd.DataFrame()
df = pd.concat(all_bars)
df = df[~df.index.duplicated(keep='first')]
df = df.sort_index()
df = df[(df.index >= start_dt) & (df.index <= end_dt)]
return df
# Example usage:
symbol = "BTC/USD"
timeframe_to_use = tradeapi.TimeFrame.Minute
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
btc_data = fetch_data(symbol, timeframe_to_use, start_date)
if not btc_data.empty:
print(f"\nFetched {len(btc_data)} rows of {symbol} data.")
print("Sample data (first 5 rows):")
print(btc_data.head())
print("\nSample data (last 5 rows):")
print(btc_data.tail())
# Ensure column names are lowercase for finta
btc_data.columns = [col.lower() for col in btc_data.columns]
else:
print("Failed to fetch data or no data available for the period.")
btc_data = pd.DataFrame() # ensure it's a DataFrame for subsequent steps
# --- REVISED create_features FUNCTION USING finta ---
def create_features(df_input): # Renamed input to df_input to avoid clash with outer scope df
df = df_input.copy() # Work on a copy
if df.empty or 'close' not in df.columns:
print("DataFrame is empty or 'close' column is missing.")
return df # Return the empty or deficient (but copied) DataFrame
# Ensure required columns for some indicators are present
required_ohlc = ['open', 'high', 'low', 'close']
has_ohlc = all(col in df.columns for col in required_ohlc)
# SMA (Simple Moving Average)
df['SMA_10'] = TA.SMA(df, period=10, column='close')
df['SMA_30'] = TA.SMA(df, period=30, column='close')
# EMA (Exponential Moving Average)
df['EMA_10'] = TA.EMA(df, period=10, column='close')
# RSI (Relative Strength Index)
df['RSI_14'] = TA.RSI(df, period=14, column='close')
# MACD (Moving Average Convergence Divergence)
macd_df = TA.MACD(df, period_fast=12, period_slow=26, signal=9, column='close')
df['MACD_12_26_9'] = macd_df['MACD']
df['MACDs_12_26_9'] = macd_df['SIGNAL']
df['MACDh_12_26_9'] = df['MACD_12_26_9'] - df['MACDs_12_26_9']
# Bollinger Bands (BBANDS)
if has_ohlc:
bb_df = TA.BBANDS(df, period=20, std_multiplier=2.0)
df['BBL_20_2.0'] = bb_df['BB_LOWER']
df['BBM_20_2.0'] = bb_df['BB_MIDDLE']
df['BBU_20_2.0'] = bb_df['BB_UPPER']
df['BBB_20_2.0'] = (df['BBU_20_2.0'] - df['BBL_20_2.0']) / df['BBM_20_2.0']
band_range = df['BBU_20_2.0'] - df['BBL_20_2.0']
df['BBP_20_2.0'] = np.where(band_range == 0, 0.5, (df['close'] - df['BBL_20_2.0']) / band_range)
else:
print("Skipping Bollinger Bands calculation as 'open', 'high', 'low', 'close' columns are not all present.")
for col_name in ['BBL_20_2.0', 'BBM_20_2.0', 'BBU_20_2.0', 'BBB_20_2.0', 'BBP_20_2.0']:
df[col_name] = np.nan
# ATR (Average True Range)
if has_ohlc:
df['ATR_14'] = TA.ATR(df, period=14)
else:
print("Skipping ATR calculation as 'open', 'high', 'low', 'close' columns are not all present.")
df['ATR_14'] = np.nan
# Lagged returns
for lag in [1, 3, 5, 10]:
df[f'return_{lag}m'] = df['close'].pct_change(periods=lag)
df.dropna(inplace=True)
return df
# --- End of REVISED create_features FUNCTION ---
# Call the feature creation function
if not btc_data.empty:
featured_data = create_features(btc_data) # No need for .copy() here as create_features does it
print("\nData with features (using finta):")
if not featured_data.empty:
print(featured_data.head())
print(f"\nNumber of rows in featured_data: {len(featured_data)}")
print("\nColumns in featured_data:")
print(featured_data.columns.tolist())
else:
print("Featured data is empty after processing and NaN removal.")
else:
print("Cannot create features, btc_data is empty.")
featured_data = pd.DataFrame()
Attempting to connect to Alpaca Paper Trading...
Successfully connected. Account status: ACTIVE
Fetching data from 2025-05-29T00:00:00+00:00 to 2025-06-04T00:00:00+00:00
Fetching data from 2025-06-04T00:01:00+00:00 to 2025-06-05T00:00:00+00:00
Fetched 5587 rows of BTC/USD data.
Sample data (first 5 rows):
close high low trade_count \
timestamp
2025-05-29 00:00:00+00:00 107854.950 107854.9500 107797.350 0
2025-05-29 00:01:00+00:00 107730.050 107786.4545 107730.050 0
2025-05-29 00:02:00+00:00 107784.335 107784.3350 107784.335 0
2025-05-29 00:03:00+00:00 107749.815 107749.8150 107749.815 0
2025-05-29 00:04:00+00:00 107669.645 107680.6050 107524.300 0
open volume vwap symbol
timestamp
2025-05-29 00:00:00+00:00 107853.9450 0.0 107826.15000 BTC/USD
2025-05-29 00:01:00+00:00 107786.4545 0.0 107758.25225 BTC/USD
2025-05-29 00:02:00+00:00 107784.3350 0.0 107784.33500 BTC/USD
2025-05-29 00:03:00+00:00 107749.8150 0.0 107749.81500 BTC/USD
2025-05-29 00:04:00+00:00 107680.6050 0.0 107602.45250 BTC/USD
Sample data (last 5 rows):
close high low trade_count \
timestamp
2025-06-04 23:54:00+00:00 104807.747 104845.250 104807.747 0
2025-06-04 23:55:00+00:00 104816.255 104816.255 104782.185 0
2025-06-04 23:57:00+00:00 104744.600 104744.600 104744.600 0
2025-06-04 23:58:00+00:00 104732.050 104739.899 104732.050 0
2025-06-04 23:59:00+00:00 104748.560 104758.500 104657.600 1
open volume vwap symbol
timestamp
2025-06-04 23:54:00+00:00 104845.250 0.000000 104826.4985 BTC/USD
2025-06-04 23:55:00+00:00 104782.185 0.000000 104799.2200 BTC/USD
2025-06-04 23:57:00+00:00 104744.600 0.000000 104744.6000 BTC/USD
2025-06-04 23:58:00+00:00 104739.899 0.000000 104735.9745 BTC/USD
2025-06-04 23:59:00+00:00 104657.600 0.000072 104657.6000 BTC/USD
Data with features (using finta):
close high low trade_count \
timestamp
2025-05-29 00:40:00+00:00 107891.3515 107891.3515 107891.3515 0
2025-05-29 00:42:00+00:00 107819.7350 107819.7350 107819.7350 0
2025-05-29 00:45:00+00:00 107887.0150 107887.0150 107887.0150 0
2025-05-29 00:47:00+00:00 107962.3700 107962.3700 107962.3700 0
2025-05-29 00:48:00+00:00 108009.4200 108009.4200 108009.4200 0
open volume vwap symbol \
timestamp
2025-05-29 00:40:00+00:00 107891.3515 0.0 107891.3515 BTC/USD
2025-05-29 00:42:00+00:00 107819.7350 0.0 107819.7350 BTC/USD
2025-05-29 00:45:00+00:00 107887.0150 0.0 107887.0150 BTC/USD
2025-05-29 00:47:00+00:00 107962.3700 0.0 107962.3700 BTC/USD
2025-05-29 00:48:00+00:00 108009.4200 0.0 108009.4200 BTC/USD
SMA_10 SMA_30 ... BBL_20_2.0 \
timestamp ...
2025-05-29 00:40:00+00:00 107940.20850 107797.621567 ... 107628.818315
2025-05-29 00:42:00+00:00 107922.53925 107796.447733 ... 107677.261002
2025-05-29 00:45:00+00:00 107904.22360 107801.679900 ... 107707.198595
2025-05-29 00:47:00+00:00 107901.82610 107807.614400 ... 107744.779959
2025-05-29 00:48:00+00:00 107911.29420 107816.267900 ... 107774.213800
BBM_20_2.0 BBU_20_2.0 BBB_20_2.0 \
timestamp
2025-05-29 00:40:00+00:00 107862.704675 108096.591035 0.004337
2025-05-29 00:42:00+00:00 107874.226550 108071.192098 0.003652
2025-05-29 00:45:00+00:00 107884.072300 108060.946005 0.003279
2025-05-29 00:47:00+00:00 107897.923525 108051.067091 0.002839
2025-05-29 00:48:00+00:00 107912.089525 108049.965250 0.002555
BBP_20_2.0 ATR_14 return_1m return_3m \
timestamp
2025-05-29 00:40:00+00:00 0.561241 76.081607 0.000338 -0.000014
2025-05-29 00:42:00+00:00 0.361672 76.737786 -0.000664 -0.001301
2025-05-29 00:45:00+00:00 0.508319 76.117071 0.000624 0.000297
2025-05-29 00:47:00+00:00 0.710412 78.004607 0.000698 0.000658
2025-05-29 00:48:00+00:00 0.852965 77.682679 0.000436 0.001759
return_5m return_10m
timestamp
2025-05-29 00:40:00+00:00 -0.000097 0.000023
2025-05-29 00:42:00+00:00 -0.001051 -0.001636
2025-05-29 00:45:00+00:00 -0.000054 -0.001695
2025-05-29 00:47:00+00:00 0.000020 -0.000222
2025-05-29 00:48:00+00:00 0.001432 0.000877
[5 rows x 25 columns]
Number of rows in featured_data: 5558
Columns in featured_data:
['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']
Explanation of Changes and finta Usage:
Import finta: from finta import TA
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.
SMA, EMA, RSI: These are straightforward. TA.SMA(df, period=X, column='close').
MACD: TA.MACD() returns a DataFrame. We extract 'MACD' and 'SIGNAL' columns and calculate the histogram (MACDh) as their difference.
Bollinger Bands: TA.BBANDS() also returns a DataFrame with 'BB_UPPER', 'BB_MIDDLE', 'BB_LOWER'.
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.
Added a check for band_range == 0 in BBP calculation to prevent division by zero.
ATR: TA.ATR() is used. This function expects 'high', 'low', and 'close' columns in the input DataFrame.
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).
.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.
Error Handling/Column Existence: Added checks to ensure 'close' and other OHLC columns exist before attempting to use them.
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.
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:...
Step 4: Model Training - Defining Target & Training
Let's define a simple target: will the price be higher or lower in N minutes?
from sklearn.model_selection import train_test_split, TimeSeriesSplit
from sklearn.ensemble import RandomForestClassifier # Example model
from sklearn.metrics import accuracy_score, classification_report
import xgboost as xgb
def prepare_data_for_model(df, target_horizon=5, test_size=0.2):
"""
Prepares X (features) and y (target) for the ML model.
Target: 1 if price increases by more than a small threshold after target_horizon periods, 0 otherwise.
-1 if price decreases by more than a small threshold. (Optional: make it 3 classes)
"""
if df.empty or 'close' not in df.columns:
print("DataFrame is empty or 'close' column is missing.")
return pd.DataFrame(), pd.Series(dtype='float64'), pd.DataFrame(), pd.Series(dtype='float64')
# Define target: 1 if price goes up in `target_horizon` minutes, 0 otherwise
# A small threshold can help avoid noise around 0% change
# price_threshold = 0.0005 # e.g., 0.05% change
# df['future_price'] = df['close'].shift(-target_horizon)
# df['price_change'] = (df['future_price'] - df['close']) / df['close']
# df['target'] = 0 # Hold
# df.loc[df['price_change'] > price_threshold, 'target'] = 1 # Buy
# df.loc[df['price_change'] < -price_threshold, 'target'] = -1 # Sell (for 3-class)
# For 2-class (Up/Not Up):
df['target'] = (df['close'].shift(-target_horizon) > df['close']).astype(int)
df.dropna(inplace=True) # Remove rows with NaN target (due to shift)
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']]
X = df[feature_columns]
y = df['target']
# Time series split is crucial: DO NOT shuffle time series data for training
# For a simple split:
split_index = int(len(X) * (1 - test_size))
X_train, X_test = X[:split_index], X[split_index:]
y_train, y_test = y[:split_index], y[split_index:]
# For more robust cross-validation, use TimeSeriesSplit
# tscv = TimeSeriesSplit(n_splits=5)
# for train_index, test_index in tscv.split(X):
# X_train, X_test = X.iloc[train_index], X.iloc[test_index]
# y_train, y_test = y.iloc[train_index], y.iloc[test_index]
# Train and evaluate your model here
return X_train, X_test, y_train, y_test, feature_columns
if not featured_data.empty:
X_train, X_test, y_train, y_test, feature_cols = prepare_data_for_model(featured_data.copy(), target_horizon=5)
if not X_train.empty:
print(f"\nTraining data shape: X_train: {X_train.shape}, y_train: {y_train.shape}")
print(f"Test data shape: X_test: {X_test.shape}, y_test: {y_test.shape}")
print(f"Features used: {feature_cols}")
# Example Model: Random Forest
# model = RandomForestClassifier(n_estimators=100, random_state=42, class_weight='balanced')
# Example Model: XGBoost (often performs well)
model = xgb.XGBClassifier(
objective='binary:logistic', # or 'multi:softprob' for multi-class
n_estimators=100,
learning_rate=0.1,
max_depth=3,
use_label_encoder=False, # Suppress a warning
eval_metric='logloss' # or 'mlogloss' for multi-class
)
model.fit(X_train, y_train)
# Evaluate on test set
y_pred = model.predict(X_test)
print("\nModel Evaluation on Test Set:")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred, zero_division=0))
# Feature importance (for tree-based models)
if hasattr(model, 'feature_importances_'):
importances = pd.Series(model.feature_importances_, index=X_train.columns).sort_values(ascending=False)
print("\nFeature Importances:")
print(importances.head(10))
else:
print("Not enough data to create training/test sets after feature engineering and target creation.")
else:
print("Cannot prepare data for model, featured_data is empty.")
model = None # Ensure model is defined even if training fails
Important Considerations for Modeling:
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.
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).
Stationarity: Price series are generally non-stationary. Features like returns or indicators often help.
Overfitting: Models can learn noise from historical data. Robust cross-validation (like TimeSeriesSplit) and regularization are key.
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.
Here are the adjustments and the complete code for Step 4:
Prerequisites:
Make sure you have the necessary libraries installed:
pip install scikit-learn xgboost pandas numpy
...
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.
Adjusted Step 4 Code:
import pandas as pd
from sklearn.model_selection import train_test_split, TimeSeriesSplit # TimeSeriesSplit for more advanced CV
from sklearn.ensemble import RandomForestClassifier # Example alternative model
from sklearn.metrics import accuracy_score, classification_report
import xgboost as xgb
import numpy as np # For np.nan and other numpy operations if needed
def prepare_data_for_model(df_input, target_horizon=5, test_size=0.2):
"""
Prepares X (features) and y (target) for the ML model.
Target: 1 if price increases after target_horizon periods, 0 otherwise.
"""
df = df_input.copy() # Work on a copy to avoid modifying the original DataFrame
if df.empty or 'close' not in df.columns:
print("DataFrame is empty or 'close' column is missing for target creation.")
# Ensure 5 items are returned to match unpacking expectations
return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), []
# Define target: 1 if price goes up in `target_horizon` minutes, 0 otherwise
# df['future_price'] = df['close'].shift(-target_horizon) # Intermediate step
# df['target'] = (df['future_price'] > df['close']).astype(int)
# Simplified:
df['target'] = (df['close'].shift(-target_horizon) > df['close']).astype(int)
# Optional: Define target with a threshold (more robust against noise)
# price_threshold = 0.0005 # 0.05%
# df['price_change_pct'] = (df['close'].shift(-target_horizon) - df['close']) / df['close']
# df['target'] = 0 # Default to 'hold' or 'no significant change'
# df.loc[df['price_change_pct'] > price_threshold, 'target'] = 1 # Price went up significantly
# df.loc[df['price_change_pct'] < -price_threshold, 'target'] = 2 # Price went down significantly (for 3-class)
# For binary (up vs not up significantly):
# df['target'] = (df['price_change_pct'] > price_threshold).astype(int)
df.dropna(inplace=True) # Remove rows with NaN target (due to shift) or NaNs from features
# Identify feature columns:
# These are the columns from the original data or intermediate target calculation steps
# The 'featured_data' (input df) should have had its columns lowercased in the previous step.
cols_to_exclude = ['open', 'high', 'low', 'close', 'volume', # Raw data
'trade_count', 'vwap', # Other raw data (if present)
'future_price', 'price_change_pct', 'target'] # Target-related (add any you create)
# Filter out columns that might not exist in df to avoid errors, then exclude
actual_cols_to_exclude = [col for col in cols_to_exclude if col in df.columns]
feature_columns = [col for col in df.columns if col not in actual_cols_to_exclude]
if not feature_columns:
print("No feature columns found after exclusion. Check exclusion list and DataFrame contents.")
return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), []
X = df[feature_columns]
y = df['target']
if X.empty or y.empty:
print("X or y is empty after processing. Not enough data.")
return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), feature_columns
# Time series split is crucial: DO NOT shuffle time series data for training
# Simple chronological split:
split_index = int(len(X) * (1 - test_size))
X_train, X_test = X.iloc[:split_index], X.iloc[split_index:]
y_train, y_test = y.iloc[:split_index], y.iloc[split_index:]
# For more robust cross-validation, consider TimeSeriesSplit from sklearn.model_selection
# tscv = TimeSeriesSplit(n_splits=5)
# for train_idx, val_idx in tscv.split(X):
# X_train_fold, X_val_fold = X.iloc[train_idx], X.iloc[val_idx]
# y_train_fold, y_val_fold = y.iloc[train_idx], y.iloc[val_idx]
# # Train model on X_train_fold, y_train_fold; evaluate on X_val_fold, y_val_fold
return X_train, X_test, y_train, y_test, feature_columns
# --- Main script execution for Step 4 ---
# This assumes 'featured_data' DataFrame is available from the previous step
model = None # Initialize model variable
if 'featured_data' in locals() and not featured_data.empty:
# Ensure 'close' column exists if not already checked by create_features
if 'close' not in featured_data.columns:
print("Error: 'close' column is missing in featured_data. Cannot proceed with model training.")
else:
X_train, X_test, y_train, y_test, feature_cols = prepare_data_for_model(
featured_data, # No .copy() needed here as prepare_data_for_model does it
target_horizon=5, # Predict 5 minutes into the future
test_size=0.2 # 20% of data for testing
)
if not X_train.empty and not y_train.empty and not X_test.empty and not y_test.empty:
print(f"\nTraining data shape: X_train: {X_train.shape}, y_train: {y_train.shape}")
print(f"Test data shape: X_test: {X_test.shape}, y_test: {y_test.shape}")
print(f"Number of features: {len(feature_cols)}")
# print(f"Features used: {feature_cols}") # Can be very long, print first few if needed
print(f"First 5 features: {feature_cols[:5]}")
# Example Model: XGBoost (often performs well)
# For XGBoost versions < 1.6, you might need use_label_encoder=False if y_train is a Pandas Series
# For XGBoost versions >= 1.6, use_label_encoder is deprecated/removed.
# If you encounter issues, ensure y_train is a NumPy array: y_train.to_numpy()
model = xgb.XGBClassifier(
objective='binary:logistic', # For binary up/down prediction
n_estimators=100,
learning_rate=0.1,
max_depth=3,
# use_label_encoder=False, # Removed for better compatibility with XGBoost >= 1.6
eval_metric='logloss' # Evaluation metric for binary classification
)
print("\nTraining the XGBoost model...")
try:
model.fit(X_train, y_train) # XGBoost can often handle pandas DataFrames/Series directly
# Evaluate on test set
y_pred = model.predict(X_test)
y_pred_proba = model.predict_proba(X_test)[:, 1] # Probabilities for the positive class
print("\nModel Evaluation on Test Set:")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
# Check unique classes in y_test and y_pred for classification_report
# This helps avoid errors if one class is missing in predictions or test set after split/dropna
report = classification_report(y_test, y_pred, zero_division=0)
print(report)
# Feature importance (for tree-based models like XGBoost)
if hasattr(model, 'feature_importances_'):
importances = pd.Series(model.feature_importances_, index=X_train.columns).sort_values(ascending=False)
print("\nTop 10 Feature Importances:")
print(importances.head(10))
except Exception as e:
print(f"An error occurred during model training or evaluation: {e}")
model = None # Ensure model is None if training failed
else:
print("Not enough data to create training/test sets after feature engineering and target creation.")
if 'feature_cols' in locals() and feature_cols: # Check if feature_cols was populated
print(f"Identified feature columns: {feature_cols[:5]}...")
else:
print("Could not identify feature columns.")
else:
print("Cannot prepare data for model, 'featured_data' is empty or not defined.")
# X_train, X_test, y_train, y_test, feature_cols will not be defined here
Key changes and considerations:
prepare_data_for_model adjustments:
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'), [].
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.
Added more checks for empty X, y, or feature_columns inside prepare_data_for_model.
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()).
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.
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.
Error Handling: Basic try-except block around model fitting and evaluation.
Clarity: Added more print statements for feedback during execution.
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.
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 parameterenable_categorical must be set to True. Invalid columns:symbol: object
Ah, that's a very common and clear error message from XGBoost!
The problem is Invalid columns:symbol: object.
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.
Solution:
You need to remove or encode the symbol column before training the model.
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.
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.
Let's go with the first, simpler option for now.
How to fix the code:
Modify the cols_to_exclude list in your prepare_data_for_model function to explicitly include 'symbol'.
Here's the relevant part of the prepare_data_for_model function with the fix:
Explanation of the fix:
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.
After making this change, your feature_cols will no longer include 'symbol', and the XGBoost training should proceed without this specific error.
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.
Run the script again, and it should get past this error. You'll then see the model training and evaluation output.
import pandas as pd
from sklearn.model_selection import train_test_split, TimeSeriesSplit # TimeSeriesSplit for more advanced CV
from sklearn.ensemble import RandomForestClassifier # Example alternative model
from sklearn.metrics import accuracy_score, classification_report
import xgboost as xgb
import numpy as np # For np.nan and other numpy operations if needed
def prepare_data_for_model(df_input, target_horizon=5, test_size=0.2):
"""
Prepares X (features) and y (target) for the ML model.
Target: 1 if price increases after target_horizon periods, 0 otherwise.
"""
df = df_input.copy() # Work on a copy to avoid modifying the original DataFrame
if df.empty or 'close' not in df.columns:
print("DataFrame is empty or 'close' column is missing for target creation.")
return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), []
df['target'] = (df['close'].shift(-target_horizon) > df['close']).astype(int)
df.dropna(inplace=True)
# Identify feature columns:
# ----- THIS IS THE KEY CHANGE -----
cols_to_exclude = ['open', 'high', 'low', 'close', 'volume', # Raw data
'trade_count', 'vwap', # Other raw data (if present)
'future_price', 'price_change_pct', 'target', # Target-related
'symbol'] # <<< ADD 'symbol' HERE
# Filter out columns that might not exist in df to avoid errors, then exclude
actual_cols_to_exclude = [col for col in cols_to_exclude if col in df.columns]
feature_columns = [col for col in df.columns if col not in actual_cols_to_exclude]
if not feature_columns:
print("No feature columns found after exclusion. Check exclusion list and DataFrame contents.")
return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), []
X = df[feature_columns]
y = df['target']
if X.empty or y.empty:
print("X or y is empty after processing. Not enough data.")
return pd.DataFrame(), pd.DataFrame(), pd.Series(dtype='float64'), pd.Series(dtype='float64'), feature_columns
split_index = int(len(X) * (1 - test_size))
X_train, X_test = X.iloc[:split_index], X.iloc[split_index:]
y_train, y_test = y.iloc[:split_index], y.iloc[split_index:]
return X_train, X_test, y_train, y_test, feature_columns
# --- Main script execution for Step 4 ---
# This assumes 'featured_data' DataFrame is available from the previous step
model = None # Initialize model variable
if 'featured_data' in locals() and not featured_data.empty:
# Ensure 'close' column exists if not already checked by create_features
if 'close' not in featured_data.columns:
print("Error: 'close' column is missing in featured_data. Cannot proceed with model training.")
else:
X_train, X_test, y_train, y_test, feature_cols = prepare_data_for_model(
featured_data, # No .copy() needed here as prepare_data_for_model does it
target_horizon=5, # Predict 5 minutes into the future
test_size=0.2 # 20% of data for testing
)
if not X_train.empty and not y_train.empty and not X_test.empty and not y_test.empty:
print(f"\nTraining data shape: X_train: {X_train.shape}, y_train: {y_train.shape}")
print(f"Test data shape: X_test: {X_test.shape}, y_test: {y_test.shape}")
print(f"Number of features: {len(feature_cols)}")
# print(f"Features used: {feature_cols}") # Can be very long, print first few if needed
print(f"First 5 features: {feature_cols[:5]}")
# Example Model: XGBoost (often performs well)
# For XGBoost versions < 1.6, you might need use_label_encoder=False if y_train is a Pandas Series
# For XGBoost versions >= 1.6, use_label_encoder is deprecated/removed.
# If you encounter issues, ensure y_train is a NumPy array: y_train.to_numpy()
model = xgb.XGBClassifier(
objective='binary:logistic', # For binary up/down prediction
n_estimators=100,
learning_rate=0.1,
max_depth=3,
# use_label_encoder=False, # Removed for better compatibility with XGBoost >= 1.6
eval_metric='logloss' # Evaluation metric for binary classification
)
print("\nTraining the XGBoost model...")
try:
model.fit(X_train, y_train) # XGBoost can often handle pandas DataFrames/Series directly
# Evaluate on test set
y_pred = model.predict(X_test)
y_pred_proba = model.predict_proba(X_test)[:, 1] # Probabilities for the positive class
print("\nModel Evaluation on Test Set:")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
# Check unique classes in y_test and y_pred for classification_report
# This helps avoid errors if one class is missing in predictions or test set after split/dropna
report = classification_report(y_test, y_pred, zero_division=0)
print(report)
# Feature importance (for tree-based models like XGBoost)
if hasattr(model, 'feature_importances_'):
importances = pd.Series(model.feature_importances_, index=X_train.columns).sort_values(ascending=False)
print("\nTop 10 Feature Importances:")
print(importances.head(10))
except Exception as e:
print(f"An error occurred during model training or evaluation: {e}")
model = None # Ensure model is None if training failed
else:
print("Not enough data to create training/test sets after feature engineering and target creation.")
if 'feature_cols' in locals() and feature_cols: # Check if feature_cols was populated
print(f"Identified feature columns: {feature_cols[:5]}...")
else:
print("Could not identify feature columns.")
else:
print("Cannot prepare data for model, 'featured_data' is empty or not defined.")
# X_train, X_test, y_train, y_test, feature_cols will not be defined here
Training data shape: X_train: (4446, 17), y_train: (4446,)
Test data shape: X_test: (1112, 17), y_test: (1112,)
Number of features: 17
First 5 features: ['SMA_10', 'SMA_30', 'EMA_10', 'RSI_14', 'MACD_12_26_9']
Training the XGBoost model...
Model Evaluation on Test Set:
Accuracy: 0.5153
precision recall f1-score support
0 0.53 0.69 0.60 588
1 0.48 0.32 0.39 524
accuracy 0.52 1112
macro avg 0.51 0.50 0.49 1112
weighted avg 0.51 0.52 0.50 1112
Top 10 Feature Importances:
BBU_20_2.0 0.076261
SMA_30 0.072104
SMA_10 0.071693
BBM_20_2.0 0.064772
MACD_12_26_9 0.064698
ATR_14 0.064123
BBL_20_2.0 0.063519
EMA_10 0.062803
return_10m 0.060938
BBB_20_2.0 0.059963
dtype: float32
It gave me this. What does it mean?
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:
Data Shapes:
Training data shape: X_train: (4446, 17), y_train: (4446,)
You have 4446 data points (rows) for training.
Each data point has 17 features (columns) after removing 'symbol' and other excluded columns.
y_train has 4446 corresponding target labels.
Test data shape: X_test: (1112, 17), y_test: (1112,)
You have 1112 data points for testing, also with 17 features each.
Number of features: 17
Confirms the count of features used.
First 5 features: ['SMA_10', 'SMA_30', 'EMA_10', 'RSI_14', 'MACD_12_26_9']
Shows the names of the first few features, confirming 'symbol' is gone and your technical indicators are present.
Model Training:
Training the XGBoost model...
This indicates the model.fit(X_train, y_train) command executed without crashing.
Model Evaluation on Test Set:
Accuracy: 0.5153
The model correctly predicted whether the price would go up or not up 51.53% of the time on the unseen test data.
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.
Classification Report: This provides a more detailed breakdown of performance for each class (0 and 1).
Target: 1 if price increases after target_horizon periods, 0 otherwise.
Class 0 (Price did NOT go up / went down or stayed same):
precision: 0.53: When the model predicted "0", it was correct 53% of the time.
recall: 0.69: The model correctly identified 69% of all actual "0" instances.
f1-score: 0.60: The harmonic mean of precision and recall for class "0".
support: 588: There were 588 actual instances of "0" in the test set.
Class 1 (Price DID go up):
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).
recall: 0.32: The model only identified 32% of all actual "1" (price up) instances. It's missing a lot of opportunities.
f1-score: 0.39: The harmonic mean for class "1".
support: 524: There were 524 actual instances of "1" in the test set.
Overall Averages:
accuracy: 0.52 (rounded from 0.5153)
macro avg: Averages the metrics for each class without considering class imbalance.
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.
Interpretation of Classification Report:
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).
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".
This performance is typical for an initial model in a complex domain like financial markets. It's a starting point.
Top 10 Feature Importances:
This list shows which features the XGBoost model found most influential in making its predictions.
BBU_20_2.0 0.076261 (Bollinger Bands Upper)
SMA_30 0.072104
SMA_10 0.071693
BBM_20_2.0 0.064772 (Bollinger Bands Middle)
MACD_12_26_9 0.064698
ATR_14 0.064123
BBL_20_2.0 0.063519 (Bollinger Bands Lower)
EMA_10 0.062803
return_10m 0.060938
BBB_20_2.0 0.059963 (Bollinger Bands Width)
Interpretation:
It seems various Bollinger Band components, moving averages, MACD, ATR, and a 10-minute lagged return are all contributing.
No single feature overwhelmingly dominates, which is common.
This information can be useful for future feature engineering or selection (e.g., focusing on refining these types of indicators or trying variations).
What does this mean for your trading bot?
It's a Start: You have a working pipeline from data fetching, feature engineering, to model training and evaluation. This is a significant achievement.
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.
Next Steps (Crucial):
Refine Target Variable:
The current target (df['close'].shift(-target_horizon) > df['close']) is very noisy.
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.
Consider a 3-class problem (up, down, hold/sideways) if you want to trade both long and short, or avoid trading in uncertain conditions.
Feature Engineering:
Experiment with different indicator periods.
Add more diverse features: volatility measures (e.g., standard deviation of returns), other momentum indicators, interaction terms between features.
Consider features from different timeframes (e.g., 5-min RSI, 15-min SMA).
Hyperparameter Tuning:
The XGBoost parameters (n_estimators, learning_rate, max_depth) are just defaults. Use techniques like GridSearchCV or RandomizedSearchCV to find better parameters.
Address Class Imbalance (if it becomes an issue):
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.
More Sophisticated Validation:
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.
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.
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).
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.
A proper backtest is event-driven and considers transaction costs, slippage, etc. This is a very simplified version.
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)
Okay, let's integrate this simplified backtesting step (Step 5) into your existing workflow.
The core idea is:
Take the X_test data (which your model hasn't seen during training).
Use your trained model to make predictions on X_test. These predictions will be our trading signals.
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.
Run the run_simple_backtest function.
Prerequisites:
You'll need matplotlib if you want to uncomment the plotting part:
pip install matplotlib
Requirement already satisfied: matplotlib in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (3.10.0) Requirement already satisfied: contourpy>=1.0.1 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from matplotlib) (1.3.1) Requirement already satisfied: cycler>=0.10 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from matplotlib) (0.12.1) Requirement already satisfied: fonttools>=4.22.0 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from matplotlib) (4.55.6) Requirement already satisfied: kiwisolver>=1.3.1 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from matplotlib) (1.4.8) Requirement already satisfied: numpy>=1.23 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from matplotlib) (2.2.5) Requirement already satisfied: packaging>=20.0 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from matplotlib) (24.2) Requirement already satisfied: pillow>=8 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from matplotlib) (11.1.0) Requirement already satisfied: pyparsing>=2.3.1 in c:\users\micro\anaconda3\envs\alpaca_ai_env\lib\site-packages (from matplotlib) (3.2.1) 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) 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) Note: you may need to restart the kernel to use updated packages.
Adjusted Step 5 Code (to be appended to your script):
This code assumes model, X_test, and featured_data are available from the previous steps.
Key Changes and Considerations:
run_simple_backtest Function:
Takes df_with_predictions_input and makes a copy to avoid modifying the original.
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.
Cost/Proceeds Calculation: Explicitly calculates cost of purchase + fee for buys, and sale value - fee for sells.
Trade Logging: Added a trades_log list to store details of each trade, which can be useful for analysis.
Descriptive Naming: portfolio_value list renamed to portfolio_value_over_time.
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.
Improved Print Output: Formatted numbers with commas for readability.
Plotting Markers: The plotting section now tries to add markers for actual buy/sell trades using the trades_log.
Return Value: The function now returns both the DataFrame with portfolio values and the trades_log.
Calling the Backtest:
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.
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.
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.
Printing Trade Log: Added a snippet to print the last few trades from the log.
To Use This:
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).
Ensure matplotlib is installed if you want the plot.
Run your entire script.
Important Notes on this Simplified Backtest:
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.
Assumptions:
Trades execute at the 'close' price of the bar where the signal is generated.
No slippage (you always get the price you expect).
trade_size_usd is fixed. You might later want to trade a percentage of your capital.
Only long positions are taken.
Strategy: The strategy is very basic:
If signal is 1 (model predicts "up") and not in position: BUY.
If signal is 0 (model predicts "not up") and in position: SELL.
This means you exit a position if the model no longer predicts an "up" move for the next target_horizon candles.
Realistic Fees: Ensure transaction_cost_pct reflects your actual trading fees. For Alpaca, check their latest fee schedule for crypto.
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.
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!
import matplotlib.pyplot as plt # Optional, for plotting
# --- Step 5: Simplified Backtesting ---
def run_simple_backtest(df_with_predictions_input, initial_capital=10000, trade_size_usd=1000, transaction_cost_pct=0.001):
# Alpaca crypto fee for taker is 0.1% to 0.3%
"""
A very simplified vectorized backtest.
Assumes df_with_predictions has a 'predicted_signal' column (1 for buy, 0 for hold/nothing).
For this example, our model's prediction (0 or 1) is the signal.
1 = Go Long (Buy), 0 = Exit Long (Sell if in position) or Do Nothing
"""
df_with_predictions = df_with_predictions_input.copy()
if df_with_predictions.empty or 'predicted_signal' not in df_with_predictions.columns or 'close' not in df_with_predictions.columns:
print("DataFrame for backtest is empty or 'predicted_signal'/'close' column missing.")
if 'predicted_signal' not in df_with_predictions.columns:
print("'predicted_signal' column is missing.")
if 'close' not in df_with_predictions.columns:
print("'close' column is missing.")
return pd.DataFrame() # Return empty dataframe on failure
capital = initial_capital
position_btc = 0 # Amount of BTC held
portfolio_value_over_time = [] # Use a more descriptive name
trades_log = [] # To log trades
# Ensure the DataFrame index is a DatetimeIndex for proper time-series operations and logging
if not isinstance(df_with_predictions.index, pd.DatetimeIndex):
print("Warning: Index is not a DatetimeIndex. Converting...")
try:
df_with_predictions.index = pd.to_datetime(df_with_predictions.index)
except Exception as e:
print(f"Could not convert index to DatetimeIndex: {e}. Backtest might behave unexpectedly.")
return pd.DataFrame()
for i in range(len(df_with_predictions)):
current_timestamp = df_with_predictions.index[i]
current_price = df_with_predictions['close'].iloc[i]
signal = df_with_predictions['predicted_signal'].iloc[i]
# Decision logic
# Signal 1: Model predicts price will go UP
if signal == 1 and position_btc == 0: # Buy signal and no current position
# Buy BTC
amount_to_buy_btc = trade_size_usd / current_price
cost_of_purchase = amount_to_buy_btc * current_price # Cost before fees
fee = cost_of_purchase * transaction_cost_pct
total_cost = cost_of_purchase + fee
if capital >= total_cost:
capital -= total_cost
position_btc += amount_to_buy_btc
trades_log.append({
'timestamp': current_timestamp, 'type': 'BUY',
'price': current_price, 'quantity_btc': amount_to_buy_btc,
'cost_usd': total_cost, 'fee_usd': fee
})
# print(f"{current_timestamp}: BUY {amount_to_buy_btc:.6f} BTC @ {current_price:.2f}, Cost: {total_cost:.2f}")
# Signal 0: Model predicts price will NOT go up (or go down/stay neutral)
elif signal == 0 and position_btc > 0: # Sell signal (or neutral) and have a position
# Sell all BTC
value_of_sale = position_btc * current_price # Value before fees
fee = value_of_sale * transaction_cost_pct
proceeds = value_of_sale - fee
capital += proceeds
trades_log.append({
'timestamp': current_timestamp, 'type': 'SELL',
'price': current_price, 'quantity_btc': position_btc,
'proceeds_usd': proceeds, 'fee_usd': fee
})
# print(f"{current_timestamp}: SELL {position_btc:.6f} BTC @ {current_price:.2f}, Proceeds: {proceeds:.2f}")
position_btc = 0
# For signals where no action is taken (e.g., signal=1 but already in position, or signal=0 and no position)
# we just calculate the current portfolio value.
current_portfolio_value = capital + (position_btc * current_price)
portfolio_value_over_time.append(current_portfolio_value)
df_with_predictions['portfolio_value'] = portfolio_value_over_time
print("\n--- Simplified Backtest Results ---")
print(f"Initial Capital: ${initial_capital:,.2f}")
if not df_with_predictions.empty:
final_portfolio_value = df_with_predictions['portfolio_value'].iloc[-1]
print(f"Final Portfolio Value: ${final_portfolio_value:,.2f}")
total_return_pct = (final_portfolio_value / initial_capital - 1) * 100
print(f"Total Return: {total_return_pct:.2f}%")
print(f"Number of trades executed: {len(trades_log)}")
else:
print("Could not calculate final portfolio value as DataFrame is empty.")
total_return_pct = 0
# Plotting
if not df_with_predictions.empty and 'portfolio_value' in df_with_predictions.columns:
plt.figure(figsize=(14, 7))
plt.plot(df_with_predictions.index, df_with_predictions['portfolio_value'], label='Portfolio Value')
# Optional: Plot buy/sell markers
buy_signals = df_with_predictions[df_with_predictions['predicted_signal'] == 1].copy()
sell_signals = df_with_predictions[df_with_predictions['predicted_signal'] == 0].copy() # This is more like 'exit'
# Filter to actual trades from log for plotting markers
actual_buy_times = [trade['timestamp'] for trade in trades_log if trade['type'] == 'BUY']
actual_sell_times = [trade['timestamp'] for trade in trades_log if trade['type'] == 'SELL']
if actual_buy_times:
buy_prices = [df_with_predictions.loc[t, 'close'] for t in actual_buy_times if t in df_with_predictions.index]
plt.scatter([t for t in actual_buy_times if t in df_with_predictions.index], buy_prices, marker='^', color='green',
label='Actual Buy', alpha=1, s=100)
if actual_sell_times:
sell_prices = [df_with_predictions.loc[t, 'close'] for t in actual_sell_times if t in df_with_predictions.index]
plt.scatter([t for t in actual_sell_times if t in df_with_predictions.index], sell_prices, marker='v', color='red',
label='Actual Sell', alpha=1, s=100)
plt.title(f'Portfolio Value Over Time (Return: {total_return_pct:.2f}%)')
plt.xlabel('Date')
plt.ylabel('Portfolio Value ($)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
return df_with_predictions, trades_log
# --- Code to call the backtest (after Step 4 Model Training) ---
# This assumes 'model', 'X_test', and 'featured_data' are defined and populated from previous steps.
backtest_results_df = pd.DataFrame() # Initialize
trades_log_list = []
if 'model' in locals() and model is not None and \
'X_test' in locals() and not X_test.empty and \
'featured_data' in locals() and not featured_data.empty:
print("\nPreparing data for backtesting...")
# Get the original rows from 'featured_data' that correspond to the 'X_test' indices.
# This ensures we have 'close' prices and other original columns for the backtest period.
# It's important that X_test.index directly maps to featured_data.index for these rows.
# Verify X_test.index is a subset of featured_data.index
if not X_test.index.isin(featured_data.index).all():
print("Error: X_test.index contains values not present in featured_data.index. Cannot align for backtest.")
else:
# Select the portion of featured_data that corresponds to X_test
data_for_backtest = featured_data.loc[X_test.index].copy()
if data_for_backtest.empty:
print("Error: Sliced data_for_backtest is empty. Check indices.")
else:
print(f"Data for backtest (based on X_test period) shape: {data_for_backtest.shape}")
# Add model predictions as the 'predicted_signal'
# X_test should contain the same features (and in the same order) as X_train
# Ensure X_test has the correct columns if any transformations were applied only to X_train
try:
# Re-check columns of X_test against model's expected features if issues arise
# print(f"X_test columns: {X_test.columns.tolist()}")
# if hasattr(model, 'feature_names_in_'):
# print(f"Model expected features: {model.feature_names_in_.tolist()}")
data_for_backtest['predicted_signal'] = model.predict(X_test)
print("Successfully added 'predicted_signal' to backtesting data.")
# Run the backtest
# Using Alpaca's taker fee for crypto, which can be around 0.1% to 0.3%
# Let's use 0.1% (0.001) as an example. Adjust as needed.
backtest_results_df, trades_log_list = run_simple_backtest(
data_for_backtest,
initial_capital=10000,
trade_size_usd=1000, # Amount to invest per trade
transaction_cost_pct=0.001 # e.g., 0.1% per trade side
)
if not backtest_results_df.empty:
print("\nSample of backtest results (last 5 rows):")
print(backtest_results_df[['close', 'predicted_signal', 'portfolio_value']].tail())
if trades_log_list:
print("\nLast 5 trades:")
for trade in trades_log_list[-5:]:
print(trade)
except ValueError as ve:
print(f"ValueError during prediction or backtest setup: {ve}")
print("This might be due to a mismatch in feature names or number of features expected by the model.")
print(f"X_test columns: {list(X_test.columns)}")
if hasattr(model, 'n_features_in_'):
print(f"Model was trained with {model.n_features_in_} features.")
if hasattr(model, 'feature_names_in_'):
print(f"Model feature names: {list(model.feature_names_in_)}")
except Exception as e:
print(f"An error occurred during backtest preparation or execution: {e}")
else:
print("\nSkipping backtest: Model, X_test, or featured_data is not available or empty.")
if 'model' not in locals() or model is None:
print("- Model is not trained or available.")
if 'X_test' not in locals() or X_test.empty:
print("- X_test data is not available.")
if 'featured_data' not in locals() or featured_data.empty:
print("- featured_data is not available.")
Preparing data for backtesting... Data for backtest (based on X_test period) shape: (1112, 25) Successfully added 'predicted_signal' to backtesting data. --- Simplified Backtest Results --- Initial Capital: $10,000.00 Final Portfolio Value: $9,792.06 Total Return: -2.08% Number of trades executed: 214
Sample of backtest results (last 5 rows):
close predicted_signal portfolio_value
timestamp
2025-06-04 23:54:00+00:00 104807.747 0 9792.056818
2025-06-04 23:55:00+00:00 104816.255 0 9792.056818
2025-06-04 23:57:00+00:00 104744.600 0 9792.056818
2025-06-04 23:58:00+00:00 104732.050 0 9792.056818
2025-06-04 23:59:00+00:00 104748.560 0 9792.056818
Last 5 trades:
{'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)}
{'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)}
{'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)}
{'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)}
{'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)}
Ok. I got this. What does it mean?
This output gives you the results of your simplified backtest. Let's break it down:
Preparation:
Preparing data for backtesting...
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'.
Successfully added 'predicted_signal' to backtesting data.: Confirms your model's predictions are now part of the dataset used for the backtest.
Backtest Summary Results:
--- Simplified Backtest Results ---
Initial Capital: $10,000.00: You started the simulation with $10,000.
Final Portfolio Value: $9,792.06: After all the simulated trades over the test period, your portfolio ended up with $9,792.06.
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.
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).
Sample of Backtest Data (Last 5 Rows):
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.
timestamp: The date and time of the data point.
close: The BTC/USD closing price for that minute.
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.
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).
Last 5 Trades Log:
This gives you a glimpse into the actual trading activity. Each dictionary represents one trade.
'timestamp': When the trade occurred.
'type': 'BUY' or 'SELL'.
'price': The BTC price at which the trade was simulated.
'quantity_btc': How much BTC was bought or sold.
'cost_usd' (for BUYs): Total USD spent, including the fee.
'proceeds_usd' (for SELLs): Total USD received, after deducting the fee.
'fee_usd': The transaction fee paid for that trade (calculated as price * quantity * transaction_cost_pct).
Observations from the trade log:
The trades are happening frequently (e.g., a SELL at 23:07, then a BUY at 23:09, then a SELL at 23:18).
The fee_usd is around $1.00 per trade, which is consistent with a trade_size_usd of
1000anda‘transactioncostpct‘of0.001(0.11000anda‘transactioncostpct‘of0.001(0.1
1000 * 0.001 = $1`.
Looking at the BUY at 23:09 and SELL at 23:18:
Bought at ~104846, cost $1001.00.
Sold at ~104769, proceeds $998.27.
This individual round trip was a small loss, exacerbated by fees.
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.
What does this mean for your trading bot and what to do next?
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:
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%.
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.
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.
Next Steps - Focus on Improving Profitability:
Reduce Trading Frequency / Improve Signal Quality:
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.
Longer target_horizon: Predicting 5 minutes ahead is short. Try 15m, 30m, or 1h. This might smooth out noise.
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.
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.
Improve Model Accuracy/Precision:
Continue with feature engineering.
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).
Try different model types (e.g., LightGBM, Logistic Regression, an LSTM if you have a lot more data and want to capture sequences).
Decrease Impact of Transaction Costs:
The most direct way is to trade less frequently (see point 1).
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).
Analyze Losing Trades:
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?
Increase Data for Training/Testing:
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.
Consider a Different Strategy Logic:
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.
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.
This is where you'd run the bot periodically (e.g., every minute).
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)
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.
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).
Key Assumptions & Globals from Previous Steps:
api: The Alpaca tradeapi.REST object, initialized and connected.
create_features: The function using finta to calculate technical indicators.
model: Your trained XGBoost model (or whatever model you used). This should be a global variable after Step 4.
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.
Adjusted Step 6 Code:
Key Changes and Enhancements:
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.
get_latest_bar_features Function:
Takes symbol_to_fetch, timeframe_to_use, and max_lookback_period as arguments.
Uses datetime.now(tz=timezone.utc) for timezone-aware start time. (You'll need from datetime import timezone).
Explicitly lowercases column names from Alpaca before passing to create_features.
Added more robust error handling and checks for sufficient data length.
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).
Added a check to ensure all feature_cols are present in the featured_bars DataFrame before selection to prevent KeyError.
check_and_place_trade Function:
Retrieves model, feature_cols, api, etc., via global.
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.
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.
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.
Improved print statements for clarity.
More detailed error handling, including traceback.print_exc() for debugging.
Main Loop:
The if __name__ == "__main__": block now includes checks to ensure model and feature_cols are actually populated before starting the loop.
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.
Added traceback.print_exc() in the main loop's exception handler.
Before Running (CRITICAL):
PAPER TRADING ONLY: Start with paper trading. Ensure your Alpaca API keys are for your paper account.
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.
create_features Function: Ensure your create_features function (the one using finta) is defined and works correctly.
API Object: Ensure api is your initialized Alpaca tradeapi.REST object.
Constants: Adjust SYMBOL_TO_TRADE, TRADE_QTY_USD, and CONFIDENCE_THRESHOLD as needed.
Exchange for Quotes: In get_latest_crypto_quote, make sure exchange="CBSE" (or another valid exchange from Alpaca for BTC/USD) is appropriate.
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.
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.
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).
How to Integrate:
Place the get_latest_bar_features and check_and_place_trade functions in your script.
Define the constants (SYMBOL_TO_TRADE, etc.) after your model training section.
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.
This provides a more robust foundation for your bot's execution logic. Test thoroughly in paper trading!
import os # Already imported, but good to note for this section
import alpaca_trade_api as tradeapi # Already imported
import pandas as pd # Already imported
from datetime import datetime, timedelta # Already imported
import time # Already imported
# --- Constants for Step 6 ---
# These should be defined after your API setup and model training
SYMBOL_TO_TRADE = "BTC/USD" # Use the same symbol format as Alpaca uses
TRADE_QTY_USD = 100 # Amount in USD to trade per signal. Adjust based on risk.
# TARGET_HORIZON_MINUTES is implicitly handled by the model's training
# but not directly used in this execution logic, other than fetching enough data.
# Ensure necessary globals are defined from previous steps
# For example, at the end of Step 4, you should have:
# model = your_trained_xgboost_model
# feature_cols = list_of_your_feature_column_names
# And from the beginning of your script:
# api = tradeapi.REST(...)
# create_features = your_finta_based_feature_creation_function
def get_latest_bar_features(symbol_to_fetch, timeframe_to_use, max_lookback_period=150):
"""
Fetches latest bars, calculates features for the most recent *complete* bar.
max_lookback_period: Number of bars to fetch to ensure enough data for all indicators.
e.g., if longest MA is 30, and RSI is 14, fetch at least 30+ buffer.
150 minutes should cover most common 1-min indicators.
"""
global api, create_features, feature_cols # Access global api, create_features, and feature_cols
# Calculate start time for fetching data
# We need enough bars for feature calculation, so go back further than just max_lookback_period
# to account for potential gaps or incomplete last bar from API.
# For 1-minute bars, max_lookback_period minutes. Let's add a buffer.
now_utc = datetime.now(tz=timezone.utc) # Ensure timezone awareness for Alpaca
start_fetch_dt = now_utc - timedelta(minutes=max_lookback_period + 60) # Extra 60 min buffer
print(f"Fetching latest {max_lookback_period} bars for {symbol_to_fetch} from {start_fetch_dt.isoformat()}...")
try:
latest_bars_df = api.get_crypto_bars(
symbol_to_fetch,
timeframe_to_use, # e.g., tradeapi.TimeFrame.Minute
start=start_fetch_dt.isoformat(),
# end defaults to now, limit helps control amount
limit=max_lookback_period + 5 # Fetch a few extra just in case
).df
# Ensure columns are lowercase as expected by 'create_features'
latest_bars_df.columns = [col.lower() for col in latest_bars_df.columns]
except Exception as e:
print(f"Error fetching latest bars: {e}")
return None
if latest_bars_df.empty:
print("No recent bars data fetched.")
return None
# The last bar from Alpaca might be incomplete if fetched mid-minute.
# We want to make predictions based on the *last fully completed* bar.
# For 1-minute data, if we fetch at XX:XX:30, the bar for XX:XX:00 is complete.
# Let's assume the second to last bar is definitely complete, or if only one bar, it's what we have.
# More robust: filter out bars with timestamp > current_minute_start
# A simple approach: ensure enough bars for features
# Longest feature (e.g., SMA_30) needs 30 periods. RSI needs 14.
# Let's say your longest feature (like SMA_30) needs 'N' periods.
# `create_features` will drop N-1 rows.
required_bars_for_features = 35 # Example: if SMA(30) is longest, need at least 30 + buffer
if len(latest_bars_df) < required_bars_for_features:
print(f"Not enough recent bars ({len(latest_bars_df)}) to calculate all features (need ~{required_bars_for_features}).")
return None
print(f"Calculating features for {len(latest_bars_df)} bars...")
# The create_features function was defined in your earlier code (using finta)
try:
featured_bars = create_features(latest_bars_df) # .copy() is handled inside create_features
except Exception as e:
print(f"Error during feature creation for latest bars: {e}")
return None
if featured_bars.empty:
print("Failed to create features (resulted in empty DataFrame).")
return None
# Return only the features for the most recent bar for which features could be calculated
# `dropna()` in `create_features` removes initial NaNs. The last row of `featured_bars` is what we need.
# The features must match `feature_cols` used during training.
try:
# Ensure all expected feature_cols are present in featured_bars before selecting
missing_cols = [col for col in feature_cols if col not in featured_bars.columns]
if missing_cols:
print(f"Error: The following feature columns are missing after calculation: {missing_cols}")
print(f"Available columns: {featured_bars.columns.tolist()}")
return None
# Select only the feature_cols and take the last row
final_features_for_prediction = featured_bars[feature_cols].iloc[-1:]
return final_features_for_prediction # Return as a DataFrame (single row)
except Exception as e:
print(f"Error selecting final features for prediction: {e}")
print(f"Feature_cols expected: {feature_cols}")
print(f"Columns in featured_bars: {featured_bars.columns.tolist()}")
return None
def check_and_place_trade():
global model, feature_cols, api, SYMBOL_TO_TRADE, TRADE_QTY_USD # Ensure these are accessible
if model is None: # Check if model is loaded/trained
print("Model is not available. Skipping trade check.")
return
if not feature_cols: # Check if feature_cols is defined
print("Feature columns not defined. Skipping trade check.")
return
print(f"\n[{datetime.now()} UTC] Checking for trading signal for {SYMBOL_TO_TRADE}...")
# Get features for the latest complete bar
# Assuming tradeapi.TimeFrame.Minute was used for training and data fetching
current_features_df = get_latest_bar_features(SYMBOL_TO_TRADE, tradeapi.TimeFrame.Minute)
if current_features_df is None or current_features_df.empty:
print("Could not get features for the latest bar. Skipping trade.")
return
# The get_latest_bar_features should already return data with columns in feature_cols order
# But an explicit reorder is a good safeguard if the selection logic changes.
# current_features_df = current_features_df[feature_cols] # This was already done in get_latest_bar_features
try:
prediction = model.predict(current_features_df) # `current_features_df` is already a DataFrame
signal = prediction[0] # Prediction for the single latest bar
proba = model.predict_proba(current_features_df)[0] # Get probabilities [prob_class_0, prob_class_1]
print(f"Features for prediction: {current_features_df.iloc[0].to_dict()}")
print(f"Model Prediction (0=NotUp, 1=Up): {signal}, Confidence (Prob for Up): {proba[1]:.4f}")
except Exception as e:
print(f"Error during model prediction: {e}")
print(f"Features passed to model: {current_features_df}")
return
# --- Alpaca API Interaction ---
try:
positions = api.list_positions()
btc_position = next((p for p in positions if p.symbol == SYMBOL_TO_TRADE), None)
# Use get_latest_trade for a more "real-time" price if available, or quote
# latest_trade = api.get_latest_crypto_trade(SYMBOL_TO_TRADE, exchange="CBSE") # Example exchange
# current_price = latest_trade.p
# Or use quotes:
latest_quote = api.get_latest_crypto_quote(SYMBOL_TO_TRADE, exchange="CBSE") # Coinbase is a common exchange for BTCUSD
current_price = (latest_quote.ap + latest_quote.bp) / 2.0 # Mid-price from ask/bid
if not current_price or current_price <= 0:
print(f"Could not get a valid current price for {SYMBOL_TO_TRADE}. Price: {current_price}")
return
print(f"Current mid-price for {SYMBOL_TO_TRADE}: ${current_price:.2f}")
# --- Trade Logic ---
# Simple strategy: Buy on signal 1, Sell (if holding) on signal 0
# Add a confidence threshold:
CONFIDENCE_THRESHOLD = 0.55 # Only trade if probability of "UP" is > 55% (example)
if signal == 1 and proba[1] >= CONFIDENCE_THRESHOLD: # Predicted UP with enough confidence
if btc_position is None or float(btc_position.qty) == 0:
qty_to_buy = TRADE_QTY_USD / current_price
# Alpaca has precision requirements for crypto orders.
# For BTC, usually 6-8 decimal places for quantity.
qty_to_buy_rounded = round(qty_to_buy, 7) # Adjust precision as needed for BTC
if qty_to_buy_rounded > 0: # Ensure non-zero quantity
print(f"BUY signal (confidence {proba[1]:.2f}). Attempting to buy {qty_to_buy_rounded:.7f} {SYMBOL_TO_TRADE} at ~${current_price:.2f}")
api.submit_order(
symbol=SYMBOL_TO_TRADE,
qty=str(qty_to_buy_rounded), # Qty as string
side='buy',
type='market',
time_in_force='gtc' # Good 'til Canceled
)
print(f"BUY order for {qty_to_buy_rounded:.7f} {SYMBOL_TO_TRADE} submitted.")
else:
print(f"Calculated buy quantity {qty_to_buy_rounded:.7f} is too small. No order placed.")
else:
print(f"BUY signal (confidence {proba[1]:.2f}), but already have a position of {btc_position.qty} {SYMBOL_TO_TRADE}. Holding.")
elif signal == 0: # Predicted NOT UP (or low confidence for UP) - Potential SELL
if btc_position and float(btc_position.qty) > 0:
qty_to_sell = float(btc_position.qty) # Sell entire position
qty_to_sell_rounded = round(qty_to_sell, 7) # Match precision
if qty_to_sell_rounded > 0:
print(f"SELL signal (or low UP confidence). Attempting to sell {qty_to_sell_rounded:.7f} {SYMBOL_TO_TRADE} at ~${current_price:.2f}")
api.submit_order(
symbol=SYMBOL_TO_TRADE,
qty=str(qty_to_sell_rounded), # Qty as string
side='sell',
type='market',
time_in_force='gtc'
)
print(f"SELL order for {qty_to_sell_rounded:.7f} {SYMBOL_TO_TRADE} submitted.")
else:
print(f"Sell quantity {qty_to_sell_rounded:.7f} is zero or negative. No order placed.")
else:
print("SELL signal (or low UP confidence), but no open position to sell. Doing nothing.")
else: # Signal is 1 but confidence is too low
print(f"UP signal (signal={signal}) but confidence {proba[1]:.2f} is below threshold {CONFIDENCE_THRESHOLD}. No action.")
except Exception as e:
print(f"Error during trade execution or API interaction: {e}")
import traceback
traceback.print_exc()
# --- Main Execution Loop (for demonstration) ---
# This should be run after your model is trained and `model` and `feature_cols` are populated.
# Example of how to make `model` and `feature_cols` global if they are defined in a function:
# This is just illustrative; in your script, they should be in the global scope after training.
# if 'model' not in globals(): model = None
# if 'feature_cols' not in globals(): feature_cols = []
if __name__ == "__main__":
# --- This block is usually at the very end of your script ---
# --- It assumes all previous steps (API init, data fetch, feature creation, model training) have run ---
# Check if the model and feature_cols are actually populated from Step 4
if 'model' not in globals() or model is None:
print("Model is not trained or not found in global scope. Exiting live trading part.")
# exit() # Or handle appropriately
elif 'feature_cols' not in globals() or not feature_cols:
print("Feature_cols not defined or empty. Exiting live trading part.")
# exit()
else:
print("\n--- Starting Live/Paper Trading Bot (Simplified Loop) ---")
print("IMPORTANT: THIS IS FOR PAPER TRADING. VERIFY ALL SETTINGS.")
print(f"Trading Symbol: {SYMBOL_TO_TRADE}, USD per Trade: {TRADE_QTY_USD}")
print(f"Using Model: {type(model).__name__}, Trained Features: {len(feature_cols)}")
# You would typically use a robust scheduler like APScheduler for live trading
# to run check_and_place_trade() precisely at the start of each new bar (e.g., every minute).
# This is a very simplified loop for demonstration.
try:
# Initial check
check_and_place_trade()
# Loop for a few more checks
for i in range(5): # Run for a few iterations (e.g., 5 minutes)
# Wait for the next minute (approximately).
# This is NOT a precise way to align with market bars.
current_seconds = datetime.now().second
wait_time = 60 - current_seconds
if wait_time < 0: wait_time = 60 + wait_time # if somehow it's past the minute
print(f"Waiting for {wait_time} seconds until the next minute...")
time.sleep(wait_time)
check_and_place_trade()
except KeyboardInterrupt:
print("\nTrading loop stopped by user.")
except Exception as e:
print(f"An unhandled error occurred in the trading loop: {e}")
import traceback
traceback.print_exc()
finally:
print("Trading loop finished.")
# else:
# if 'model' not in globals() or model is None:
# print("Script not run as main, or model not yet trained. Live trading functions defined but not executed.")
super ai .:. improve yourself .:. improve your business .:. improve the world .:. about .:. contact .:. general ai .:. ai / ml courses .:. ai art gallery