πŸ€– Trading Bot Framework

August 12, 2026 Β· View on GitHub

A Production-Ready, Kubernetes-Native Algorithmic Trading System

kubectl create secret generic tradingbot-secrets --from-env-file=.env --namespace=tradingbots-2025 --dry-run=client -o yaml | kubectl apply -f -

Trading Bot Framework

This framework allows developers to build, backtest, and deploy automated trading strategies as Kubernetes CronJobs. It handles the "boring stuff"β€”data ingestion, technical analysis, database persistence, and portfolio trackingβ€”so you can focus on the alpha.

πŸš€ Why this Framework?

  • Batteries Included: 150+ Technical Indicators (RSI, MACD, etc.) ready out of the box.
  • Infrastructure as Code: Native Helm charts for easy scaling on K8s.
  • Data Consistency: Built-in caching and PostgreSQL persistence for trade history and market data.
  • Backtesting to Production: One class handles local testing, hyperparameter optimization, and live execution.

πŸ›  System Architecture

The system is designed to be lightweight and stateless. Each "Bot" is a containerized instance triggered by a schedule.

  1. Ingestion: Fetches data from Yahoo Finance (with DB caching).
  2. Analysis: Enriches data with the ta library (Technical Analysis).
  3. Execution: BotClass manages the state of your portfolio in PostgreSQL.
  4. Monitoring: Real-time performance tracking via the included Dashboard.

⚑ Quick Start

1. Requirements

  • Python 3.12+ (We recommend uv for speed)
  • Docker (for local DB)

2. Launch Local Environment

# Start PostgreSQL
docker run -d --name pg-trading -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=tradingbot -p 5432:5432 postgres:17-alpine

# Install project
uv sync
export POSTGRES_URI="postgresql://postgres:pass@localhost:5432/tradingbot"

3. Your First Strategy

Create a simple RSI Mean Reversion bot in seconds:

from tradingbot.utils.botclass import Bot


class RSIBot(Bot):
    def __init__(self):
        super().__init__("RSIBot", "AAPL", interval="1m", period="1d")

    def decisionFunction(self, row):
        if row["momentum_rsi"] < 30:
            return 1  # Buy
        if row["momentum_rsi"] > 70:
            return -1  # Sell
        return 0


if __name__ == "__main__":
    bot = RSIBot()
    bot.run()  # Single iteration

4. Local Development & Testing

Backtest your strategy before going live:

bot = RSIBot()
results = bot.local_backtest(initial_capital=10000.0)
print(f"Sharpe Ratio: {results['sharpe_ratio']:.2f}")
print(f"Yearly Return: {results['yearly_return']:.2%}")

Optimize hyperparameters automatically:

class RSIBot(Bot):
    # Define search space
    param_grid = {
        "rsi_buy": [25, 30, 35],
        "rsi_sell": [65, 70, 75],
    }

    def __init__(self, rsi_buy=30.0, rsi_sell=70.0, **kwargs):
        super().__init__("RSIBot", "AAPL", interval="1m", period="1d", **kwargs)
        self.rsi_buy = rsi_buy
        self.rsi_sell = rsi_sell

    def decisionFunction(self, row):
        if row["momentum_rsi"] < self.rsi_buy:
            return 1
        if row["momentum_rsi"] > self.rsi_sell:
            return -1
        return 0


# Optimize and backtest
bot = RSIBot()
bot.local_development()  # Finds best params, then backtests

Key Features:

  • Data pre-fetching: Historical data fetched once, reused for all parameter combinations
  • Database caching: Data persisted to DB, subsequent runs are instant
  • Parallel execution: Uses multiple CPU cores automatically
  • Automatic QuantStats reports: Generated and uploaded to Google Cloud Storage with Sharpe/return optimizations and drawdown analysis

πŸ“ˆ Dashboard & Monitoring

