π€ 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 -

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.
- Ingestion: Fetches data from Yahoo Finance (with DB caching).
- Analysis: Enriches data with the
talibrary (Technical Analysis). - Execution:
BotClassmanages the state of your portfolio in PostgreSQL. - 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).

Overview Dashboard shows:
- Current Worth, Total Return %, Annualized Return %
- Sharpe Ratio, Sortino Ratio, Max Drawdown %
- Volatility, Total Trades, Start Date

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:
- Deployment Overview - Complete deployment options
- Kubernetes Deployment - Cluster setup
- Helm Charts - Configuration details
π§° 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 vialocal_backtest(). Bots that overridemakeOneIteration()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, preferdecisionFunction.
Key Methods
| Method | Description |
|---|---|
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):
- get_market_data(symbol, period) β Market data (OHLCV), default last two weeks.
- get_portfolio_status() β Current portfolio worth (USD) and holdings.
- get_recent_trades(limit) β Recent trades; for sells, profit of the closed trade is shown.
- get_stock_news(symbol, limit) β Recent news for a symbol (title, link, publisher, published_at) from the database.
- get_stock_earnings(symbol, limit) β Recent earnings dates and EPS (estimate, reported, surprise %) for a symbol from the database.
- 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
- Deployment Overview - Kubernetes vs local deployment options
- Kubernetes Deployment - Cluster setup and configuration
- Helm Charts - Bot scheduling and Helm configuration
Guides
- Technical Analysis - Complete indicator reference
- Portfolio Management - Advanced portfolio operations
- Local Development - Development workflows
- AI Tools - LangChain + OpenRouter tools; cheap-first with fallback and sanity checks
- Telegram Monitor - Channel monitoring, AI summarization, ticker extraction
API Reference
- Bot API - Complete Bot class documentation
- Data Service - Data fetching and caching
- Portfolio Manager - Trading operations
- AITools API -
run_ai_with_tools,run_ai_simple,run_ai_simple_with_fallback
π‘ 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 becomesIBIND_OAUTH1A_CONSUMER_KEY, and a mismatch later fails auth with no useful error. - Upload
public_signature.pem,public_encryption.pem, anddhparam.pem. - Generate the access token and access token secret β
IBIND_OAUTH1A_ACCESS_TOKENandIBIND_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.