The bot saves all state to a single JSON file (bot_state.json) after every cycle. Writes are atomic: the bot writes to a .tmp file first, then renames it over the target — so a crash mid-write cannot corrupt the state file.
There is no pickle (.pkl) file. JSON is the only persistence format.
Default: bot_state.json in the project root directory.
Configurable via .env:
STATE_FILE=bot_state.json
{
"iteration": 42,
"initial_equity": 130.0,
"current_equity": 134.50,
"peak_equity": 135.00,
"cash_balance": 90.00,
"total_pnl": 4.50,
"total_trades": 3,
"winning_trades": 2,
"losing_trades": 1,
"max_drawdown": 0.02,
"system_1_symbols": ["BTC/USDT"],
"system_2_symbols": [],
"is_paused": false,
"paused_at": null,
"pause_reason": "",
"active_positions": {
"BTC/USDT": {
"symbol": "BTC/USDT",
"exchange": "kraken",
"system": 1,
"units": [
{
"entry_price": 60000.0,
"quantity": 0.00043,
"entry_time": "2026-01-15T10:30:00+00:00"
}
],
"initial_atr": 2100.0,
"initial_n": 2100.0,
"stop_price": 55800.0,
"avg_entry_price": 60000.0,
"total_quantity": 0.00043,
"unrealized_pnl": 1.30,
"opened_at": "2026-01-15T10:30:00+00:00",
"last_pyramid_price": 60000.0,
"highest_price_since_entry": 61200.0,
"trailing_stop_enabled": false
}
},
"closed_positions": [
{
"symbol": "ETH/USDT",
"exchange": "kraken",
"system": 2,
"units": [...],
"initial_atr": 120.0,
"initial_n": 120.0,
"stop_price": 0.0,
"avg_entry_price": 3200.0,
"total_quantity": 0.012,
"unrealized_pnl": 0.0,
"opened_at": "2026-01-10T08:00:00+00:00",
"last_pyramid_price": 3200.0,
"highest_price_since_entry": 3400.0,
"trailing_stop_enabled": false,
"exit_price": 3350.0,
"exit_reason": "EXIT_SIGNAL",
"exit_time": "2026-01-14T16:00:00+00:00",
"realized_pnl": 1.80
}
],
"equity_history": [
{"timestamp": "2026-01-15T10:00:00+00:00", "equity": 130.0},
{"timestamp": "2026-01-15T10:05:00+00:00", "equity": 131.2}
],
"saved_at": "2026-01-15T10:35:00+00:00"
}| Field | Type | Description |
|---|---|---|
iteration |
int | Bot cycle count since startup |
initial_equity |
float | Account size at bot start |
current_equity |
float | Current equity (cash + unrealized P&L) |
peak_equity |
float | Highest equity ever reached |
cash_balance |
float | Realized cash (decremented on buy, incremented on sell) |
total_pnl |
float | Sum of all realized P&L |
total_trades |
int | Count of closed trades |
winning_trades |
int | Trades with P&L >= 0 |
losing_trades |
int | Trades with P&L < 0 |
max_drawdown |
float | Maximum drawdown as fraction (0.05 = 5%) |
system_1_symbols |
list | Symbols with active System 1 positions |
system_2_symbols |
list | Symbols with active System 2 positions |
is_paused |
bool | True = no new entries (existing positions still managed) |
paused_at |
str or null | ISO timestamp of when pause was set |
pause_reason |
str | Human-readable reason for pause |
active_positions |
object | Open positions keyed by symbol |
closed_positions |
list | Trade history (archived closed positions) |
equity_history |
list | List of {timestamp, equity} snapshots — one per bot cycle, used for Sharpe/Sortino calculation and the dashboard equity curve |
saved_at |
str | ISO timestamp of last save |
| Field | Description |
|---|---|
symbol |
Trading pair, e.g. BTC/USDT |
exchange |
Always kraken |
system |
1 (20-day) or 2 (55-day) |
units |
List of individual units (max 4 per Turtle rules) |
initial_atr |
ATR (N) at first entry — locked for life of position |
initial_n |
Same as initial_atr (alias for clarity) |
stop_price |
Current stop: 2N below most recent entry |
avg_entry_price |
Cost-weighted average entry across all units |
total_quantity |
Sum of all unit quantities |
unrealized_pnl |
Last calculated unrealized P&L |
opened_at |
ISO timestamp of first unit entry |
last_pyramid_price |
Price of most recent unit (for 0.5N pyramid check) |
highest_price_since_entry |
float — highest market price seen since this position was opened; used by the trailing stop to ratchet the stop upward |
trailing_stop_enabled |
bool — whether the trailing stop is active for this position; set to True when TRAILING_STOP_ENABLED=True in config |
| Field | Description |
|---|---|
entry_price |
Price at which this unit was entered |
quantity |
Size of this unit in base currency |
entry_time |
ISO timestamp |
| Field | Description |
|---|---|
exit_price |
Fill price at close |
exit_reason |
STOP_HIT, EXIT_SIGNAL, or EMERGENCY_STOP |
exit_time |
ISO timestamp |
realized_pnl |
Actual P&L realized at close |
# Pretty-print (if jq installed)
cat bot_state.json | jq .
# Python
python -m json.tool bot_state.json
# Summary via export utility
python export_state_to_json.pyStop the bot before editing — the bot overwrites the state file at the end of every cycle.
"current_equity": 500.0,
"initial_equity": 500.0,
"peak_equity": 500.0,
"cash_balance": 500.0"is_paused": true,
"pause_reason": "Manual pause for review",
"paused_at": "2026-01-15T10:00:00+00:00""total_trades": 0,
"winning_trades": 0,
"losing_trades": 0,
"total_pnl": 0.0,
"max_drawdown": 0.0rm bot_state.json
# Bot creates a fresh state on next runThe bot writes state as:
- Write full state to
bot_state.json.tmp os.replace(tmp, target)— atomic on both POSIX and Windows
If the process dies mid-write, only the .tmp file is affected. The previous bot_state.json remains intact.
- There are no
.pklfiles — JSON is the only persistence format - There are no automatic
.bakbackups — if you need a backup, copy the file manually before editing - If
bot_state.jsondoes not exist on startup, the bot initializes fresh state fromconfig.pydefaults - If
bot_state.jsonis corrupt (invalid JSON), the bot logs an error and starts fresh
Pro Tip: Always validate JSON after manual edits before restarting the bot:
python -m json.tool bot_state.json > /dev/null && echo "JSON valid"