The framework includes a built-in visualization suite to track your bots' performance. Each backtest generates a professional QuantStats report with cumulative returns, drawdown analysis, monthly heatmaps, and more (see example).

Portfolio Overview

Overview Dashboard shows:

  • Current Worth, Total Return %, Annualized Return %
  • Sharpe Ratio, Sortino Ratio, Max Drawdown %
  • Volatility, Total Trades, Start Date

Bot Detail Page

Bot Detail Page includes:

  • Portfolio value charts, daily returns distribution
  • Monthly returns heatmap, drawdown visualization
  • Current holdings table, complete trade history

The dashboard is deployed automatically with the Helm chart. See Deployment for setup.

πŸ— Deployment

Production (Kubernetes)

The system treats every bot as a CronJob. Define your schedule in values.yaml and deploy:

1. Create Kubernetes Secret:

# Create .env file with:
# POSTGRES_PASSWORD=yourpassword
# POSTGRES_URI=postgresql://postgres:yourpassword@psql-service:5432/postgres
# OPENROUTER_API_KEY=yourkey (if using AI bots)
# BASIC_AUTH_PASSWORD=yourpassword (for dashboard)

# Create namespace
kubectl create namespace tradingbots-2025

# Create secret
kubectl create secret generic tradingbot-secrets \
  --from-env-file=.env \
  --namespace=tradingbots-2025

2. Configure Bots:

# helm/tradingbots/values.yaml
bots:
  - name: rsibot
    schedule: '*/5 * * * 1-5' # Every 5 mins, Mon-Fri

3. Deploy:

helm upgrade --install tradingbots \
  ./helm/tradingbots \
  --create-namespace \
  --namespace tradingbots-2025

PostgreSQL is automatically deployed via Helm (if postgresql.enabled: true in values.yaml).

For detailed guides, see:

🧰 Developer Reference

Bot Implementation Levels

See the comprehensive Bot Implementation Levels guide for detailed explanations, examples, trade-offs, and pitfalls for each pattern.

Quick summary:

1. Simple (Recommended): decisionFunction(row) For single-row TA signals. The base class handles data fetching, self.data (full history slice), and buy/sell execution automatically β€” no makeOneIteration needed:

def decisionFunction(self, row):
    if row["momentum_rsi"] < 30:
        return 1
    if row["momentum_rsi"] > 70:
        return -1
    return 0

If your signal needs the full historical DataFrame (e.g. Hurst exponent, rolling z-scores), use self.data β€” the base class sets it automatically before calling decisionFunction:

def decisionFunction(self, row):
    lookback = self.data.tail(50)  # self.data is always the slice up to current bar
    zscore = (row["close"] - lookback["close"].mean()) / lookback["close"].std()
    return 1 if zscore < -2 else (-1 if zscore > 2 else 0)

1b. Multi-Asset: tickers=[...] + decisionFunction(row) For strategies that trade multiple instruments. The framework calls decisionFunction once per ticker per bar, sets self._current_ticker and populates self.datas with all tickers' history up to the current bar:

def __init__(self):
    super().__init__("MyBot", tickers=["QQQ", "GLD", "TLT"], interval="1d", period="1y")


def decisionFunction(self, row):
    ticker = self._current_ticker
    # self.datas[ticker] has history up to current bar for all tickers
    ...
    return 1  # -1 or 0

2. Override makeOneIteration() β€” only when you need external APIs or portfolio-weight rebalancing that can't map to -1/0/1 signals:

def makeOneIteration(self):
    fear_greed = get_fear_greed_index()  # External API β€” not backtestable
    if fear_greed >= 70:
        self.buy("QQQ")
    return 1
def makeOneIteration(self):
    data = self.getYFDataMultiple(["QQQ", "GLD", "TLT"])
    weights = optimize_portfolio(data)  # Portfolio optimizer β€” outputs weights, not signals
    self.rebalancePortfolio(weights)
    return 0

