Autonomous trading system with technical analysis, market regime detection, AI validation, and self-optimization. Designed to run 24/7 on Raspberry Pi 4.
- Project Philosophy
- Overview
- System Architecture
- Strategy Engine
- Risk Management
- AI Layers
- Scheduler and Timing
- Market Hours by Asset
- Telegram Notifications
- State and Data Persistence
- Installation
- Configuration
- Operation
- Monitoring and SQL Queries
- Project Structure
- Tech Stack
Generate real returns from financial markets with the minimum available resources — and never lose a cent of what has already been earned.
This project was born with concrete constraints: modest hardware (Raspberry Pi 4), a limited investment budget, and free or low-cost APIs. Those constraints are part of the design, not a temporary obstacle. Every technical decision must optimize what we have, not assume resources that don't exist.
1. Minimum profitability is enough. The goal is not the perfect trade or maximum alpha. A consistent, small gain sustained over time meets the objective. The bot doesn't need to be the most aggressive — it needs to be the most reliable.
2. Gains are protected above all else. Trailing stop, automatic break-even, P17 (exit monitor), P15 (multi-timeframe confirmation): every layer exists to prevent a gain from turning into a loss. Protecting capital is as important as generating it.
3. Demo phase = learning, not paralysis. We're in paper trading to learn. Demo losses don't hurt financially, but they do teach. More trades executed = more data = better strategy calibration. Being too strict with entry filters doesn't protect — it blinds the bot to real opportunities.
4. Technical resources as strategic assets. Every tool (Claude, Gemini, eToro API, SQLite, Telegram) was chosen for its cost-benefit ratio. Before adding a new technology or dependency, we evaluate whether the value it provides justifies the operational cost.
5. Code without technical debt. No Frankenstein code. No duplication. No features that don't add measurable value. Every change goes through the question: does this improve the system or just complicate it? Simplicity is a competitive advantage when resources are limited.
A single bot with capital synchronized to an eToro demo account operating in demo mode. Each cycle (every 4 hours, aligned to candle closes) detects the market regime per symbol and applies the most appropriate technical strategy. An autonomous agent (Advisor) reviews historical performance weekly or after losing streaks, and adjusts parameters conservatively.
Supports crypto assets (24/7) and ETFs/stocks with restricted market hours (e.g. QQQ, 9:30–16:00 ET).
Historical evolution: The system started as 3 parallel bots with fixed strategies (BOT-A SMA / BOT-B Combined / BOT-C Aggressive). It was consolidated into a single bot with dynamic strategy selection based on market regime.
+---------------------------------------------------------------------------+
| while running: |
| |
| 1. run_cycle() <- technical analysis + execution per symbol |
| | |
| 2. sleep(10 min) <- post-cycle window |
| | |
| 3. advisor check? <- weekly or 3 consecutive losses |
| | +-- _run_advisor() -> background thread (non-blocking) |
| | |
| 4. get_next_cycle_time() <- calculates time to next aligned slot |
| | |
| 5. sleep(remaining) <- sleeps until next cycle |
| | +-- intercept 22:00 -> _send_daily_summary() |
| |
+---------------------------------------------------------------------------+
run_cycle()
+-- check_daily_loss_limit()
|
+-- for symbol in symbols:
process_symbol(symbol)
+-- is_market_open(symbol) <- skip if market closed
+-- data_provider.update_prices() <- eToro API -> cache
+-- get_recent_candles(symbol, "4h", 100) <- last 100 candles
+-- get_recent_candles(symbol, "1d", 60) <- HTF (Higher Timeframe)
+-- strategy_engine.analyze()
| +-- detect_market_regime() <- ATR% + HTF -> regime
| +-- apply strategy <- RSI / SMA / Combined
| +-- dynamic SL/TP (ATR) <- entry ± ATR × multiplier
| +-- ai_validator.validate() <- Gemini 2.0 Flash + F&G
+-- [if confidence >= min_confidence]
| +-- check spread < max_spread_pct <- spread filter
| +-- calc size (risk-based) <- risk management
| +-- BUY -> open_position() -> exchange.create_order()
| +-- SELL -> close_position() -> exchange.close_position()
+-- check_position_stops(live_price) <- SL/TP in real time
StrategyEngine.detect_market_regime(data_4h, data_1d) calculates each cycle:
| Condition | Regime | Strategy |
|---|---|---|
| ATR% < 1.5% | RANGING |
RSI mean reversion |
| htf_strength > 0.5 and HTF directional | TRENDING |
SMA crossover |
| Otherwise | MIXED |
Combined (voting) |
When the regime changes, an immediate Telegram alert is sent with the new regime and ATR%.
SMA Crossover (sma_cross) — fast=10, slow=30. BUY on golden cross, SELL on death cross. Volume filter > 80% of 20-candle average.
RSI Mean Reversion (rsi_reversal) — period 14. BUY if RSI < 30, SELL if RSI > 70.
Combined (combined) — weighted voting of SMA and RSI. Threshold 0.7.
Auto (auto) — calls detect_market_regime() per cycle and symbol, selects the optimal strategy automatically.
Each cycle, the strategy engine emits a signal per symbol. The bot's action depends on the emitted state:
| State | Emoji | Meaning | Bot Action |
|---|---|---|---|
BUY |
🟢 | Entry conditions met (bullish indicators aligned) | Opens long position |
SELL |
🔴 | Exit conditions (bearish indicators or RSI overbought) | Closes existing position |
HOLD |
🟡 | Neutral market — no clear trend or RSI extreme | No action. Open position continues with trailing stop |
HOLD_BULLISH |
🟡 | Ongoing bullish trend, but missing confirmation (RSI, volume or spread) | Does not open position directly. P18 BuyMonitor uses it as favorable context to look for a 1H entry |
HOLD_BEARISH |
🟠 | Ongoing bearish trend without an explicit death cross | Does not close directly. P17 ExitMonitor uses it as an early exit signal in momentum_1h |
| Strategy | BUY |
SELL |
HOLD_BULLISH |
HOLD_BEARISH |
HOLD |
|---|---|---|---|---|---|
sma_cross |
SMA10 crosses above SMA30 (golden cross) | SMA10 crosses below SMA30 (death cross) | SMA10 > SMA30 × 1.005 without a new cross | SMA10 < SMA30 × 0.995 | No clear differential |
rsi_reversal |
RSI < 30 (oversold) | RSI > 70 (overbought) | — | — | RSI between 30–70 |
combined |
Weighted SMA+RSI vote ≥ 0.7 | Vote ≤ −0.7 | SMA vote is HOLD_BULLISH |
SMA vote is HOLD_BEARISH |
Inconclusive vote |
momentum_1h |
EMA9 > EMA21 + RSI 40–65 + volume ≥ 1× average | — | EMAs aligned but RSI or volume insufficient | EMA9 < EMA21 (bearish on 1H) | Neutral |
momentum_1his the P18 (BuyMonitor) strategy for detecting entries on 1H candles. It does not generateSELL— closing is the responsibility of the trailing stop and P17 ExitMonitor.
StrategyEngine._calculate_atr(data, period=14) is the central volatility calculation method. It is called by both detect_market_regime() and all 3 strategies to calculate dynamic SL/TP, eliminating code duplication.
StrategyEngine._compute_rsi(closes, period=14) implements Wilder's RSI and is the single source of truth for this indicator in the project. All modules that need RSI (P15, P18, rsi_mean_reversion) call this static method — there are no alternative implementations.
technical signal -> GeminiTradeValidator.validate()
+ Fear & Greed Index (alternative.me)
+-- CONFIRM -> confidence *= boost_factor
+-- REJECT -> signal discarded
+-- REDUCE_SIZE -> position_value *= 0.5
+-- NEUTRAL -> no modification (signal continues)
+-- WAIT -> converted to HOLD
Position size is calculated dynamically to risk a fixed percentage of capital (default 1%) if the Stop Loss is hit.
risk_amount = capital * risk_per_trade_pct # e.g. 1%
sl_distance = abs(entry - stop_loss)
size = risk_amount / sl_distance- Safety cap: Size will never exceed
max_position_pct(default 25% of capital). - AI adjustment: If the AI validator returns
REDUCE_SIZE, the calculated size is cut in half.
SL and TP levels are calculated based on the asset's real ATR:
| Strategy | Stop Loss | Take Profit |
|---|---|---|
sma_cross |
entry - ATR × 2.0 |
entry + ATR × 3.0 |
rsi_reversal |
entry - ATR × 1.5 |
entry + ATR × 2.5 |
combined |
entry - ATR × 2.0 |
entry + ATR × 3.5 |
Fallback to fixed percentages (−5%/+10%) if data is insufficient to calculate ATR.
The bot doesn't always enter immediately at the 4H candle close. It uses parallel threads to optimize the entry price:
- MTF Monitor (Multi-Timeframe - P15): If the 4H signal is BUY, defers entry to a monitor that analyzes 1H/15M candles. Seeks confirmation (RSI not overbought) to enter at a better intra-cycle moment.
- Entry Monitor (P14): If price has spiked, queues the order and waits for a specific pullback before executing.
To avoid overexposure to systemic crypto market risk:
- Calculates Pearson Correlation (30-cycle window) between the candidate asset and existing open positions.
- If correlation exceeds
max_correlation(default 0.75), the entry is blocked.
Tiered Trailing Stop (5 levels):
- Break-even (gain ≥
profit_lock_pct0.5%): SL → entry price. Guaranteed minimum protection. - Phase 2 — Immediate trail (≥ 0.5×ATR): SL = peak − 0.5×ATR. No dead zone: captures gain from the first tick above the trigger.
- Phase 3 — Continuous trail (≥ 2.0×ATR): SL = peak − 0.5×ATR. Same formula, reflects larger gains.
- Phase 4 — Maximum lock-in (≥ 3.5×ATR): SL = peak − 0.25×ATR — maximum accumulated profit protection.
Each escalation generates a Telegram alert. trailing_phase (0–4) is persisted in the state.
Levels are checked against the real-time live ticker price every cycle.
Before opening a trade, the spread ((ask - bid) / bid) is verified. If it exceeds max_spread_pct (default 0.2%), the entry is cancelled to avoid excessive execution costs.
P&L is calculated net of estimated commissions (fee_rate_pct, default 0.1% per leg).
Net PnL = (Exit - Entry) * Size - Fees
If daily_pnl / daily_start_capital exceeds max_daily_loss_pct (default 5%), the cycle aborts without processing signals. Resets at midnight.
Three consecutive losses trigger the emergency Advisor.
If all symbols accumulate 6 consecutive cycles without executing any trade (only HOLDs), the Advisor activates early to evaluate pair rotation. The consecutive_hold_cycles counter is persisted in the state JSON between restarts.
Model: Gemini 2.0 Flash — validates technical signals before each position opening. The prompt includes the crypto market Fear & Greed Index (src/utils/fear_greed.py) for sentiment context.
Models: Claude Opus 4.6 → Gemini 2.0 Flash (automatic fallback on billing/quota errors).
The active backend is controlled with the AI_PRIMARY variable in .env (claude by default). If AI_PRIMARY=gemini, the Advisor uses Gemini directly without attempting Claude.
When it activates:
- 7 days since last session (routine review)
- 3 consecutive losses (emergency intervention)
- 6 consecutive cycles without any trade (dead market)
- Manually:
python3 run_advisor.py "reason"
Available tools:
| Tool | Description |
|---|---|
read_config |
Reads config/config_auto.json |
read_performance |
SQLite: win rate, P&L, drawdown |
read_logs |
Last N lines of logs/bot.log |
update_config |
Writes changes (whitelist of parameters only) |
restart_bots |
Executes ./bot.sh restart |
scan_market |
Scans 15 pairs: ATR%, volume, trend |
| Backtest context | Automatically reads data/backtest_results.json (HybridBacktest) and data/pipeline_backtest_results.json (PipelineBacktest) when building the prompt |
Pre-rotation signals: the Advisor does NOT rotate a symbol if it detects signs of imminent movement:
atr_pct < 1.0%→ Bollinger Squeeze (imminent breakout, wait)vol_trend_pct > 20%with flat trend → volume accumulation (movement incoming)
Fear & Greed Index: injected into the user message before each Advisor session for macro market context.
Cross-session memory: each parameter change is recorded in the advisor_changes table (SQLite). On startup, the Advisor automatically receives the history of the last 3 days to avoid re-applying redundant adjustments.
Operator context notes: advisor_notes table in the DB. Allows injecting persistent context (e.g. cause of a specific loss, infrastructure changes) that the Advisor reads before making decisions. Add with:
INSERT INTO advisor_notes (created_at, note, active) VALUES (date('now'), 'your note here', 1);Safety guardrails:
| Parameter | Allowed range | Max change/session |
|---|---|---|
min_confidence |
0.45 – 0.80 | 10% |
max_position_pct |
0.20 – 0.95 | 10% |
max_daily_loss_pct |
2 – 10% | 10% |
update_interval_hours |
2 – 8h (integer) | 10% |
risk_per_trade_pct |
0.5 – 2.5% | 10% |
max_spread_pct |
0.1 – 0.5% | 10% |
ai_validation.min_ai_confidence |
0.50 – 0.80 | 10% |
exit_monitor_min_confidence |
0.45 – 0.75 | 10% |
buy_monitor_min_confidence |
0.55 – 0.80 | 10% |
profit_lock_pct |
0.3 – 2.0% | 10% |
mtf_rsi_max |
50 – 70 | 10% |
momentum_1h_rsi_min |
30.0 – 50.0 | 10% |
momentum_1h_vol_ratio_min |
0.7 – 1.5 | 10% |
buy_monitor_cooldown_hours |
0.0 – 8.0h | 10% |
get_next_cycle_time(interval_hours=4) aligns cycles to round UTC hours: [0, 4, 8, 12, 16, 20].
Safety check: if the next slot is less than 2 minutes away, it skips to the next one.
Daily summary: the bot intercepts the sleep of the cycle that crosses 22:00 local time and sends the summary before continuing.
Crypto assets trade 24/7. ETFs and stocks are only analyzed and traded during market hours. Outside those hours the symbol is automatically skipped with a log message.
Assets with restricted hours (NYSE/NASDAQ): 9:30–16:00 ET, Monday to Friday.
| Symbol | Hours |
|---|---|
| QQQ, SPY | 09:30–16:00 ET (Mon-Fri) |
| GLD, SLV | 09:30–16:00 ET (Mon-Fri) |
| AAPL, MSFT, TSLA, NVDA | 09:30–16:00 ET (Mon-Fri) |
To add a new asset with restricted hours, edit RESTRICTED_ASSETS in src/utils/market_hours.py.
| Event | When |
|---|---|
| Position opened | Immediately |
| Position closed | Immediately (includes P&L) |
| Stop Loss / Take Profit | When triggered |
| Strategy change (auto) | When regime changes (with ATR%) |
| Critical error | System exceptions |
| Daily summary | Every day at 22:00 |
| Advisor change | When parameter is modified (with reasoning) |
Cycles with no activity (only HOLDs) do not generate notifications — controlled by "cycle_summary": false.
| Command | Action |
|---|---|
/status |
General status: cycle, strategy, symbols, daily P&L |
/balance |
Cash, position values, unrealized and total P&L |
/positions |
Open positions with entry, current price, P&L, SL/TP |
/signals |
Signals generated in the last cycle (action, confidence, regime) |
/next |
Time until the next analysis cycle (available from the first second of startup) |
/pi |
Hardware health: CPU, temperature, RAM, disk, uptime |
/ask [question] |
Opens a persistent chat session with the AI (Claude Haiku 4.5 or Gemini 2.0 Flash based on AI_PRIMARY). Maintains conversation history (max 20 exchanges). Auto-closes on inactivity (warning at ~30s, close at 60s). Any / command closes the session. Without parameter: starts empty chat |
/setai [claude|gemini] |
Change primary AI at runtime (persists to .env). Without parameter: guided flow |
/analyze [symbol] |
Full technical analysis + AI verdict on demand (crypto, stocks, ETFs). Without parameter: guided flow |
/pause |
Block BUY/SELL execution (analysis continues) |
/resume |
Resume normal trading |
/add [symbol] |
Add symbol to the next cycle (validates on exchange, persists config). Without parameter: guided flow |
/remove [symbol] |
Remove active symbol (warns if position is open). Without parameter: guided flow |
/advisor |
Launch the Advisor manually from Telegram |
/reload |
Reload config_auto.json without restarting the bot |
/scout |
Proactive asset rotation scan via Advisor |
/rotate [current] [new] |
Manually rotate symbol (validates cooldown and universe). Without parameters: guided 2-step flow |
/weekly |
Generate weekly report on demand (trades, win rate, PnL, best strategy) |
/backtest [symbol] |
Run a quick historical backtest with Binance data |
/verbose |
Toggle verbose mode: HOLD signals generate a Telegram alert with full analysis (strategy, regime, RSI, ATR%, SL/TP, reasoning) |
/issue [description] |
Create a GitHub issue directly from Telegram with technical bot context (capital, positions, active symbol) |
/cancelar |
Cancel ongoing guided flow |
/help |
List of all available commands (22 total) |
The listener uses long polling (20s server-side timeout) — instant response, ~3 req/min. 22 commands available. The BotFather menu updates automatically on each startup via setMyCommands — no manual intervention required.
The bot calls setMyCommands automatically on startup — the menu syncs without manual steps. If you need to register them manually (e.g. in BotFather for another bot), send to @BotFather → /setcommands and paste:
status - General bot status
balance - Capital, positions and total P&L
positions - Open positions with SL/TP
signals - Signals from the last cycle
next - Time until next cycle
pi - CPU, RAM, disk and Pi temperature
ask - Ask the AI about the bot
add - Add active symbol (e.g: add BTC/USDT)
remove - Remove active symbol
pause - Pause BUY/SELL execution
resume - Resume trading
advisor - Launch the Advisor manually
setai - Change primary AI (claude or gemini)
analyze - Technical analysis + AI on demand
reload - Reload config without restarting
scout - Proactive asset rotation scan
rotate - Manually rotate symbol (e.g: rotate BTC ETH)
weekly - Generate weekly performance report
backtest - Quick historical test (e.g: backtest BTC/USDT)
verbose - Toggle detailed HOLD alerts
issue - Create GitHub issue from Telegram
cancelar - Cancel ongoing operation
help - List of available commands
Persists capital, open positions, active strategies per symbol, daily P&L, and the static cycle counter. Automatically restored on restart.
{
"capital": 54772.08,
"daily_pnl": 0,
"daily_start_capital": 54772.08,
"last_reset_date": "2026-02-21",
"positions": {},
"strategy_per_symbol": {
"ARB/USDT": "sma_cross",
"POL/USDT": "sma_cross"
},
"consecutive_hold_cycles": 0,
"pending_entries": {},
"mtf_pending": {},
"saved_at": "2026-02-21T19:45:43"
}Three tables, all in WAL mode for greater integrity against power cuts:
| Table | Content |
|---|---|
trades |
One record per closed position: P&L, strategy, AI fields (ai_recommendation, ai_confidence, ai_sentiment, ai_reasoning) |
signals |
Every signal generated per cycle (BUY/SELL/HOLD), with symbol, action, confidence, strategy, regime, and ATR% |
validator_decisions |
Every AI Validator decision on BUY/SELL signals: recommendation, AI confidence, risk level, reasoning |
| File | Content |
|---|---|
bot.log |
Main log: cycles, signals, trades, scheduler |
error.log |
Errors and exceptions |
| Requirement | Minimum version | Notes |
|---|---|---|
| Python | 3.11+ | Raspberry Pi OS Bookworm includes 3.11. To install 3.13: sudo apt install python3.13 |
| git | any | sudo apt install git |
| RAM | 2 GB | Pi 4 / Pi 5 |
# 1. Clone the repository
git clone <repo-url>
cd trading
# 2. Create virtual environment
python3 -m venv venv
source venv/bin/activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Create directory structure (if it doesn't exist)
mkdir -p data config logs
# 5. Configure credentials
cp .env.example .env # if it exists, or create manually (see Environment Variables section)
nano .env
# 6. Copy bot configuration
cp config/config_example.json config/config_auto.json # if an example exists
# Or copy from another server: scp user@old-pi:projects/trading/config/config_auto.json config/
# 7. Verify installation (must complete without errors)
python3 -c "import ccxt, pandas, numpy, anthropic, google.generativeai, matplotlib, yfinance, psutil, dotenv; print('OK')"
# 8. Start the bot
bash bot.sh startThe SQLite database (
data/trading.db) and all tables are created automatically on the first run. No manual initialization step is required.
# eToro API (demo)
ETORO_PUBLIC_KEY=your_public_key
ETORO_PRIVATE_KEY=your_private_key
ETORO_ENV=demo
# Telegram
TELEGRAM_BOT_TOKEN=your_token
TELEGRAM_CHAT_ID=your_chat_id
# Per-cycle AI Validation (required)
GOOGLE_API_KEY=your_google_key
# Trading Advisor (optional — without key, uses Gemini as primary)
ANTHROPIC_API_KEY=your_anthropic_key
# Primary AI: 'claude' (Opus + Gemini fallback) or 'gemini' (Gemini only)
AI_PRIMARY=claude{
"_bot_name": "AUTO BOT",
"strategy": "auto",
"symbols": ["ARB/USDT", "POL/USDT"],
"capital": 54772.08,
"min_confidence": 0.60,
"max_position_pct": 0.05,
"max_daily_loss_pct": 5,
"update_interval_hours": 4,
"exchange": { "name": "etoro", "testnet": true },
"ai_validation": {
"enabled": true,
"min_ai_confidence": 0.6,
"override_on_reject": false,
"boost_on_confirm": true
},
"telegram": {
"enabled": true,
"notifications": {
"startup": true,
"cycle_summary": false,
"trades": true,
"daily_summary": true
}
}
}| Parameter | Effect |
|---|---|
min_confidence |
Threshold to execute signals (higher = more selective) |
max_position_pct |
Fraction of capital per position (currently 5%) |
max_daily_loss_pct |
Daily circuit breaker |
update_interval_hours |
Cycle frequency |
symbols |
Pair rotation (requires Advisor justification with scan data) |
Steps to migrate to a new Pi without losing history or active positions.
ssh your-pi "cd projects/trading && bash bot.sh stop"# From the local machine (adjust IPs/hostnames to your network)
PI_OLD=your-pi
PI_NEW=oajm-pi5 # adjust to the Pi 5 hostname/IP
# Full database (trade history, signals, Advisor changes)
scp ${PI_OLD}:projects/trading/data/trading.db ${PI_NEW}:projects/trading/data/
# Bot state (capital, open positions, cooldowns, counters)
scp ${PI_OLD}:projects/trading/data/state_auto.json ${PI_NEW}:projects/trading/data/
# Active configuration
scp ${PI_OLD}:projects/trading/config/config_auto.json ${PI_NEW}:projects/trading/config/
# Credentials
scp ${PI_OLD}:projects/trading/.env ${PI_NEW}:projects/trading/
# Backtest results (context for Advisor — optional but recommended)
scp ${PI_OLD}:projects/trading/data/backtest_results.json ${PI_NEW}:projects/trading/data/ 2>/dev/null
scp ${PI_OLD}:projects/trading/data/pipeline_backtest_results.json ${PI_NEW}:projects/trading/data/ 2>/dev/null
# Historical logs (optional)
scp ${PI_OLD}:projects/trading/logs/bot.log ${PI_NEW}:projects/trading/logs/ssh ${PI_NEW} "cd projects/trading && source venv/bin/activate && \
python3 -c \"
import sqlite3, json, os
db = sqlite3.connect('data/trading.db')
tables = db.execute(\\\"SELECT name FROM sqlite_master WHERE type='table'\\\").fetchall()
trades = db.execute('SELECT count(*) FROM trades').fetchone()[0]
db.close()
state = json.load(open('data/state_auto.json'))
print('Tables:', [t[0] for t in tables])
print('Historical trades:', trades)
print('Capital in state:', state.get('capital'))
print('Open positions:', list(state.get('positions', {}).keys()))
print('OK')
\""ssh ${PI_NEW} "cd projects/trading && bash bot.sh start"If there are open positions during migration: the bot detects them automatically from
state_auto.jsonand resumes tracking (trailing stop, P14/P15/P17 monitors). The SL registered in eToro remains active throughout the migration — there is no risk of unprotected positions.
| File | Reason |
|---|---|
data/cache/ |
OHLCV cache — regenerates automatically |
venv/ |
Rebuild with pip install -r requirements.txt |
logs/error.log |
Errors from the old Pi are not relevant |
data/*.tmp |
Atomic write temporary files |
# Start / stop / restart
./bot.sh start
./bot.sh stop
./bot.sh restart
# Process status
./bot.sh status
# Real-time logs
./bot.sh follow
# Last N lines
./bot.sh logs 200
# Trade statistics
./bot.sh stats
# Trigger the Advisor manually
python3 run_advisor.py "Reason for analysis"
# Run tests
source venv/bin/activate && python3 -m pytest tests/ -vNote:
BOT_CONFIGis already configured internally inbot.sh— it does not need to be passed as an environment variable.
Restarts: the bot automatically restores capital, open positions, active strategies per symbol, daily P&L, and
consecutive_hold_cycles. It does not generate false strategy change alerts.
# Recent trades with P&L
sqlite3 data/trading.db "
SELECT symbol, strategy,
round(entry_price,2), round(exit_price,2),
round(pnl,2) as pnl, round(pnl_pct,2) as pct,
ai_recommendation, created_at
FROM trades ORDER BY created_at DESC LIMIT 10;"
# Performance by strategy
sqlite3 data/trading.db "
SELECT strategy,
count(*) as trades,
round(100.0*sum(case when pnl>0 then 1 else 0 end)/count(*),1) as win_rate,
round(sum(pnl),2) as total_pnl
FROM trades GROUP BY strategy;"
# AI Validator impact (from trades table)
sqlite3 data/trading.db "
SELECT ai_recommendation,
count(*) as total,
round(100.0*sum(case when pnl>0 then 1 else 0 end)/count(*),1) as win_rate,
round(avg(pnl),2) as avg_pnl
FROM trades WHERE ai_recommendation != 'N/A'
GROUP BY ai_recommendation;"
# Validator decisions (all, including signals that never became trades)
sqlite3 data/trading.db "
SELECT recommendation, count(*) as total,
round(avg(ai_confidence),2) as avg_conf,
round(100.0*sum(should_execute)/count(*),1) as execute_pct
FROM validator_decisions
GROUP BY recommendation ORDER BY count(*) DESC;"
# Signal distribution by action and symbol
sqlite3 data/trading.db "
SELECT symbol, action, count(*) as total,
round(avg(confidence),2) as avg_conf
FROM signals
GROUP BY symbol, action ORDER BY symbol, count(*) DESC;"trading/
├── trading_bot.py # Main bot (TradingBot class)
├── run_advisor.py # Manual Advisor trigger
├── bot.sh # Bot manager (start/stop/restart/status/logs/stats)
├── diagnose.sh # System diagnostics
├── .env # Credentials (never in git)
├── requirements.txt
│
├── src/
│ ├── exchanges/
│ │ └── etoro.py # eToro API wrapper (CCXT-compatible interface)
│ ├── core/
│ │ ├── claude_advisor.py # Autonomous Advisor (Claude Opus 4.6 / Gemini fallback)
│ │ ├── ai_validator.py # Per-cycle validation with Gemini 2.0 Flash + Fear & Greed
│ │ └── telegram_notifier.py
│ ├── strategies/
│ │ └── strategies.py # StrategyEngine: SMA, RSI, Combined, Auto + dynamic ATR + HTF
│ ├── analysis/
│ │ └── backtest.py # HybridBacktest (4H) + PipelineBacktest (1H+4H, full flow)
│ ├── data/
│ │ └── data_provider.py # eToro (primary) + Yahoo Finance (fallback) + OHLCV cache + SQLite gateway
│ └── utils/
│ ├── market_hours.py # Market hours by asset (ETFs/stocks vs crypto)
│ ├── fear_greed.py # Fear & Greed Index (alternative.me API)
│ ├── scheduler.py # Cycles aligned to 4h UTC candle closes
│ └── logger.py
│
├── config/
│ └── config_auto.json # Active configuration
│
├── data/
│ ├── trading.db # SQLite (WAL mode): trade history
│ ├── cache/ # OHLCV cache (parquet, 4h validity)
│ └── state_AUTO_BOT.json # State: capital, positions, strategies, hold_cycles
│
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Synthetic OHLCV fixtures
│ └── test_strategies.py # 19 tests: SMA, RSI, Combined, _calculate_atr
│
└── logs/
├── bot.log # Main log
└── error.log # Errors and exceptions
| Component | Technology |
|---|---|
| Exchange | eToro Public API (demo mode) |
| OHLCV Data | eToro API (primary) + Yahoo Finance (fallback) |
| Per-cycle AI | Gemini 2.0 Flash |
| AI Advisor | Claude Opus 4.6 → Gemini 2.0 Flash (fallback) |
| Fear & Greed | alternative.me API (free) |
| Notifications | Telegram Bot API |
| Database | SQLite WAL mode (data/trading.db) |
| OHLCV Cache | Parquet (4h validity) |
| Scheduler | Custom, aligned to 4h UTC candle closes |
| Tests | pytest + pytest-cov (19 tests) |
| Target hardware | Raspberry Pi 4 (4 GB RAM) / Raspberry Pi 5 (8–16 GB RAM) |
Detected: February 2026
| Format | Result |
|---|---|
instrumentIds=100000 (1 ID) |
✅ 200 OK |
instrumentIds=100000,100001 (comma) |
❌ 500 Internal Server Error |
instrumentIds=100000&instrumentIds=100001 (repeated) |
Workaround: fetch_ticker() makes an individual call per symbol. Minimal impact.
Implemented in v3.19 (P8): StateManager.save() now writes to a temporary file (.tmp) and renames it with os.replace(), an atomic operation on Unix. If the Pi shuts down during the write, load() recovers the state from .tmp.
Detected: February 2026
Instrument numeric IDs are hardcoded in etoro.py. If eToro changes these IDs, the bot will fail for those symbols.
Detected: February 2026
The WAIT recommendation from the validator is converted to HOLD. If the original signal was an exit (SELL), this could block a necessary sale.
- Credentials only in
.env(included in.gitignore) - The Advisor only modifies parameters from a strict whitelist with a 10% change limit per session
- All modifications notified via Telegram with model reasoning
- Paper trading by default (eToro demo, no real funds)
See ROADMAP.md for the full details of implemented and planned features.
- Centralized
_calculate_atr()— elimination of duplicate ATR calculation - Dynamic ATR-based SL/TP in all 3 strategies (with fallback to fixed %)
- Fear & Greed Index in AI Validator and Advisor
- SQLite WAL mode in all DB accesses
- Pre-rotation signals in Advisor SYSTEM_PROMPT (Bollinger Squeeze, volume accumulation)
- pytest test suite (19/19 passing)
signalstable — persists all generated signals (BUY/SELL/HOLD) per cyclevalidator_decisionstable — persists each AI Validator decision for performance analysisAI_PRIMARYvariable in.env— controls AI backend across the entire project without touching code/setaicommand — changes primary AI from Telegram with immediate effect and persistence/analyzecommand — technical analysis + AI validation on demand for any asset- Advisor
_read_performance()enriched: context for new bot + validator stats
_restart_bots():Popen(start_new_session=True)— bot survives Advisor restartself._lock = threading.RLock()in TradingBot — shared reentrant lock- Snapshots in
run_cycle,save_state,/add,/remove,/balance,/positions
check_position_stops(): phase 1 break-even (gain >= 1.5xATR, SL to entry) + phase 2 trail (SL follows price at 2xATR)- Telegram alert when break-even activates
size = (capital x risk_per_trade_pct%) / abs(entry - SL), capped atmax_position_pct(25%)risk_usdsaved in position; detailed log with sizing method
open_position()cancels entry if(ask - bid) / bid > max_spread_pct(0.2%) + Telegram alert
fees = (entry_notional + exit_notional) x fee_rate_pct%; capital and daily_pnl use net value- DB:
feesandpnl_grosscolumns (idempotent migration)
- New
src/core/state_manager.pywith ownRLockfor atomic I/O TradingBotdelegatessave_state()/load_state()to StateManager; removed_state_file()
- Complete Thread Safety:
close_positionuses atomicpop()under lock;run_cyclewritescurrent_price/atrunder lock;check_position_stopsreads position under lock;capital/daily_pnlunder lock - AI Validation fix:
ai_min_confidencewas used before initialization → AI silently disabled even when config saidenabled: true - Telegram startup retry:
test_connection()retries 3× with 3s backoff - Exchange startup retry:
setup_exchange()retries 3× with 5s backoff consecutive_hold_cyclespersists between restarts: was being overwritten with= 0afterload_state()- Telegram listener backoff: sleeps 30s after 3 consecutive failed polls (network down)
_get_symbol_performance()implemented: Advisor was calling it but it never existed →AttributeErroron every rundaily_summary_senttiming: flag was set before sending; if it failed, the summary was lost- Unrealized P&L None guard:
(pos.get('current_price') or entry)avoidsTypeError /rotatewhitelist fail-safe: rejects rotation ifMARKET_UNIVERSEdoesn't load- SQLite connection leak:
_read_performancenow withtry/finallyand guaranteed close advisor_changes/advisor_notestables in_init_db(): clean install no longer fails- ZeroDivisionError in
_calculate_atr: guardif closes[-1] == 0: return None, None - Duplicate
self.last_signals: second initialization in__init__removed
- Guided prompts: commands that require parameters (
/analyze,/ask,/add,/remove,/setai,/rotate) now work correctly from the Telegram menu. When they arrive without arguments, the bot responds with a contextual prompt and waits for the user's reply./rotateuses a 2-step flow (symbol to replace → new symbol). Writingcancelaror/cancelaraborts the flow at any point /nextfrom startup:next_cycle_timeis now calculated before entering the main loop, eliminating the initial period where/nextshowed "No information yet"telegram_notifier.py: removedtext.startswith('/')filter incheck_commands()— plain text responses from the guided flow now reach the handler (security maintained bychat_idfilter)
- New
DailyAuditorclass with 5 levels: instances, filesystem, data integrity, code consistency, and AI analysis. Runs as a daemon thread at 22:00. - Level 0: detects multiple bot instances via
/proc/<pid>/cmdline - Full Telegram report with critical issues, warnings, and AI diagnostics
- P7:
check_position_stops()rewritten with 4 progressive phases (break-even → 0.5×ATR);trailing_phasesaved in position; distinct Telegram alert per escalation - P8:
StateManager.save()uses atomic write via.tmp+os.replace();load()recovers from.tmpon shutdown during write
- Sub-hour:
run_forever()supportsupdate_interval_hours < 1; proportional post-cycle sleep (30s for sub-hour); dynamic candle timeframe based onupdate_interval_hours - P14 — Entry Monitor:
pending_entriesdict with pending BUY signals; daemon thread checks price every N minutes; executes entry when price ≤entry_targetor spread is low; discards signal at the start of the next cycle - P15 — MTF (Multi-Timeframe): intra-cycle monitor analyzes 1H/15M candles when 4H signal is BUY; seeks RSI confirmation (not overbought) on smaller TF before executing;
mtf_pendingdict persists between restarts;mtf_enabledandmtf_tfinconfig_auto.json
v3.22: Extended DailyAuditor + MTF/P14 Persistence + Chat sessions (commits 745c0b7, e99b394, 069a23d, 2e6e45f — 2026-02-25/26)
- DailyAuditor — P13/P14/P15 integrity: alerts if
pending_entries > 3(possible leak); alerts ifmtf_pendingexceeds the number of configured symbols; verifies presence of P13/P14/P15 config keys; validatesmtf_tfagainst supported timeframes - DailyAuditor — Telegram stability: chunked AI analysis (Telegram character limit); HTML escaping in AI analysis text to avoid parse errors
- P14/P15 persistence:
pending_entriesandmtf_pendingincluded insave_state()andload_state()— survive bot restarts - Chat sessions in
/ask: persistent chat mode with multi-turn history;_chat_timeout_monitor_loopthread sends inactivity warning and closes session automatically; any/command closes the session
- P16 — Universe Pulse Scanner: scan of the full MARKET_UNIVERSE (15 pairs) at the end of each 4H cycle; detects movements > ±5% or ATR% > 3% in pairs outside the active config; automatic rotation proposal (best alert ↔ worst asset); Telegram inline buttons
[✅ Rotate X→Y]/[❌ Ignore]executable with one tap; 8h cooldown per symbol to avoid spam; fresh data forced viaupdate_prices()before analyzing - pinned_symbols: new field in
config_auto.json; the Advisor can never rotate out a pinned symbol; BTC/USDT configured as pinned by default - Telegram inline keyboards:
send_message()acceptsreply_markup;get_updates()listens forcallback_query; newanswer_callback_query();_handle_callback()inTradingBotdispatches actions from buttons
- PipelineBacktest: new class in
src/analysis/backtest.pythat replicates the full bot flow in 1H+4H: BuyMonitor (momentum_1h), P15 confirmation, ATR phase trailing stop, P17 exit signals, post-close cooldown. Runs weekly alongside HybridBacktest and savesdata/pipeline_backtest_results.json. - Advisor PipelineBacktest:
_read_pipeline_backtest_results()reads the JSON and adds actionable context to the Advisor prompt (WR, return, exit types, P15/P17 stats per symbol). The Advisor now receives real data from the flow that drives its decisions. - BuyMonitor cooldown:
buy_monitor_cooldown_hours(0–8h) inADJUSTABLE_PARAMS._bm_last_closerecords close epoch inclose_position(); the P18 loop filters candidates within the cooldown window and logs how many minutes they have left. Designed for the NEAR/USDT March 2–4 pattern: 7 consecutive re-entries in a ranging market. - Trailing Phase 2 (fix #21): multiplier 0.75×ATR → 0.5×ATR. Eliminates the dead zone where the SL was at break-even but gains weren't being captured. Now: any tick above the trigger (0.5×ATR) moves the SL above entry. Same change in
PipelineBacktest._TRAIL_PHASES.
- verbose_alerts: detailed alert mode for HOLD signals — toggle with
/verbose /issue: create GitHub issues with technical context directly from Telegram- Auto BotFather registration:
setMyCommandson startup — menu syncs automatically - Dead code:
show_status()removed (empty wrapper overlog_cycle_end()) - P18 filter 4: now uses
entry_target_pctfrom config instead of the hardcoded 1.015% /reloadsafety: warns if symbols with open positions were removed- Centralized SQL: 4 new methods in
DataProvider—TradingBotno longer accesses SQLite directly - Unified RSI:
StrategyEngine._compute_rsi()as the single source of truth — removed duplicate implementation inTradingBot
- P6: Switch to live production (eToro live) — manual decision required
- P9: Economic Calendar — free APIs do not include this endpoint; future solution
Software for educational purposes. Trading carries substantial risk of loss. The authors are not responsible for financial losses. Always start with paper trading.
Version: 3.27 | Status: Active Paper Trading (eToro Demo) | Updated: March 8, 2026