Python algorithmic trading bot framework for Kubernetes: backtesting, hyperparameter optimization, 150+ technical analysis indicators (RSI, MACD, Bollinger Bands, ADX), portfolio management, PostgreSQL integration, Helm deployment, CronJob scheduling. Minimal overhead, production-ready, Yahoo Finance data.
39
stars
77
commits
Jupyter Notebook
primary language
Sep 8, 2026
updated
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.
The system is designed to be lightweight and stateless. Each "Bot" is a containerized instance triggered by a schedule.
ta library (Technical Analysis).BotClass manages the state of your portfolio in PostgreSQL.# 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"
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
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:
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:

Bot Detail Page includes:
The dashboard is deployed automatically with the Helm chart. See Deployment for setup.
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:
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.
| 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. |
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):
See AI Tools Guide and AITools API for details.
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)
Access over 150 indicators via the row object:
trend_macd, trend_adx, trend_ichimoku_a, trend_sma_fast, trend_sma_slowmomentum_rsi, momentum_stoch, momentum_ao, momentum_roc, momentum_ppovolatility_bbh (Bollinger High), volatility_bbl (Bollinger Low), volatility_atrvolume_vwap, volume_obv, volume_mfiSee Technical Analysis Guide for complete list.
Online Documentation: justinguese.github.io/python_tradingbot_framework/
run_ai_with_tools, run_ai_simple, run_ai_simple_with_fallbackThe 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.
[!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).
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
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:
TILEDOM01). Record it verbatim: it becomes
IBIND_OAUTH1A_CONSUMER_KEY, and a mismatch later fails auth with no useful error.public_signature.pem, public_encryption.pem, and dhparam.pem.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.
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.
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
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
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.
See Example Bots for implementation details.
77 commits
Jupyter Notebook
81.3%
Python
18.5%
Python algorithmic trading bot framework for Kubernetes: backtesting, hyperparameter optimization, 150+ technical analysis indicators (RSI, MACD, Bollinger Bands, ADX), portfolio management, PostgreSQL integration, Helm deployment, CronJob scheduling. Minimal overhead, production-ready, Yahoo Finance data.
39
stars
77
commits
Jupyter Notebook
primary language
Sep 8, 2026
updated
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.
The system is designed to be lightweight and stateless. Each "Bot" is a containerized instance triggered by a schedule.
ta library (Technical Analysis).BotClass manages the state of your portfolio in PostgreSQL.# 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"
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
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:
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:

Bot Detail Page includes:
The dashboard is deployed automatically with the Helm chart. See Deployment for setup.
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:
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.
| 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. |
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):
See AI Tools Guide and AITools API for details.
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)
Access over 150 indicators via the row object:
trend_macd, trend_adx, trend_ichimoku_a, trend_sma_fast, trend_sma_slowmomentum_rsi, momentum_stoch, momentum_ao, momentum_roc, momentum_ppovolatility_bbh (Bollinger High), volatility_bbl (Bollinger Low), volatility_atrvolume_vwap, volume_obv, volume_mfiSee Technical Analysis Guide for complete list.
Online Documentation: justinguese.github.io/python_tradingbot_framework/
run_ai_with_tools, run_ai_simple, run_ai_simple_with_fallbackThe 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.
[!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).
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
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:
TILEDOM01). Record it verbatim: it becomes
IBIND_OAUTH1A_CONSUMER_KEY, and a mismatch later fails auth with no useful error.public_signature.pem, public_encryption.pem, and dhparam.pem.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.
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.
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
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
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.
See Example Bots for implementation details.
77 commits
Jupyter Notebook
81.3%
Python
18.5%