Backtesting: Only bots using decisionFunction() (Levels 1 and 1b) are backtestable via local_backtest(). Bots that override makeOneIteration() with external API calls, AI models, or portfolio-weight optimizers cannot be replayed on historical data and must be validated via live runs. If your strategy is yfinance-only, prefer decisionFunction.

Key Methods

MethodDescription
getYFDataWithTA()Fetches OHLCV + 150 indicators.
decisionFunction(row)Logic applied to every candle. Return -1, 0, 1.
makeOneIteration()Override for custom logic.
local_backtest()Simulates strategy performance on historical data.
local_development()Optimize hyperparameters + backtest.
buy(symbol) / sell(symbol)Automated portfolio and DB logging.
rebalancePortfolio(weights)Rebalance to target weights.
run_ai(system_prompt, user_message)Runs AI with tools (main LLM); returns model response. Requires OPENROUTER_API_KEY.
run_ai_simple(system_prompt, user_message)Single-turn, no tools (cheap LLM); for summarization, extraction, classification.
run_ai_simple_with_fallback(system_prompt, user_message, sanity_check=..., fallback_to_main=True)Cheap LLM first; validates output; retries with main LLM if sanity check fails.

AI Tools (LangChain + OpenRouter)

Two LLMs: main (OPENROUTER_MAIN_MODEL, default deepseek/deepseek-v3.2) for tool-using flows; cheap (OPENROUTER_CHEAP_MODEL, default openrouter/free) for simple single-turn text tasks. Set OPENROUTER_API_KEY (required); optionally set the two model env vars.

With tools (main LLM):

response = bot.run_ai(
    system_prompt="You are a trading assistant.", user_message="Summarize my recent trades and portfolio."
)
print(response)  # Model response as string

Simple tasks, no tools (cheap LLM): summarization, extraction, classification, rewriting:

summary = bot.run_ai_simple(system_prompt="You summarize in one sentence.", user_message="Summarize: ...")

Cheap-first with fallback: Try cheap LLM first, validate output for sanity, and retry with main LLM if the result fails. Use for simple tasks when you want to save cost but guarantee sane results:

result = bot.run_ai_simple_with_fallback(
    system_prompt="You classify sentiment.",
    user_message="Classify as buy/hold/sell: ...",
    sanity_check=None,  # optional; default rejects empty/refusal/error prefix
    fallback_to_main=True,
)

Tools available to the model (when using run_ai):

  1. get_market_data(symbol, period) – Market data (OHLCV), default last two weeks.
  2. get_portfolio_status() – Current portfolio worth (USD) and holdings.
  3. get_recent_trades(limit) – Recent trades; for sells, profit of the closed trade is shown.
  4. get_stock_news(symbol, limit) – Recent news for a symbol (title, link, publisher, published_at) from the database.
  5. get_stock_earnings(symbol, limit) – Recent earnings dates and EPS (estimate, reported, surprise %) for a symbol from the database.
  6. get_stock_insider_trades(symbol, limit) – Recent insider transactions (insider, type, shares, value) for a symbol from the database.

See AI Tools Guide and AITools API for details.

Portfolio Structure

Portfolio is stored as JSON in the database:

portfolio = {
    "USD": 10000.0,  # Cash
    "QQQ": 5.5,  # Holdings (quantity, not value)
    "AAPL": 10.0,  # More holdings
}

Access via: bot.dbBot.portfolio.get("USD", 0)

Available Indicators

Access over 150 indicators via the row object:

  • Trend: trend_macd, trend_adx, trend_ichimoku_a, trend_sma_fast, trend_sma_slow
  • Momentum: momentum_rsi, momentum_stoch, momentum_ao, momentum_roc, momentum_ppo
  • Volatility: volatility_bbh (Bollinger High), volatility_bbl (Bollinger Low), volatility_atr
  • Volume: volume_vwap, volume_obv, volume_mfi

See Technical Analysis Guide for complete list.

πŸ“– Documentation

Online Documentation: justinguese.github.io/python_tradingbot_framework/

Getting Started

  • Quick Start Guide - Complete local development workflow with PostgreSQL setup, bot creation at different abstraction levels, backtesting, and hyperparameter tuning
  • Installation - System requirements and dependency installation
  • Creating a Bot - Detailed bot creation patterns and examples

Deployment

Guides

API Reference

πŸ“‘ Telegram Channel Monitor

The framework includes an optional Telegram channel monitor that polls channels for new messages, summarizes them with AI, extracts the primary asset ticker, and writes results to the database.

# helm/tradingbots/values.yaml
telegramMonitor:
  enabled: true
  schedule: '*/30 * * * *' # Every 30 minutes
  channels: 'some_news_channel,-1001234567890'
  fetchLimit: '50'

How it works: Runs as a CronJob β€” connects via a Telethon StringSession (no persistent process), fetches recent messages, skips already-stored ones, summarizes each with the cheap LLM, and persists to telegram_messages table with channel, text, summary, symbol, and published_at.

Required secrets: TELEGRAM_API_ID, TELEGRAM_API_HASH, TELEGRAM_SESSION_STRING (from my.telegram.org).

See Telegram Monitor Guide for full setup instructions.

πŸ“ˆ Live Trading (Collective2, Interactive Brokers, eToro & Darwinex)

Warning

DISCLAIMER: This software is for educational and research purposes only. Trading involves significant risk of loss and is not suitable for all investors. Use of "Live Trading" features is strictly at your own risk. The authors and contributors are not liable for any financial losses, damages, or unintended trades incurred. Always test strategies thoroughly in a paper-trading environment before deploying real capital.

The framework can mirror your paper-bot portfolios to a live brokerage account. Supported brokers: Collective2 (World API v4), Interactive Brokers (IBKR Web API via headless OAuth 1.0a / ibind), eToro (Public REST API), and Darwinex (DXtrade API).

1. Configure Environment

Add these to your .env or Kubernetes secrets:

# Collective2
COLLECTIVE2_API_KEY=your_api_key
COLLECTIVE2_SYSTEM_ID=12345678

# Interactive Brokers (IBKR Web API β€” headless OAuth 1.0a, no gateway)
IB_ACCOUNT_ID=DU1234567   # paper accounts start with DU; live with U
IBIND_USE_OAUTH=True
IBIND_OAUTH1A_CONSUMER_KEY=YOURKEY
IBIND_OAUTH1A_ACCESS_TOKEN=your_access_token
IBIND_OAUTH1A_ACCESS_TOKEN_SECRET=your_access_token_secret
IBIND_OAUTH1A_DH_PRIME=hex_dh_prime_from_dhparam_pem
IBIND_OAUTH1A_ENCRYPTION_KEY_FP=/etc/ibkr/private_encryption.pem
IBIND_OAUTH1A_SIGNATURE_KEY_FP=/etc/ibkr/private_signature.pem

#   ^ see "Generating the IBKR OAuth credentials" below for where these come from

# eToro (Public REST API)
ETORO_API_KEY=your_public_key
ETORO_USER_KEY=your_user_key
ETORO_DEMO=true             # true for demo/paper, false for live

# Darwinex (DXtrade API)
DARWINEX_USERNAME=your_username
DARWINEX_PASSWORD=your_password
DARWINEX_ACCOUNT_ID=12345   # optional
DARWINEX_DEMO=true          # true for demo/paper, false for live

# Shared
LIVETRADE_BOT_WEIGHTS='{"adaptivemeanreversionbot": 1.0}'
LIVETRADE_DRY_RUN=false

Generating the IBKR OAuth Credentials

None of the IBIND_OAUTH1A_* values are handed to you in one place β€” you generate the key material locally, register it with IBKR, and IBKR returns two of the five. Requires a funded IBKR Pro account (Lite won't work); paper rides on the live account's entitlement.

1. Generate the keys and DH params locally:

mkdir -p ~/ibkr-oauth-paper && cd ~/ibkr-oauth-paper
openssl genrsa -out private_signature.pem 2048
openssl rsa -in private_signature.pem -pubout -out public_signature.pem
openssl genrsa -out private_encryption.pem 2048
openssl rsa -in private_encryption.pem -pubout -out public_encryption.pem
openssl dhparam -out dhparam.pem 2048   # slow, a minute or two

Use a separate keypair for paper and live β€” do not reuse one across both.

2. Register at the OAuth self-service portal.

It is not a menu item inside Client Portal. It's a separate app behind its own SSO entry point β€” open it in a browser already logged in as the username you want the bot to trade as (your paper username for paper):

https://ndcdyn.interactivebrokers.com/sso/Login?action=OAUTH&RL=1&ip2loc=US

The action=OAUTH parameter is what routes you there instead of the normal portal. Use the US domain β€” IBKR support has reported other entry points misbehaving. If it bounces you back to Client Portal, the username isn't OAuth-enabled: raise a ticket with API Support.

On that page:

  • Consumer Key β€” you invent this. It is not issued by IBKR. Exactly 9 characters, alphanumeric (e.g. TILEDOM01). Record it verbatim: it becomes IBIND_OAUTH1A_CONSUMER_KEY, and a mismatch later fails auth with no useful error.
  • Upload public_signature.pem, public_encryption.pem, and dhparam.pem.
  • Generate the access token and access token secret β†’ IBIND_OAUTH1A_ACCESS_TOKEN and IBIND_OAUTH1A_ACCESS_TOKEN_SECRET.

3. Extract the DH prime (a hex string derived from your dhparam.pem; the portal never shows it) β†’ IBIND_OAUTH1A_DH_PRIME:

python3 -c "
import subprocess, re
out = subprocess.run(['openssl','dhparam','-in','dhparam.pem','-text'],
                     capture_output=True, text=True).stdout
m = re.search(r'(?:prime|P):\s*((?:\s*[0-9a-fA-F:]+\s*)+)', out)
print(re.sub(r'[\s:]', '', m.group(1)) if m else 'No prime found')
"

4. Keep the two private keys (private_encryption.pem, private_signature.pem) on disk and point IBIND_OAUTH1A_*_KEY_FP at them. In Kubernetes they are mounted at /etc/ibkr from the ib-oauth-keys secret.

Activation is not always instant. There's no formal approval process for first-party self-service (the 8–14 week compliance review applies to third-party OAuth vendors), but consumer keys have been reported taking 24 hours to ~2 weeks to go live, possibly tied to IBKR's weekend server restarts. Register early; a 401 on day one isn't necessarily a misconfiguration.

Loading the IBKR Credentials Into Kubernetes

All five string values plus the two private PEMs go into a single ib-oauth-keys secret β€” that is what the CronJob reads its env from and mounts at /etc/ibkr.

Do not try to build it with one kubectl create secret call. --from-env-file cannot be combined with --from-file, so a secret mixing string values and file content is impossible in a single invocation:

error: from-env-file cannot be combined with from-file or from-literal

Use the helper instead, which assembles the manifest and pipes it to apply:

./scripts/create_ib_oauth_secret.sh [key-dir]     # default: ~/ibkr-oauth-paper

key-dir must hold oauth.env, private_encryption.pem, and private_signature.pem. Write oauth.env with an editor, not the shell β€” --from-literal and export both leak values into shell history and into ps output while the command runs:

IBIND_OAUTH1A_CONSUMER_KEY=DFTRADEBO
IBIND_OAUTH1A_ACCESS_TOKEN=...
IBIND_OAUTH1A_ACCESS_TOKEN_SECRET=...
IBIND_OAUTH1A_DH_PRIME=...
IB_ACCOUNT_ID=DU1234567

The script refuses to apply anything if a value is empty or the consumer key isn't exactly 9 characters, strips stray whitespace (a trailing newline inside a token fails IBKR auth with an unhelpful error), never writes plaintext to disk, and prints only key names and byte lengths so no value reaches your terminal or scrollback. Re-running it updates the secret in place rather than erroring with "already exists".

Because it exits non-zero on failure, gate the cleanup on success β€” otherwise a failed apply still destroys the tokens you just pasted:

./scripts/create_ib_oauth_secret.sh && shred -u ~/ibkr-oauth-paper/oauth.env

Override the target with KUBE_CONTEXT, NAMESPACE, or SECRET_NAME env vars. The default context is luxvps β€” the cluster's default context is a different cluster that will simply hang.

Then enable the CronJob:

liveTrade:
  dryRun: "true"                 # first run only
  interactiveBrokers:
    enabled: true

Full runbook: docs/guides/live-trading.md.

Inspect Account & Portfolio

Each broker module is runnable directly to print the account summary and current positions β€” useful for sanity-checking credentials, account IDs, and mappings before running the copier:

# Collective2
uv run python -m tradingbot.livetrade.collective2

# Interactive Brokers (IBKR Web API; reads IB_ACCOUNT_ID + IBIND_OAUTH1A_* from .env)
uv run python -m tradingbot.livetrade.interactive_brokers

# eToro (reads ETORO_API_KEY, ETORO_USER_KEY, ETORO_DEMO from .env)
uv run python -m tradingbot.livetrade.etoro

# Darwinex (reads DARWINEX_USERNAME, DARWINEX_PASSWORD, DARWINEX_DEMO from .env)
uv run python -m tradingbot.livetrade.darwinex

2. Map Your Tickers

Yfinance symbols often differ from broker symbols (e.g., EURUSD=X vs EURUSD). The framework includes an Assisted Ticker Discovery script to help you map them:

# 1. Discover unmapped tickers from your bots and trades
uv run python -m tradingbot.livetrade.discover_symbols

# 2. Edit symbol_map.review.json in your editor
#    Add "selected_symbol" and "selected_type" for the tickers you want to map.

# 3. Apply the approved mappings to the master symbol_map.json
uv run python -m tradingbot.livetrade.discover_symbols --apply

3. Deploy the Copier

The copier runs as a standalone script per broker. Deploy as a CronJob to run shortly after your trading bots:

# Collective2
uv run python -m tradingbot.livetrade_collective2

# Interactive Brokers
uv run python -m tradingbot.livetrade_interactive_brokers

# eToro
uv run python -m tradingbot.livetrade_etoro

# Darwinex
uv run python -m tradingbot.livetrade_darwinex

Each broker is its own Helm CronJob gated by an independent flag in values.yaml β€” enable them separately (liveTrade.collective2.enabled, liveTrade.interactiveBrokers.enabled, liveTrade.etoro.enabled, liveTrade.darwinex.enabled), so you can run any combination or none. All default to false. You can also cap how much of the account each broker mirrors via LIVETRADE_PORTFOLIO_FRACTION (default 1.0 = full account; e.g. 0.5 = half).

See the Live Trading Guide for advanced configuration and mapping rules.

🎯 Example Bots

  • eurusdtreebot.py - Decision tree-based strategy for EUR/USD
  • feargreedbot.py - Uses Fear & Greed Index API for market sentiment
  • swingtitaniumbot.py - Swing trading strategy
  • xauzenbot.py - Gold (XAU) trading bot
  • sharpeportfoliooptweekly.py - Portfolio optimization with Sharpe ratio
  • aihedgefundbot.py - AI-driven portfolio rebalancing
  • deepseektoolbot.py - AI with tools (research + submit weights); cheap LLM sanity-check and main-LLM retry
  • gptbasedstrategytabased.py - GPT-based strategy with technical analysis

See Example Bots for implementation details.