Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets

This commit is contained in:
Kolp
2026-09-24 13:22:23 +07:00
commit 642cc11a9f
18968 changed files with 5683248 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
# === Tradernet API credentials ===
# Обязательно для LIVE. DEMO может работать с пустыми ключами.
# Получить: tradernet.by → настройки → API-ключи
TRADERNET_PUBLIC_KEY=
TRADERNET_PRIVATE_KEY=
TRADERNET_LOGIN=
TRADERNET_PASSWORD=
TRADERNET_BASE_URL=https://tradernet.by
# === Trading pair ===
SYMBOL=BTC-USDT.IMEX
QUOTE=USDT
BASE=BTC
# === Grid parameters ===
GRID_LEVELS=10
GRID_STEP_PERCENT=0.005
GRID_TAKE_PROFIT_PERCENT=0.5
GRID_RENEW_THRESHOLD=3
GRID_PRICE_OFFSET=0
GRID_STALE_DROP_PERCENT=5.0
# === State ===
STATE_DIR=/root/.openclaw
SERVER_URL=https://gridbot.kolp.pro
# === Demo / Live safety ===
DEMO_MODE=true
DEMO_START_BALANCE=100
LIVE_TRADING_DRY_RUN=true
# === Telegram (опционально) ===
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
# === Flask ===
HOST=0.0.0.0
PORT=5555
DEBUG=false
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: при первом запуске задать сложный пароль, иначе в логе будет WARNING.
WEB_USERNAME=admin
WEB_PASSWORD=changeme
# === Kronos advisor ===
# KRONOS_ENABLED: false = сетка работает на константах из .env, advisor не вызывается
# KRONOS_LIVE_ENABLED: false (default) = kronos_advice.json пишется, но НЕ применяется к ордерам
# true = советник реально подменяет шаг/центр/паузу
KRONOS_ENABLED=false
KRONOS_LIVE_ENABLED=false
KRONOS_MODEL=kronos-mini
KRONOS_TF_MIN=60
KRONOS_LOOKBACK=400
KRONOS_PRED_LEN=24
KRONOS_MIN_CONFIDENCE=0.4
KRONOS_ADVICE_FILE=/root/.openclaw/kronos_advice.json
+23
View File
@@ -0,0 +1,23 @@
# Secrets
.env
.env.bak
.env.*
!.env.example
.auth
# Python
__pycache__/
*.pyc
venv/
.venv/
# State
grid-state.json
kronos_advice.json
state/
# Logs
*.log
# Backups
backup_*/
+71
View File
@@ -0,0 +1,71 @@
# 📊 Backtest results — 3 configurations
**Dataset:** `BTCUSDT-1h-long.csv` (10 000 свечей, Apr 2025 – Jun 2026, $62k–$126k)
**Период:** 14 месяцев, трендовый рынок (BTC вырос с $82k до $106k за этот период)
**Сетка:** GRID_LEVELS=10, GRID_STEP_PERCENT=0.5%, GRID_TAKE_PROFIT=50%
**Kronos:** mini, tf=60m, lookback=400, pred_len=24 (24h горизонт)
## Результаты
| Mode | PnL | #Trades | WinRate | MaxDD | Sharpe |
|---|---|---|---|---|---|
| static (no advisor) | **+1672.60%** | 377 | 100% | -0.79% | 1.38 |
| conservative (Kronos) | +1038.08% | 415 | 100% | -0.79% | **1.41** ⭐ |
| **adjusted (Kronos)** | +1450.05% | 392 | 100% | **-0.69%** ⭐ | 1.36 |
| minimal (Kronos) | +1604.20% | 385 | 100% | -0.79% | 1.31 |
## Выводы
### Что работает
- **adjusted** — лучший **trade-off**: PnL +1450% (vs static +1672%, разница всего -13%) при **лучшем MaxDD -0.69%** (vs -0.79%). Это sweet spot: Kronos слегка улучшает risk-adjusted returns.
- **conservative** даёт **лучший Sharpe** (1.41) — больше сделок (415), меньше волатильность возврата.
- **minimal** почти догоняет static — Kronos минимально вмешивается, просто подсказывает.
### Что не работает
- На **сильно трендовом** рынке (BTC x2 за год) grid-стратегия сама по себе отлично работает: покупай на откатах, продавай на росте. Kronos часто говорит "pause" — и пропускает прибыльные сделки.
- Чем **жёстче** параметры Kronos (conservative), тем больше **упущенная прибыль** (-38% vs static).
### Аномалия
**WinRate = 100%** на всех 4 конфигурациях. Это синтетика моего backtest-симулятора: `simulate_grid()` считает успешным любой "round-trip" (купил → продал выше). Реальный WinRate зависит от:
- Проскальзывания
- Комиссий (я закладывал 0.1%, реально на Tradernet ~0.2%)
- Незаполнения ордеров
- Точности тейк-профита
Реальные цифры будут скромнее, **но относительный расклад сохранится**: static выиграет у Kronos в тренде, adjusted — лучший компромисс.
## Рекомендация
В **trending** рынке (как сейчас, BTC растёт):
- Использовать `minimal` или `adjusted` режим
- `conservative` слишком агрессивен с pause
В **sideways** рынке (если будет коррекция/боковик):
- Можно переключиться на `conservative` — он лучше выдерживает волатильность
## Где в коде настраивать
В `kronos/advisor.py`:
```python
PAUSE_BIAS_THRESHOLD = 0.6
PAUSE_RANGE_PCT = 0.05 # ← 0.05 (conservative), 0.08 (adjusted), 0.10 (minimal)
```
В `kronos/compare_backtest.py` — клампинг step:
```python
# conservative
"step_floor": 0.7, "step_ceil": 1.5
# adjusted
"step_floor": 0.85, "step_ceil": 1.3
# minimal
"step_floor": 0.9, "step_ceil": 1.15
```
В `main.py` (trading_loop) — то, что реально используется в проде:
```python
eff_step = max(static_step * 0.7, min(static_step * 1.5, kronos_step)) # ← conservative
```
Чтобы переключиться на **adjusted**, поменять на:
```python
eff_step = max(static_step * 0.85, min(static_step * 1.3, kronos_step))
```
+262
View File
@@ -0,0 +1,262 @@
# Деплой Kronos advisor на боевой сервер
**Файл пакета:** `kolp-deploy-2026-06-05.tar.gz` (130 КБ, 18 файлов, без `__pycache__`)
**SHA256:** см. `kolp-deploy-2026-06-05.tar.gz.sha256`
**Сервер:** `46.29.164.31`, корень проекта `/root/grid-bot`
---
## ⏱ 5-минутный план (на сервере)
### 0. Бэкап (30 сек)
```bash
cd /root/grid-bot
sudo systemctl stop grid-bot
tar -czf ~/grid-bot-backup-$(date +%F).tar.gz \
--exclude='kronos-venv' --exclude='__pycache__' \
--exclude='.env' --exclude='.env.bak' \
--exclude='*.log' --exclude='data' --exclude='models' .
ls -la ~/grid-bot-backup-*.tar.gz
```
### 1. Скопировать пакет (1 мин)
```bash
# На своём ноуте:
scp kolp-deploy-2026-06-05.tar.gz root@46.29.164.31:/root/grid-bot/
```
### 2. Распаковать поверх (30 сек)
```bash
cd /root/grid-bot
tar -xzf kolp-deploy-2026-06-05.tar.gz
# Проверить, что новые файлы на месте:
ls -la kronos/ kronos_advisor_main.py kronos-advisor.* kronos_advice.json
# Файлы main.py / config.py / .env.example / templates/dashboard.html — перезаписаны
```
### 3. Поднять venv для advisor-а (1 мин)
```bash
cd /root/grid-bot
python3 -m venv --without-pip --system-site-packages kronos-venv
curl -sSL https://bootstrap.pypa.io/get-pip.py | kronos-venv/bin/python
kronos-venv/bin/pip install --quiet --upgrade pip
kronos-venv/bin/pip install --quiet \
torch --index-url https://download.pytorch.org/whl/cpu
kronos-venv/bin/pip install --quiet \
numpy pandas einops==0.8.1 huggingface_hub==0.33.1 \
matplotlib==3.9.3 tqdm==4.67.1 safetensors==0.6.2 \
requests python-dotenv
# Скачать модель (~30 МБ, ~10 сек):
kronos-venv/bin/python -c "
import os
os.environ['HF_HOME'] = os.path.abspath('models/hf-cache')
os.makedirs(os.environ['HF_HOME'], exist_ok=True)
from huggingface_hub import snapshot_download
snapshot_download('NeoQuasar/Kronos-Tokenizer-2k', local_dir='models/Kronos-Tokenizer-2k', allow_patterns=['*.json','*.safetensors','*.txt'])
snapshot_download('NeoQuasar/Kronos-mini', local_dir='models/Kronos-mini', allow_patterns=['*.json','*.safetensors','*.txt'])
print('OK')
"
```
### 4. Обновить .env (1 мин)
```bash
cd /root/grid-bot
# Добавить новые переменные, ЕСЛИ их ещё нет
grep -q '^WEB_USERNAME=' .env || cat >> .env <<'EOF'
# === Web auth (ОБЯЗАТЕЛЬНО сменить) ===
WEB_USERNAME=kolpakov
WEB_PASSWORD=***ВАШ_СЛОЖНЫЙ_ПАРОЛЬ***
# === Kronos advisor ===
KRONOS_ENABLED=true
KRONOS_LIVE_ENABLED=false # ← сначала false, проверим в проде
KRONOS_MODEL=kronos-mini
KRONOS_TF_MIN=60
KRONOS_LOOKBACK=400
KRONOS_PRED_LEN=24
KRONOS_MIN_CONFIDENCE=0.4
KRONOS_ADVICE_FILE=/root/.openclaw/kronos_advice.json
EOF
# Сменить права
chmod 600 .env
```
### 5. Запустить grid-bot (30 сек)
```bash
sudo systemctl start grid-bot
sudo systemctl status grid-bot --no-pager
# В логах должно появиться:
# [INFO] Kronos kronos-mini loaded on cpu (max_context=2048)
# или просто запуск без Kronos (если KRONOS_ENABLED=false)
```
### 6. Установить systemd-timer для advisor-а (1 мин)
```bash
sudo cp /root/grid-bot/kronos-advisor.{service,timer} /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now kronos-advisor.timer
systemctl list-timers kronos-advisor.timer
# Должно показать "next" через <15 мин
```
### 7. Проверить работу (1 мин)
```bash
# 1) Через 5-10 мин проверить, что kronos_advice.json обновляется
ls -la /root/.openclaw/kronos_advice.json
cat /root/.openclaw/kronos_advice.json
# 2) Дашборд
curl -u "kolpakov:ВАШ_ПАРОЛЬ" https://gridbot.kolp.pro/api/kronos | python3 -m json.tool
# 3) Логи advisor-а
journalctl -u kronos-advisor.service -n 20 --no-pager
tail -50 /root/grid-bot/kronos-advisor.log
```
---
## 🔒 Безопасность — обязательно
1. **Сменить оба комплекта ключей Tradernet** (9adf.../97b0... и a8e6.../7313...) — они утекли в архив, который я видел.
- `https://tradernet.by` → Настройки → API-ключи → отозвать старые, выпустить новые
- Положить новые в `.env` (не в `config.py`!)
- В `config.py` старые ключи **уже удалены** (только `os.getenv`)
2. **`WEB_PASSWORD`** поставить сложный (16+ символов, не как в `.env.example`)
3. **Telegram-токен** — тоже отозвать (`@BotFather` → `/revoke` → новый)
---
## 🚦 Стратегия включения в проде
### Этап 1 — безопасно (текущий шаг)
- `KRONOS_ENABLED=true`
- `KRONOS_LIVE_ENABLED=false`
- Советник пишет `kronos_advice.json`, видим в дашборде
- **Бот работает как раньше**, Kronos **не влияет** на ордера
### Этап 2 — полу-включение (через 1-2 дня, если метрики ОК)
- `KRONOS_LIVE_ENABLED=true`
- Kronos начинает реально корректировать шаг/центр/паузу
- Следить за логом `[KRONOS] applied: ...`
- При проблемах → `KRONOS_LIVE_ENABLED=false` в `.env` + `sudo systemctl restart grid-bot`
### Этап 3 — прод-настройки (когда уверен)
- То же, что этап 2
- Включить Telegram-уведомления о смене совета (уже в коде — сработает само)
- Опционально: сменить `KRONOS_TF_MIN` на 15/30/120
---
## 🔄 Откат (если что-то пошло не так)
```bash
cd /root/grid-bot
sudo systemctl stop grid-bot
# Восстановить из бэкапа
tar -xzf ~/grid-bot-backup-2026-06-05.tar.gz
# Если нужно полностью выключить Kronos:
sed -i 's/^KRONOS_ENABLED=true/KRONOS_ENABLED=false/' .env
sed -i 's/^KRONOS_LIVE_ENABLED=true/KRONOS_LIVE_ENABLED=false/' .env
sudo systemctl start grid-bot
```
Или ещё проще:
```bash
# В .env:
KRONOS_ENABLED=false
KRONOS_LIVE_ENABLED=false
sudo systemctl restart grid-bot
# Бот работает как ДО интеграции с Kronos
```
---
## 📁 Что внутри пакета
```
grid-bot/
├── main.py ← обновлён (Kronos-интеграция)
├── config.py ← секреты → .env, KRONOS_* переменные
├── .env.example ← шаблон .env
├── .gitignore ← защита .env
├── kronos/ ← NEW: пакет советника
│ ├── __init__.py
│ ├── advisor.py ← KronosAdvisor
│ ├── data_source.py ← Tradernet + Binance источники
│ ├── backtest.py ← backtest static vs adaptive
│ └── compare_backtest.py ← сравнение conservative/adjusted/minimal
├── kronos_advisor_main.py ← CLI (запускается из systemd)
├── kronos_advice.json ← последний совет (пример)
├── kronos-advisor.service ← systemd unit
├── kronos-advisor.timer ← каждые 15 мин
├── kronos-advisor.README.md ← подробная докуменация по unit
├── templates/
│ └── dashboard.html ← обновлён (🧠 Kronos-блок)
└── static/
├── chart.umd.min.js
└── chartjs-plugin-annotation.min.js
```
**НЕ включено в пакет (нужно отдельно):**
- `kronos-venv/` (создаётся на шаге 3)
- `models/Kronos-Tokenizer-2k/` + `models/Kronos-mini/` (скачивается на шаге 3)
- `data/` (нет нужды — данные тянутся с биржи)
- `.env` и `.env.bak` (ты добавишь сам)
- `venv/` (твой существующий)
---
## 📊 Что мы знаем о качестве Kronos-прогноза
Результаты backtest-а на 14 мес BTCUSDT 1h (Apr 2025 → Jun 2026):
| Mode | PnL | WinRate | MaxDD | Sharpe |
|---|---|---|---|---|
| static (фикс 0.5%) | +1672% | 100% | -0.79% | 1.38 |
| conservative (Kronos) | +515% | 100% | -0.69% | 1.16 |
| adjusted (Kronos) | *в процессе* | | | |
| minimal (Kronos) | *в процессе* | | | |
**Вывод честный:**
- В **текущей** реализации Kronos для grid на трендовом рынке вредит (статичная сетка BTC-тренда выгоднее)
- Однако **Telegram-уведомления** и **adaptive pause** — это ценная страховка от резких обвалов
- Backtest на отрезке **с трендом вверх** искажает картину: grid зарабатывает на колебаниях, а Kronos их «фильтрует»
- На **боковике** adaptive может выиграть — нужно тестировать в проде в разные фазы рынка
**Рекомендация:** запускай в режиме «Этап 1» (наблюдение), через 1-2 недели включай «Этап 2», следи за метриками. Не жди чудес — Kronos это **советник**, не замена стратегии.
---
## ⚠️ Что я НЕ делал
- Не отзывал ключи Tradernet (ты сам)
- Не запускал ничего на твоём сервере
- Не менял существующий `grid-bot.service` (только добавил `kronos-advisor.{service,timer}`)
- Не трогал твой `.env` и `.env.bak`
- Не интегрировал в существующий дашборд `b.html` (если он у тебя живёт — нужно руками перенести Kronos-блок)
---
## 📞 Если что-то не работает
```bash
# Смотри логи в таком порядке:
journalctl -u grid-bot -n 50 --no-pager
journalctl -u kronos-advisor.service -n 50 --no-pager
tail -100 /root/grid-bot/kronos-advisor.log
tail -100 /root/grid-bot/margin-bot.log
# Проверь, что модель на месте
ls -la /root/grid-bot/models/Kronos-Tokenizer-2k/
ls -la /root/grid-bot/models/Kronos-mini/
# Проверь, что совет генерируется
cd /root/grid-bot
kronos-venv/bin/python kronos_advisor_main.py --live 2>&1 | tail -10
cat /root/.openclaw/kronos_advice.json
```
Если `kronos_advisor_main.py` падает — пришли мне вывод ошибки, разберёмся.
+338
View File
@@ -0,0 +1,338 @@
# LIVE TRADING CHECKLIST — grid-bot BTC-USDT.IMEX
**Дата создания:** 2026-06-26 23:30 GMT+7
**Last update:** 2026-06-26 23:41 GMT+7 — ответы @KAndrew на open questions
**Бот:** grid-bot v0.13.x (PID 1552921, mode=demo, peak=$375.68)
**Цель:** выход на реальную торговлю через Cifra Markets (BY) / Tradernet
**Целевой размер позиции (от @KAndrew):** $135
> ⚠️ **Текущий статус: НЕ ГОТОВ.** Ниже — что должно быть сделано перед переключением `LIVE_TRADING_DRY_RUN=false`.
---
## 🚦 Блокеры (без них live = деньги на ветер)
### 1. Подтвердить fees Cifra Markets ✅ ЗАКРЫТ (27.06 00:03)
- [x] Зайти в ЛК **cifra.by** → раздел «Тарифы» — @KAndrew подтвердил
- [x] Тариф: **«Консультационный»** (ДЛЯ СТАРТА). Возможно есть другие tiers по объёму — уточнить при выходе на стабильный оборот
- [x] Записать **точные %** для BTC-USDT: **0.5% per side** (round-trip = **1.0%**)
- [x] Обновить `.env`: `FEE_RATE_PERCENT=0.5`, `FEE_SAFETY_MULTIPLIER=1.5` (было 0.2 / 1.25)
- [ ] **Открытый вопрос**: «Списывается только в рублях» — реальный fee может быть выше из-за скрытого RUB/USD спреда (~0.1–0.2%)
- [ ] При следующем рестарте бота подхватится автоматически
**Расчёт margin (fee=0.5% per side):**
- Round-trip fee: **1.00%**
- Step 0.5% − fee 1.00% = **−0.50%** — УБЫТОК ❌
- Validator `fee × safety=1.5`: 1.00% × 1.5 = **1.50%** → step должен быть ≥ 1.5%
- TP 2% − fees 1.00% = **1.00% net profit** на полный цикл ✅
- **Step обязательно поднимать** до 1.5–2.0%
### 2. Накопить demo-метрики
- [ ] Минимум **30 закрытых сделок** (сейчас 13) — не дни, а TP-fill-ы
- [ ] Минимум **14 дней** непрерывной работы в demo без критических багов
- [ ] **Win-rate > 50%** (TP-fill / total fills)
- [ ] **Avg PnL per trade > 0** после учёта fees
- [ ] **Max DD < 8%** за весь период (у нас сейчас −4.86% от peak)
### 3. Настроить kill switch для live
- [ ] `LIVE_KILL_CLOSE_POSITION=true` (сейчас `false` — это риск)
- [ ] `LIVE_KILL_DD_PCT=5.0` (сейчас не задан → fallback на 10%)
- [ ] Проверить TG-polling работает: `/api/kill-switch/status` → `tg_poll_sec: 30`
- [ ] Протестировать `/api/kill-switch/test` с reason=`live_test` (на demo!)
### 4. Code review (см. секцию ниже)
- [ ] Пройтись по всем fix-точкам из MEMORY.md (`*100`-баги, anchor-формулы, percent-конверсии)
- [ ] Сверить все места с `take_profit_percent` (был баг ×4 → anchor $260K)
- [ ] Проверить формулы asymmetric split (3:2 / 2:3)
### 5. Live API smoke test (см. секцию ниже)
- [ ] Тестовый BUY на минимальный lot с ручным отменом через ЛК
- [ ] Тестовый SELL (TP-fill) на минимальный lot
- [ ] Проверить, что `urllib`-запросы реально доходят до Tradernet, нет 403/timeout
---
## ⚙️ Pre-live .env настройки (что поменять)
```bash
# === ОБЯЗАТЕЛЬНО изменить перед live ===
# 1. Trading mode
DEMO_MODE=false # было: true
LIVE_TRADING_DRY_RUN=false # было: true — это переключатель на реальные ордера
# 2. Real Tradernet API credentials (НЕ дефолтные)
TRADERNET_PUBLIC_KEY=<real_key> # отозвать старые demo-ключи
TRADERNET_PRIVATE_KEY=<real_key>
TRADERNET_LOGIN=<real_login>
TRADERNET_PASSWORD=<real_pass>
# 3. Kill switch — ОБЯЗАТЕЛЬНО
LIVE_KILL_CLOSE_POSITION=true # было: false
LIVE_KILL_DD_PCT=5.0 # 5% (было: не задано → 10%)
KILL_DD_PCT=10.0 # для demo остаётся 10%
# 4. Fee rate (после подтверждения в ЛК Cifra)
FEE_RATE_PERCENT=0.2 # ОБНОВИТЬ после проверки точных %
FEE_SAFETY_MULTIPLIER=1.5 # было: 1.25 — увеличить запас
# 5. Strategy — для live понизить агрессивность
STRATEGY_LOCK_PCT=1.5 # было: 2.0 — раньше блокировать
STRATEGY_LOCK_COOLDOWN_SEC=1800 # было: 900 — реже переключаться
# 6. Inactivity rebuild — для live ВЫКЛЮЧИТЬ
INACTIVITY_RESTART_HOURS=999 # было: 6 — на live пусть работает как есть
INACTIVITY_PRICE_DRIFT_PCT=99.0 # было: 0.25 — фактически отключить
# 7. Kronos — пока НЕ включать на live
KRONOS_LIVE_ENABLED=false # оставить false, пока не подтверждена статистика
# === Что НЕ менять ===
GRID_STEP_PERCENT=0.005 # 0.5% — нормально для fee 0.2%
GRID_TAKE_PROFIT_PERCENT=2.0 # 2% TP — нормально
GRID_LEVELS=10 # дефолт (но у нас сейчас 3 в state.json)
STRATEGY_CENTER_MODE=sma # OK
```
> После правок — **обязательно** перезапустить бота с `kill -9 PID` + новый запуск через `nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown`
---
## 🔍 Code review checklist (что перепроверить в коде)
### Pattern 1: Percent vs ratio confusion (исторически баг ×4)
```bash
# Найти все места, где take_profit_percent или step_percent умножаются:
grep -n "take_profit_percent\|step_percent" /root/grid-bot/main.py /root/grid-bot/grid.py
```
**Проверить каждое вхождение:**
- Если переменная хранится в **процентах** (2.0 = 2%), формула должна быть `value / 100.0`
- Если в **долях** (0.02 = 2%), формула должна быть `value` напрямую
- В `grid.py:pnl_percent()` — уже `*100`, значит внутри ratio
### Pattern 2: Anchor calculation
```bash
grep -n "sell_anchor\|entry_price.*1\.\|anchor_for_filled" /root/grid-bot/main.py
```
- Убедиться что **anchor = entry_price × (1 + take_profit_percent / 100)**
- НЕ `entry_price * (1.0 + take_profit_percent)` (это и был баг 17.06, anchor × 4)
### Pattern 3: Live order placement
```bash
grep -n "_place_live_buy\|_place_live_sell\|_live_cancel" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- Проверить, что `LIVE_TRADING_DRY_RUN=false` реально идёт в код, не в логику демо
- Проверить обработку ошибок `403 / 429 / timeout` — бот должен ретраить ИЛИ тихо отменять (не падать)
- Проверить, что при ошибке API бот НЕ дублирует ордер
### Pattern 4: Asymmetric grid split
```bash
grep -n "STRATEGY_ASYMMETRY\|buy.*sell\|split" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- bias=up → 3 BUY / 2 SELL (anchor выше)
- bias=down → 2 BUY / 3 SELL (anchor ниже)
- bias=range → 3 BUY / 2 SELL (default)
### Pattern 5: Peak equity reset
```bash
grep -n "peak_equity\|_peak_lock" /root/grid-bot/main.py
```
- При live-старте вызвать `kill_switch.reset()` — peak начнёт отсчитываться от live-баланса
- Без reset — KS может триггернуться сразу (peak=$375.68 demo, на live у нас меньше)
### Pattern 6: 403 авто-pause (требует НОВОГО кода) ⭐
**Требование @KAndrew:** при HTTP 403 от Tradernet API → авто-pause бота, НЕ kill.
**Сейчас:** KS считает все API errors одинаково (`api_errors_threshold=5` за 600 сек → kill).
**Что нужно сделать:**
- [ ] Разделить счётчики ошибок: `api_errors_403` (auth/perm) vs `api_errors_5xx` (infra)
- [ ] При `api_errors_403 >= 1` → bot_running=false (pause), KS НЕ triggered, reason=`auth_403`
- [ ] При `api_errors_5xx >= 5` за 600 сек → KS trigger (как сейчас)
- [ ] При resume (после ручного `/api/bot/start`) — счётчики сбрасываются
- [ ] TG-алерт при любой 403: «⚠️ 403 от Tradernet — бот поставлен на паузу»
**Файл для правки:** `/root/grid-bot/main.py` — секция API error tracking (см. grep `_api_errors`)
**Smoke test после фикса:**
```bash
# На demo: подменить API ключ на невалидный, дождаться 403 → бот должен встать на паузу
sed -i 's/TRADERNET_PUBLIC_KEY=.*/TRADERNET_PUBLIC_KEY=INVALID_KEY/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py'); cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 60
curl -s -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/status | grep bot_running
# → должно быть bot_running: false, reason: auth_403
# Восстановить ключ!
```
### Pattern 7: TG-уведомления на сделку (требует НОВОГО кода) ⭐
**Требование @KAndrew:** каждая сделка (BUY fill / SELL fill / TP-fill) → TG-сообщение с деталями.
**Сейчас:** TG вызывается только при `tg_notify()` в `main.py:384` (KS-alerts, важные изменения). Сами сделки НЕ спамят.
**Что нужно сделать:**
- [ ] Хук в `check_fill_buy` / `check_fill_sell` / `check_take_profit` после успешного fill → `tg_notify(...)` с шаблоном:
- BUY: `🟢 BUY fill @ $XXX (qty Y, balance $Z)`
- SELL: `🔴 SELL fill @ $XXX (pnl +$W)`
- TP-fill: `🎯 TP @ $XXX (pnl +$W, total trades: N)`
- [ ] Anti-spam: не слать TG если fill дубликат (idempotency по order_id)
- [ ] Quiet hours НЕ применяются (24/7 по решению @KAndrew)
- [ ] Тест: дождаться следующего BUY fill на demo → проверить TG
**Файлы для правки:** `/root/grid-bot/main.py` (buy/sell fill handlers) + `/root/grid-bot/grid.py` (TP logic)
---
## 🧪 Smoke test (перед переключением на live)
### Pre-flight (на demo)
```bash
# 1. Kill switch готов?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/kill-switch/status
# → enabled=true, triggered=false, reason=none, peak=$375.68
# 2. Тест KS в demo (НЕ в live!)
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/test
# → reason="test:..." triggered=true
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/reset
# → reason="none"
# 3. API ключи валидны?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/account
# → account_id, balance > 0, currency=USDT
```
### Live dry-run (минимальная позиция)
```bash
# 1. Переключить на live dry-run
sed -i 's/LIVE_TRADING_DRY_RUN=true/LIVE_TRADING_DRY_RUN=false/' /root/grid-bot/.env
# (НЕ ТРОГАТЬ DEMO_MODE — оставить true чтобы не запускать боевую логику)
# 2. Запустить, наблюдать 1 час
kill -9 $(pgrep -f 'python.*main.py')
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 3600
# 3. Проверить логи
grep -E 'LIVE_|PLACE|ERROR' /root/grid-bot/bot.log | tail -30
# 4. Откатить если что-то не так
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py')
```
---
## 🚀 Phased rollout plan (целевой размер $135)
### Phase 0 — Smoke (1 день)
- `DEMO_MODE=true`, `LIVE_TRADING_DRY_RUN=false` (только меняем флаг, не логику)
- Наблюдаем 1 день — бот реально стучится в Tradernet API, ордера не размещает
- Проверяем логи на 403/timeout/retry
### Phase 1 — Micro-live Week 1 (5–7 дней)
- **$40** (≈30% от $135)
- `GRID_LEVELS=3`, `GRID_STEP_PERCENT=0.005`, `GRID_TAKE_PROFIT_PERCENT=2.0`
- `LIVE_KILL_DD_PCT=5.0`, `LIVE_KILL_CLOSE_POSITION=true`
- `KRONOS_LIVE_ENABLED=false` (default, осторожно)
- 24/7 (по решению @KAndrew)
- Авто-pause при 403 (после код-фикса)
- Ежедневный отчёт в TG (PnL, win-rate, max DD, #trades) — `@Kolp` генерит сам
### Phase 2 — Half Week 2 (5–7 дней)
- **$90** (≈67% от $135)
- Если Phase 1: PnL > 0 И DD < 3% И win-rate > 50%
- Иначе: остаёмся на $40 ещё на неделю
### Phase 3 — Full Week 3-4
- **$135** (100%)
- Если Phase 2 стабильно PnL > 0 И DD < 3%
- Включить `KRONOS_LIVE_ENABLED=true` (опционально, после подтверждения Kronos-метрик в demo)
### Steady state
- 100% ($135), 24/7, full automation
- Weekly отчёт в TG (можно выключить если утомит)
> ❌ **Откатить на Phase N-1 (или в demo) если:** DD > 5%, > 3 API errors подряд, любой phantom-order баг, > 5 trades подряд в убыток, TG spam от kill-switch.
---
## ↩️ Rollback plan (если что-то пошло не так)
```bash
# 1. Остановить бота
kill -9 $(pgrep -f 'python.*main.py')
# 2. Откатить .env на demo
sed -i 's/DEMO_MODE=false/DEMO_MODE=true/' /root/grid-bot/.env
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
# 3. Вручную закрыть все висящие ордера через ЛК cifra.by → «Активные заявки»
# 4. Запустить demo снова
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
# 5. Сделать snapshot кода
cp -r /root/grid-bot /root/grid-bot.live-fail-$(date +%Y%m%d_%H%M%S)
# 6. Проверить убытки в ЛК cifra.by → История сделок
```
---
## ✅ Решения (ответы @KAndrew, 26.06 23:41 GMT+7)
| # | Вопрос | Решение |
|---|---|---|
| 1 | Размер позиции на live | **$135** (целевой финальный размер) |
| 2 | Одновременно ордеров | **3** (как сейчас в state.json) |
| 3 | Kronos live | **Вариант А — выключить навсегда** (подтверждено @KAndrew). Код оставить как legacy, `STRATEGY_BIAS_SOURCE=trend` |
| 4 | Ночной режим | **24/7** (без QUIET_HOURS) |
| 5 | Если API 403 | **авто-pause** (НЕ kill) — требует код-фикса, см. Code review §6 |
| 6 | TG-отчёты | **(a) каждое сообщение на сделку** (подтверждено @KAndrew) — BUY/SELL/TP-fill → отдельный TG. Требует код-фикса, см. Code review §7 |
> Все defaults можно переопределить — скажи явно.
---
## 📊 Текущие метрики (для сравнения с live)
| Метрика | Demo сейчас | Live минимум |
|---|---|---|
| Total trades | 13 | 30+ (закрытых) |
| Total PnL | $3.06 | > $0 за неделю |
| Peak equity | $375.68 | — |
| Max DD | −4.86% | < 5% |
| Win-rate | 1/1 = 100% (n=1) | > 50% (n≥20) |
| Avg TP-fill time | ~2ч | — |
| Avg step fill | ~30 мин | — |
| Days running | ~14 | 14+ |
| Uptime | ~95% (были рестарты) | > 98% |
---
## 🔗 Связанные файлы
- `/root/grid-bot/.env` — основные настройки
- `/root/grid-bot/config.py` — defaults (все через `os.getenv`)
- `/root/grid-bot/main.py` — trading loop + KS + state.json
- `/root/grid-bot/grid.py` — grid logic, TP, fills
- `/root/grid-bot/api.py` — Tradernet REST client
- `~/.openclaw/workspace/MEMORY.md` — патчи, баги, решения
- `~/.openclaw/workspace/memory/2026-06-*.md` — daily notes
---
**Автор:** Kolp (OpenClaw + M3)
**Версия:** 1.1 (обновлено 2026-06-26 23:41 GMT+7)
**История:**
- v1.0 (23:30) — первая версия, open questions
- v1.1 (23:41) — ответы @KAndrew: $135, 3 ордера, 24/7, 403→pause
- v1.2 (23:42) — TG-отчёты на сделку (Pattern 7)
- v1.3 (23:45) — Kronos = Вариант А (выключить)
- v1.4 (23:48) — все 6 open questions закрыты
- v1.5 (23:54) — fees=0.15% (первая оценка @KAndrew)
- v1.6 (27.06 00:03) — fees=**0.5% per side** (тариф «Консультационный», round-trip=1.0%). Требует пересмотра step
- v1.7 (27.06 00:09) — step=**2.0%** применён + soft-rebuild выполнен. Новая сетка: 3 BUY @ $58,846/$59,145/$59,444 (от new_start $60,164)
**Следующий шаг:** дождаться BUY-fill или новых решений @KAndrew
+261
View File
@@ -0,0 +1,261 @@
# LIVE TRADING CHECKLIST — grid-bot BTC-USDT.IMEX
**Дата создания:** 2026-06-26
**Бот:** grid-bot v0.13.x (PID 1552921, mode=demo, peak=$375.68)
**Цель:** выход на реальную торговлю через Cifra Markets (BY) / Tradernet
> ⚠️ **Текущий статус: НЕ ГОТОВ.** Ниже — что должно быть сделано перед переключением `LIVE_TRADING_DRY_RUN=false`.
---
## 🚦 Блокеры (без них live = деньги на ветер)
### 1. Подтвердить fees Cifra Markets
- [ ] Зайти в ЛК **cifra.by** → раздел «Тарифы» / «Комиссии»
- [ ] Записать **точные %** для BTC-USDT: maker / taker, market / limit
- [ ] Если fee > 0.25% per side → обновить `FEE_RATE_PERCENT` и пересмотреть `GRID_STEP_PERCENT`
- [ ] Учесть спред bid/ask (сейчас по quote: 0.09% — мелкий, проверить на реальных сделках)
### 2. Накопить demo-метрики
- [ ] Минимум **30 закрытых сделок** (сейчас 13) — не дни, а TP-fill-ы
- [ ] Минимум **14 дней** непрерывной работы в demo без критических багов
- [ ] **Win-rate > 50%** (TP-fill / total fills)
- [ ] **Avg PnL per trade > 0** после учёта fees
- [ ] **Max DD < 8%** за весь период (у нас сейчас −4.86% от peak)
### 3. Настроить kill switch для live
- [ ] `LIVE_KILL_CLOSE_POSITION=true` (сейчас `false` — это риск)
- [ ] `LIVE_KILL_DD_PCT=5.0` (сейчас не задан → fallback на 10%)
- [ ] Проверить TG-polling работает: `/api/kill-switch/status` → `tg_poll_sec: 30`
- [ ] Протестировать `/api/kill-switch/test` с reason=`live_test` (на demo!)
### 4. Code review (см. секцию ниже)
- [ ] Пройтись по всем fix-точкам из MEMORY.md (`*100`-баги, anchor-формулы, percent-конверсии)
- [ ] Сверить все места с `take_profit_percent` (был баг ×4 → anchor $260K)
- [ ] Проверить формулы asymmetric split (3:2 / 2:3)
### 5. Live API smoke test (см. секцию ниже)
- [ ] Тестовый BUY на минимальный lot с ручным отменом через ЛК
- [ ] Тестовый SELL (TP-fill) на минимальный lot
- [ ] Проверить, что `urllib`-запросы реально доходят до Tradernet, нет 403/timeout
---
## ⚙️ Pre-live .env настройки (что поменять)
```bash
# === ОБЯЗАТЕЛЬНО изменить перед live ===
# 1. Trading mode
DEMO_MODE=false # было: true
LIVE_TRADING_DRY_RUN=false # было: true — это переключатель на реальные ордера
# 2. Real Tradernet API credentials (НЕ дефолтные)
TRADERNET_PUBLIC_KEY=<real_key> # отозвать старые demo-ключи
TRADERNET_PRIVATE_KEY=<real_key>
TRADERNET_LOGIN=<real_login>
TRADERNET_PASSWORD=<real_pass>
# 3. Kill switch — ОБЯЗАТЕЛЬНО
LIVE_KILL_CLOSE_POSITION=true # было: false
LIVE_KILL_DD_PCT=5.0 # 5% (было: не задано → 10%)
KILL_DD_PCT=10.0 # для demo остаётся 10%
# 4. Fee rate (после подтверждения в ЛК Cifra)
FEE_RATE_PERCENT=0.2 # ОБНОВИТЬ после проверки точных %
FEE_SAFETY_MULTIPLIER=1.5 # было: 1.25 — увеличить запас
# 5. Strategy — для live понизить агрессивность
STRATEGY_LOCK_PCT=1.5 # было: 2.0 — раньше блокировать
STRATEGY_LOCK_COOLDOWN_SEC=1800 # было: 900 — реже переключаться
# 6. Inactivity rebuild — для live ВЫКЛЮЧИТЬ
INACTIVITY_RESTART_HOURS=999 # было: 6 — на live пусть работает как есть
INACTIVITY_PRICE_DRIFT_PCT=99.0 # было: 0.25 — фактически отключить
# 7. Kronos — пока НЕ включать на live
KRONOS_LIVE_ENABLED=false # оставить false, пока не подтверждена статистика
# === Что НЕ менять ===
GRID_STEP_PERCENT=0.005 # 0.5% — нормально для fee 0.2%
GRID_TAKE_PROFIT_PERCENT=2.0 # 2% TP — нормально
GRID_LEVELS=10 # дефолт (но у нас сейчас 3 в state.json)
STRATEGY_CENTER_MODE=sma # OK
```
> После правок — **обязательно** перезапустить бота с `kill -9 PID` + новый запуск через `nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown`
---
## 🔍 Code review checklist (что перепроверить в коде)
### Pattern 1: Percent vs ratio confusion (исторически баг ×4)
```bash
# Найти все места, где take_profit_percent или step_percent умножаются:
grep -n "take_profit_percent\|step_percent" /root/grid-bot/main.py /root/grid-bot/grid.py
```
**Проверить каждое вхождение:**
- Если переменная хранится в **процентах** (2.0 = 2%), формула должна быть `value / 100.0`
- Если в **долях** (0.02 = 2%), формула должна быть `value` напрямую
- В `grid.py:pnl_percent()` — уже `*100`, значит внутри ratio
### Pattern 2: Anchor calculation
```bash
grep -n "sell_anchor\|entry_price.*1\.\|anchor_for_filled" /root/grid-bot/main.py
```
- Убедиться что **anchor = entry_price × (1 + take_profit_percent / 100)**
- НЕ `entry_price * (1.0 + take_profit_percent)` (это и был баг 17.06, anchor × 4)
### Pattern 3: Live order placement
```bash
grep -n "_place_live_buy\|_place_live_sell\|_live_cancel" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- Проверить, что `LIVE_TRADING_DRY_RUN=false` реально идёт в код, не в логику демо
- Проверить обработку ошибок `403 / 429 / timeout` — бот должен ретраить ИЛИ тихо отменять (не падать)
- Проверить, что при ошибке API бот НЕ дублирует ордер
### Pattern 4: Asymmetric grid split
```bash
grep -n "STRATEGY_ASYMMETRY\|buy.*sell\|split" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- bias=up → 3 BUY / 2 SELL (anchor выше)
- bias=down → 2 BUY / 3 SELL (anchor ниже)
- bias=range → 3 BUY / 2 SELL (default)
### Pattern 5: Peak equity reset
```bash
grep -n "peak_equity\|_peak_lock" /root/grid-bot/main.py
```
- При live-старте вызвать `kill_switch.reset()` — peak начнёт отсчитываться от live-баланса
- Без reset — KS может триггернуться сразу (peak=$375.68 demo, на live у нас меньше)
---
## 🧪 Smoke test (перед переключением на live)
### Pre-flight (на demo)
```bash
# 1. Kill switch готов?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/kill-switch/status
# → enabled=true, triggered=false, reason=none, peak=$375.68
# 2. Тест KS в demo (НЕ в live!)
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/test
# → reason="test:..." triggered=true
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/reset
# → reason="none"
# 3. API ключи валидны?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/account
# → account_id, balance > 0, currency=USDT
```
### Live dry-run (минимальная позиция)
```bash
# 1. Переключить на live dry-run
sed -i 's/LIVE_TRADING_DRY_RUN=true/LIVE_TRADING_DRY_RUN=false/' /root/grid-bot/.env
# (НЕ ТРОГАТЬ DEMO_MODE — оставить true чтобы не запускать боевую логику)
# 2. Запустить, наблюдать 1 час
kill -9 $(pgrep -f 'python.*main.py')
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 3600
# 3. Проверить логи
grep -E 'LIVE_|PLACE|ERROR' /root/grid-bot/bot.log | tail -30
# 4. Откатить если что-то не так
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py')
```
---
## 🚀 Phased rollout plan
### Week 1 (после всех блокеров выше)
- Live с **$50–100** (5% от целевого капитала)
- Только дневная сессия (07:00–22:00 GMT+7)
- `GRID_LEVELS=3`, `step=0.5%`, `TP=2%`
- Ежедневный отчёт в TG: PnL, win-rate, max DD, #trades
### Week 2
- Если Week 1 PnL > 0 И DD < 3% → увеличить до **$200–300**
- То же самое
### Week 3-4
- Если непрерывный PnL > 0 → довести до **50% целевого капитала**
- Включить Kronos live (`KRONOS_LIVE_ENABLED=true`)
### Steady state
- 100% целевого капитала
- Ночной режим (без изменений — бот работает 24/7)
> ❌ **Откатить на demo если:** DD > 5%, > 3 API errors подряд, любой phantom-order баг, TG spam от kill-switch.
---
## ↩️ Rollback plan (если что-то пошло не так)
```bash
# 1. Остановить бота
kill -9 $(pgrep -f 'python.*main.py')
# 2. Откатить .env на demo
sed -i 's/DEMO_MODE=false/DEMO_MODE=true/' /root/grid-bot/.env
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
# 3. Вручную закрыть все висящие ордера через ЛК cifra.by → «Активные заявки»
# 4. Запустить demo снова
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
# 5. Сделать snapshot кода
cp -r /root/grid-bot /root/grid-bot.live-fail-$(date +%Y%m%d_%H%M%S)
# 6. Проверить убытки в ЛК cifra.by → История сделок
```
---
## ❓ Open questions (нужны ответы перед go-live)
1. **Какой размер позиции на live?** $100 / $500 / $1000 / другая сумма?
2. **Сколько ордеров одновременно на live?** Оставить 3 (как сейчас) или 10 (дефолт)?
3. **Kronos live: да/нет/позже?** Сейчас `KRONOS_LIVE_ENABLED=false` — оставляем?
4. **Что делать ночью?** Бот работает 24/7, или добавить `QUIET_HOURS=23:00–07:00`?
5. **Что если Tradernet API начнёт возвращать 403?** Авто-pause или auto-kill?
6. **Частота отчётов в TG?** Раз в час / раз в сделку / только при алертах?
---
## 📊 Текущие метрики (для сравнения с live)
| Метрика | Demo сейчас | Live минимум |
|---|---|---|
| Total trades | 13 | 30+ (закрытых) |
| Total PnL | $3.06 | > $0 за неделю |
| Peak equity | $375.68 | — |
| Max DD | −4.86% | < 5% |
| Win-rate | 1/1 = 100% (n=1) | > 50% (n≥20) |
| Avg TP-fill time | ~2ч | — |
| Avg step fill | ~30 мин | — |
| Days running | ~14 | 14+ |
| Uptime | ~95% (были рестарты) | > 98% |
---
## 🔗 Связанные файлы
- `/root/grid-bot/.env` — основные настройки
- `/root/grid-bot/config.py` — defaults (все через `os.getenv`)
- `/root/grid-bot/main.py` — trading loop + KS + state.json
- `/root/grid-bot/grid.py` — grid logic, TP, fills
- `/root/grid-bot/api.py` — Tradernet REST client
- `~/.openclaw/workspace/MEMORY.md` — патчи, баги, решения
- `~/.openclaw/workspace/memory/2026-06-*.md` — daily notes
---
**Автор:** Kolp (OpenClaw + M3)
**Следующий review:** когда Андрей даст ответы на Open questions
+307
View File
@@ -0,0 +1,307 @@
# LIVE TRADING CHECKLIST — grid-bot BTC-USDT.IMEX
**Дата создания:** 2026-06-26 23:30 GMT+7
**Last update:** 2026-06-26 23:41 GMT+7 — ответы @KAndrew на open questions
**Бот:** grid-bot v0.13.x (PID 1552921, mode=demo, peak=$375.68)
**Цель:** выход на реальную торговлю через Cifra Markets (BY) / Tradernet
**Целевой размер позиции (от @KAndrew):** $135
> ⚠️ **Текущий статус: НЕ ГОТОВ.** Ниже — что должно быть сделано перед переключением `LIVE_TRADING_DRY_RUN=false`.
---
## 🚦 Блокеры (без них live = деньги на ветер)
### 1. Подтвердить fees Cifra Markets
- [ ] Зайти в ЛК **cifra.by** → раздел «Тарифы» / «Комиссии»
- [ ] Записать **точные %** для BTC-USDT: maker / taker, market / limit
- [ ] Если fee > 0.25% per side → обновить `FEE_RATE_PERCENT` и пересмотреть `GRID_STEP_PERCENT`
- [ ] Учесть спред bid/ask (сейчас по quote: 0.09% — мелкий, проверить на реальных сделках)
### 2. Накопить demo-метрики
- [ ] Минимум **30 закрытых сделок** (сейчас 13) — не дни, а TP-fill-ы
- [ ] Минимум **14 дней** непрерывной работы в demo без критических багов
- [ ] **Win-rate > 50%** (TP-fill / total fills)
- [ ] **Avg PnL per trade > 0** после учёта fees
- [ ] **Max DD < 8%** за весь период (у нас сейчас −4.86% от peak)
### 3. Настроить kill switch для live
- [ ] `LIVE_KILL_CLOSE_POSITION=true` (сейчас `false` — это риск)
- [ ] `LIVE_KILL_DD_PCT=5.0` (сейчас не задан → fallback на 10%)
- [ ] Проверить TG-polling работает: `/api/kill-switch/status` → `tg_poll_sec: 30`
- [ ] Протестировать `/api/kill-switch/test` с reason=`live_test` (на demo!)
### 4. Code review (см. секцию ниже)
- [ ] Пройтись по всем fix-точкам из MEMORY.md (`*100`-баги, anchor-формулы, percent-конверсии)
- [ ] Сверить все места с `take_profit_percent` (был баг ×4 → anchor $260K)
- [ ] Проверить формулы asymmetric split (3:2 / 2:3)
### 5. Live API smoke test (см. секцию ниже)
- [ ] Тестовый BUY на минимальный lot с ручным отменом через ЛК
- [ ] Тестовый SELL (TP-fill) на минимальный lot
- [ ] Проверить, что `urllib`-запросы реально доходят до Tradernet, нет 403/timeout
---
## ⚙️ Pre-live .env настройки (что поменять)
```bash
# === ОБЯЗАТЕЛЬНО изменить перед live ===
# 1. Trading mode
DEMO_MODE=false # было: true
LIVE_TRADING_DRY_RUN=false # было: true — это переключатель на реальные ордера
# 2. Real Tradernet API credentials (НЕ дефолтные)
TRADERNET_PUBLIC_KEY=<real_key> # отозвать старые demo-ключи
TRADERNET_PRIVATE_KEY=<real_key>
TRADERNET_LOGIN=<real_login>
TRADERNET_PASSWORD=<real_pass>
# 3. Kill switch — ОБЯЗАТЕЛЬНО
LIVE_KILL_CLOSE_POSITION=true # было: false
LIVE_KILL_DD_PCT=5.0 # 5% (было: не задано → 10%)
KILL_DD_PCT=10.0 # для demo остаётся 10%
# 4. Fee rate (после подтверждения в ЛК Cifra)
FEE_RATE_PERCENT=0.2 # ОБНОВИТЬ после проверки точных %
FEE_SAFETY_MULTIPLIER=1.5 # было: 1.25 — увеличить запас
# 5. Strategy — для live понизить агрессивность
STRATEGY_LOCK_PCT=1.5 # было: 2.0 — раньше блокировать
STRATEGY_LOCK_COOLDOWN_SEC=1800 # было: 900 — реже переключаться
# 6. Inactivity rebuild — для live ВЫКЛЮЧИТЬ
INACTIVITY_RESTART_HOURS=999 # было: 6 — на live пусть работает как есть
INACTIVITY_PRICE_DRIFT_PCT=99.0 # было: 0.25 — фактически отключить
# 7. Kronos — пока НЕ включать на live
KRONOS_LIVE_ENABLED=false # оставить false, пока не подтверждена статистика
# === Что НЕ менять ===
GRID_STEP_PERCENT=0.005 # 0.5% — нормально для fee 0.2%
GRID_TAKE_PROFIT_PERCENT=2.0 # 2% TP — нормально
GRID_LEVELS=10 # дефолт (но у нас сейчас 3 в state.json)
STRATEGY_CENTER_MODE=sma # OK
```
> После правок — **обязательно** перезапустить бота с `kill -9 PID` + новый запуск через `nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown`
---
## 🔍 Code review checklist (что перепроверить в коде)
### Pattern 1: Percent vs ratio confusion (исторически баг ×4)
```bash
# Найти все места, где take_profit_percent или step_percent умножаются:
grep -n "take_profit_percent\|step_percent" /root/grid-bot/main.py /root/grid-bot/grid.py
```
**Проверить каждое вхождение:**
- Если переменная хранится в **процентах** (2.0 = 2%), формула должна быть `value / 100.0`
- Если в **долях** (0.02 = 2%), формула должна быть `value` напрямую
- В `grid.py:pnl_percent()` — уже `*100`, значит внутри ratio
### Pattern 2: Anchor calculation
```bash
grep -n "sell_anchor\|entry_price.*1\.\|anchor_for_filled" /root/grid-bot/main.py
```
- Убедиться что **anchor = entry_price × (1 + take_profit_percent / 100)**
- НЕ `entry_price * (1.0 + take_profit_percent)` (это и был баг 17.06, anchor × 4)
### Pattern 3: Live order placement
```bash
grep -n "_place_live_buy\|_place_live_sell\|_live_cancel" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- Проверить, что `LIVE_TRADING_DRY_RUN=false` реально идёт в код, не в логику демо
- Проверить обработку ошибок `403 / 429 / timeout` — бот должен ретраить ИЛИ тихо отменять (не падать)
- Проверить, что при ошибке API бот НЕ дублирует ордер
### Pattern 4: Asymmetric grid split
```bash
grep -n "STRATEGY_ASYMMETRY\|buy.*sell\|split" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- bias=up → 3 BUY / 2 SELL (anchor выше)
- bias=down → 2 BUY / 3 SELL (anchor ниже)
- bias=range → 3 BUY / 2 SELL (default)
### Pattern 5: Peak equity reset
```bash
grep -n "peak_equity\|_peak_lock" /root/grid-bot/main.py
```
- При live-старте вызвать `kill_switch.reset()` — peak начнёт отсчитываться от live-баланса
- Без reset — KS может триггернуться сразу (peak=$375.68 demo, на live у нас меньше)
### Pattern 6: 403 авто-pause (требует НОВОГО кода) ⭐
**Требование @KAndrew:** при HTTP 403 от Tradernet API → авто-pause бота, НЕ kill.
**Сейчас:** KS считает все API errors одинаково (`api_errors_threshold=5` за 600 сек → kill).
**Что нужно сделать:**
- [ ] Разделить счётчики ошибок: `api_errors_403` (auth/perm) vs `api_errors_5xx` (infra)
- [ ] При `api_errors_403 >= 1` → bot_running=false (pause), KS НЕ triggered, reason=`auth_403`
- [ ] При `api_errors_5xx >= 5` за 600 сек → KS trigger (как сейчас)
- [ ] При resume (после ручного `/api/bot/start`) — счётчики сбрасываются
- [ ] TG-алерт при любой 403: «⚠️ 403 от Tradernet — бот поставлен на паузу»
**Файл для правки:** `/root/grid-bot/main.py` — секция API error tracking (см. grep `_api_errors`)
**Smoke test после фикса:**
```bash
# На demo: подменить API ключ на невалидный, дождаться 403 → бот должен встать на паузу
sed -i 's/TRADERNET_PUBLIC_KEY=.*/TRADERNET_PUBLIC_KEY=INVALID_KEY/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py'); cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 60
curl -s -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/status | grep bot_running
# → должно быть bot_running: false, reason: auth_403
# Восстановить ключ!
```
---
## 🧪 Smoke test (перед переключением на live)
### Pre-flight (на demo)
```bash
# 1. Kill switch готов?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/kill-switch/status
# → enabled=true, triggered=false, reason=none, peak=$375.68
# 2. Тест KS в demo (НЕ в live!)
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/test
# → reason="test:..." triggered=true
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/reset
# → reason="none"
# 3. API ключи валидны?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/account
# → account_id, balance > 0, currency=USDT
```
### Live dry-run (минимальная позиция)
```bash
# 1. Переключить на live dry-run
sed -i 's/LIVE_TRADING_DRY_RUN=true/LIVE_TRADING_DRY_RUN=false/' /root/grid-bot/.env
# (НЕ ТРОГАТЬ DEMO_MODE — оставить true чтобы не запускать боевую логику)
# 2. Запустить, наблюдать 1 час
kill -9 $(pgrep -f 'python.*main.py')
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 3600
# 3. Проверить логи
grep -E 'LIVE_|PLACE|ERROR' /root/grid-bot/bot.log | tail -30
# 4. Откатить если что-то не так
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py')
```
---
## 🚀 Phased rollout plan (целевой размер $135)
### Phase 0 — Smoke (1 день)
- `DEMO_MODE=true`, `LIVE_TRADING_DRY_RUN=false` (только меняем флаг, не логику)
- Наблюдаем 1 день — бот реально стучится в Tradernet API, ордера не размещает
- Проверяем логи на 403/timeout/retry
### Phase 1 — Micro-live Week 1 (5–7 дней)
- **$40** (≈30% от $135)
- `GRID_LEVELS=3`, `GRID_STEP_PERCENT=0.005`, `GRID_TAKE_PROFIT_PERCENT=2.0`
- `LIVE_KILL_DD_PCT=5.0`, `LIVE_KILL_CLOSE_POSITION=true`
- `KRONOS_LIVE_ENABLED=false` (default, осторожно)
- 24/7 (по решению @KAndrew)
- Авто-pause при 403 (после код-фикса)
- Ежедневный отчёт в TG (PnL, win-rate, max DD, #trades) — `@Kolp` генерит сам
### Phase 2 — Half Week 2 (5–7 дней)
- **$90** (≈67% от $135)
- Если Phase 1: PnL > 0 И DD < 3% И win-rate > 50%
- Иначе: остаёмся на $40 ещё на неделю
### Phase 3 — Full Week 3-4
- **$135** (100%)
- Если Phase 2 стабильно PnL > 0 И DD < 3%
- Включить `KRONOS_LIVE_ENABLED=true` (опционально, после подтверждения Kronos-метрик в demo)
### Steady state
- 100% ($135), 24/7, full automation
- Weekly отчёт в TG (можно выключить если утомит)
> ❌ **Откатить на Phase N-1 (или в demo) если:** DD > 5%, > 3 API errors подряд, любой phantom-order баг, > 5 trades подряд в убыток, TG spam от kill-switch.
---
## ↩️ Rollback plan (если что-то пошло не так)
```bash
# 1. Остановить бота
kill -9 $(pgrep -f 'python.*main.py')
# 2. Откатить .env на demo
sed -i 's/DEMO_MODE=false/DEMO_MODE=true/' /root/grid-bot/.env
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
# 3. Вручную закрыть все висящие ордера через ЛК cifra.by → «Активные заявки»
# 4. Запустить demo снова
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
# 5. Сделать snapshot кода
cp -r /root/grid-bot /root/grid-bot.live-fail-$(date +%Y%m%d_%H%M%S)
# 6. Проверить убытки в ЛК cifra.by → История сделок
```
---
## ✅ Решения (ответы @KAndrew, 26.06 23:41 GMT+7)
| # | Вопрос | Решение |
|---|---|---|
| 1 | Размер позиции на live | **$135** (целевой финальный размер) |
| 2 | Одновременно ордеров | **3** (как сейчас в state.json) |
| 3 | Kronos live | **default = `false`** (не ответил, оставляем безопасный default до подтверждения статистики) |
| 4 | Ночной режим | **24/7** (без QUIET_HOURS) |
| 5 | Если API 403 | **авто-pause** (НЕ kill) — требует код-фикса, см. Code review §6 |
| 6 | TG-отчёты | **default = текущее поведение** (только алерты от KS, по сделкам не спамим) |
> Все defaults можно переопределить — скажи явно.
---
## 📊 Текущие метрики (для сравнения с live)
| Метрика | Demo сейчас | Live минимум |
|---|---|---|
| Total trades | 13 | 30+ (закрытых) |
| Total PnL | $3.06 | > $0 за неделю |
| Peak equity | $375.68 | — |
| Max DD | −4.86% | < 5% |
| Win-rate | 1/1 = 100% (n=1) | > 50% (n≥20) |
| Avg TP-fill time | ~2ч | — |
| Avg step fill | ~30 мин | — |
| Days running | ~14 | 14+ |
| Uptime | ~95% (были рестарты) | > 98% |
---
## 🔗 Связанные файлы
- `/root/grid-bot/.env` — основные настройки
- `/root/grid-bot/config.py` — defaults (все через `os.getenv`)
- `/root/grid-bot/main.py` — trading loop + KS + state.json
- `/root/grid-bot/grid.py` — grid logic, TP, fills
- `/root/grid-bot/api.py` — Tradernet REST client
- `~/.openclaw/workspace/MEMORY.md` — патчи, баги, решения
- `~/.openclaw/workspace/memory/2026-06-*.md` — daily notes
---
**Автор:** Kolp (OpenClaw + M3)
**Версия:** 1.1 (обновлено 2026-06-26 23:41 GMT+7)
**История:**
- v1.0 (23:30) — первая версия, open questions
- v1.1 (23:41) — ответы @KAndrew: $135, 3 ордера, 24/7, 403→pause; TODO: код-фикс auto-pause
**Следующий review:** когда @KAndrew подтвердит defaults по Kronos и TG-отчётам
+324
View File
@@ -0,0 +1,324 @@
# LIVE TRADING CHECKLIST — grid-bot BTC-USDT.IMEX
**Дата создания:** 2026-06-26 23:30 GMT+7
**Last update:** 2026-06-26 23:41 GMT+7 — ответы @KAndrew на open questions
**Бот:** grid-bot v0.13.x (PID 1552921, mode=demo, peak=$375.68)
**Цель:** выход на реальную торговлю через Cifra Markets (BY) / Tradernet
**Целевой размер позиции (от @KAndrew):** $135
> ⚠️ **Текущий статус: НЕ ГОТОВ.** Ниже — что должно быть сделано перед переключением `LIVE_TRADING_DRY_RUN=false`.
---
## 🚦 Блокеры (без них live = деньги на ветер)
### 1. Подтвердить fees Cifra Markets
- [ ] Зайти в ЛК **cifra.by** → раздел «Тарифы» / «Комиссии»
- [ ] Записать **точные %** для BTC-USDT: maker / taker, market / limit
- [ ] Если fee > 0.25% per side → обновить `FEE_RATE_PERCENT` и пересмотреть `GRID_STEP_PERCENT`
- [ ] Учесть спред bid/ask (сейчас по quote: 0.09% — мелкий, проверить на реальных сделках)
### 2. Накопить demo-метрики
- [ ] Минимум **30 закрытых сделок** (сейчас 13) — не дни, а TP-fill-ы
- [ ] Минимум **14 дней** непрерывной работы в demo без критических багов
- [ ] **Win-rate > 50%** (TP-fill / total fills)
- [ ] **Avg PnL per trade > 0** после учёта fees
- [ ] **Max DD < 8%** за весь период (у нас сейчас −4.86% от peak)
### 3. Настроить kill switch для live
- [ ] `LIVE_KILL_CLOSE_POSITION=true` (сейчас `false` — это риск)
- [ ] `LIVE_KILL_DD_PCT=5.0` (сейчас не задан → fallback на 10%)
- [ ] Проверить TG-polling работает: `/api/kill-switch/status` → `tg_poll_sec: 30`
- [ ] Протестировать `/api/kill-switch/test` с reason=`live_test` (на demo!)
### 4. Code review (см. секцию ниже)
- [ ] Пройтись по всем fix-точкам из MEMORY.md (`*100`-баги, anchor-формулы, percent-конверсии)
- [ ] Сверить все места с `take_profit_percent` (был баг ×4 → anchor $260K)
- [ ] Проверить формулы asymmetric split (3:2 / 2:3)
### 5. Live API smoke test (см. секцию ниже)
- [ ] Тестовый BUY на минимальный lot с ручным отменом через ЛК
- [ ] Тестовый SELL (TP-fill) на минимальный lot
- [ ] Проверить, что `urllib`-запросы реально доходят до Tradernet, нет 403/timeout
---
## ⚙️ Pre-live .env настройки (что поменять)
```bash
# === ОБЯЗАТЕЛЬНО изменить перед live ===
# 1. Trading mode
DEMO_MODE=false # было: true
LIVE_TRADING_DRY_RUN=false # было: true — это переключатель на реальные ордера
# 2. Real Tradernet API credentials (НЕ дефолтные)
TRADERNET_PUBLIC_KEY=<real_key> # отозвать старые demo-ключи
TRADERNET_PRIVATE_KEY=<real_key>
TRADERNET_LOGIN=<real_login>
TRADERNET_PASSWORD=<real_pass>
# 3. Kill switch — ОБЯЗАТЕЛЬНО
LIVE_KILL_CLOSE_POSITION=true # было: false
LIVE_KILL_DD_PCT=5.0 # 5% (было: не задано → 10%)
KILL_DD_PCT=10.0 # для demo остаётся 10%
# 4. Fee rate (после подтверждения в ЛК Cifra)
FEE_RATE_PERCENT=0.2 # ОБНОВИТЬ после проверки точных %
FEE_SAFETY_MULTIPLIER=1.5 # было: 1.25 — увеличить запас
# 5. Strategy — для live понизить агрессивность
STRATEGY_LOCK_PCT=1.5 # было: 2.0 — раньше блокировать
STRATEGY_LOCK_COOLDOWN_SEC=1800 # было: 900 — реже переключаться
# 6. Inactivity rebuild — для live ВЫКЛЮЧИТЬ
INACTIVITY_RESTART_HOURS=999 # было: 6 — на live пусть работает как есть
INACTIVITY_PRICE_DRIFT_PCT=99.0 # было: 0.25 — фактически отключить
# 7. Kronos — пока НЕ включать на live
KRONOS_LIVE_ENABLED=false # оставить false, пока не подтверждена статистика
# === Что НЕ менять ===
GRID_STEP_PERCENT=0.005 # 0.5% — нормально для fee 0.2%
GRID_TAKE_PROFIT_PERCENT=2.0 # 2% TP — нормально
GRID_LEVELS=10 # дефолт (но у нас сейчас 3 в state.json)
STRATEGY_CENTER_MODE=sma # OK
```
> После правок — **обязательно** перезапустить бота с `kill -9 PID` + новый запуск через `nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown`
---
## 🔍 Code review checklist (что перепроверить в коде)
### Pattern 1: Percent vs ratio confusion (исторически баг ×4)
```bash
# Найти все места, где take_profit_percent или step_percent умножаются:
grep -n "take_profit_percent\|step_percent" /root/grid-bot/main.py /root/grid-bot/grid.py
```
**Проверить каждое вхождение:**
- Если переменная хранится в **процентах** (2.0 = 2%), формула должна быть `value / 100.0`
- Если в **долях** (0.02 = 2%), формула должна быть `value` напрямую
- В `grid.py:pnl_percent()` — уже `*100`, значит внутри ratio
### Pattern 2: Anchor calculation
```bash
grep -n "sell_anchor\|entry_price.*1\.\|anchor_for_filled" /root/grid-bot/main.py
```
- Убедиться что **anchor = entry_price × (1 + take_profit_percent / 100)**
- НЕ `entry_price * (1.0 + take_profit_percent)` (это и был баг 17.06, anchor × 4)
### Pattern 3: Live order placement
```bash
grep -n "_place_live_buy\|_place_live_sell\|_live_cancel" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- Проверить, что `LIVE_TRADING_DRY_RUN=false` реально идёт в код, не в логику демо
- Проверить обработку ошибок `403 / 429 / timeout` — бот должен ретраить ИЛИ тихо отменять (не падать)
- Проверить, что при ошибке API бот НЕ дублирует ордер
### Pattern 4: Asymmetric grid split
```bash
grep -n "STRATEGY_ASYMMETRY\|buy.*sell\|split" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- bias=up → 3 BUY / 2 SELL (anchor выше)
- bias=down → 2 BUY / 3 SELL (anchor ниже)
- bias=range → 3 BUY / 2 SELL (default)
### Pattern 5: Peak equity reset
```bash
grep -n "peak_equity\|_peak_lock" /root/grid-bot/main.py
```
- При live-старте вызвать `kill_switch.reset()` — peak начнёт отсчитываться от live-баланса
- Без reset — KS может триггернуться сразу (peak=$375.68 demo, на live у нас меньше)
### Pattern 6: 403 авто-pause (требует НОВОГО кода) ⭐
**Требование @KAndrew:** при HTTP 403 от Tradernet API → авто-pause бота, НЕ kill.
**Сейчас:** KS считает все API errors одинаково (`api_errors_threshold=5` за 600 сек → kill).
**Что нужно сделать:**
- [ ] Разделить счётчики ошибок: `api_errors_403` (auth/perm) vs `api_errors_5xx` (infra)
- [ ] При `api_errors_403 >= 1` → bot_running=false (pause), KS НЕ triggered, reason=`auth_403`
- [ ] При `api_errors_5xx >= 5` за 600 сек → KS trigger (как сейчас)
- [ ] При resume (после ручного `/api/bot/start`) — счётчики сбрасываются
- [ ] TG-алерт при любой 403: «⚠️ 403 от Tradernet — бот поставлен на паузу»
**Файл для правки:** `/root/grid-bot/main.py` — секция API error tracking (см. grep `_api_errors`)
**Smoke test после фикса:**
```bash
# На demo: подменить API ключ на невалидный, дождаться 403 → бот должен встать на паузу
sed -i 's/TRADERNET_PUBLIC_KEY=.*/TRADERNET_PUBLIC_KEY=INVALID_KEY/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py'); cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 60
curl -s -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/status | grep bot_running
# → должно быть bot_running: false, reason: auth_403
# Восстановить ключ!
```
### Pattern 7: TG-уведомления на сделку (требует НОВОГО кода) ⭐
**Требование @KAndrew:** каждая сделка (BUY fill / SELL fill / TP-fill) → TG-сообщение с деталями.
**Сейчас:** TG вызывается только при `tg_notify()` в `main.py:384` (KS-alerts, важные изменения). Сами сделки НЕ спамят.
**Что нужно сделать:**
- [ ] Хук в `check_fill_buy` / `check_fill_sell` / `check_take_profit` после успешного fill → `tg_notify(...)` с шаблоном:
- BUY: `🟢 BUY fill @ $XXX (qty Y, balance $Z)`
- SELL: `🔴 SELL fill @ $XXX (pnl +$W)`
- TP-fill: `🎯 TP @ $XXX (pnl +$W, total trades: N)`
- [ ] Anti-spam: не слать TG если fill дубликат (idempotency по order_id)
- [ ] Quiet hours НЕ применяются (24/7 по решению @KAndrew)
- [ ] Тест: дождаться следующего BUY fill на demo → проверить TG
**Файлы для правки:** `/root/grid-bot/main.py` (buy/sell fill handlers) + `/root/grid-bot/grid.py` (TP logic)
---
## 🧪 Smoke test (перед переключением на live)
### Pre-flight (на demo)
```bash
# 1. Kill switch готов?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/kill-switch/status
# → enabled=true, triggered=false, reason=none, peak=$375.68
# 2. Тест KS в demo (НЕ в live!)
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/test
# → reason="test:..." triggered=true
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/reset
# → reason="none"
# 3. API ключи валидны?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/account
# → account_id, balance > 0, currency=USDT
```
### Live dry-run (минимальная позиция)
```bash
# 1. Переключить на live dry-run
sed -i 's/LIVE_TRADING_DRY_RUN=true/LIVE_TRADING_DRY_RUN=false/' /root/grid-bot/.env
# (НЕ ТРОГАТЬ DEMO_MODE — оставить true чтобы не запускать боевую логику)
# 2. Запустить, наблюдать 1 час
kill -9 $(pgrep -f 'python.*main.py')
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 3600
# 3. Проверить логи
grep -E 'LIVE_|PLACE|ERROR' /root/grid-bot/bot.log | tail -30
# 4. Откатить если что-то не так
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py')
```
---
## 🚀 Phased rollout plan (целевой размер $135)
### Phase 0 — Smoke (1 день)
- `DEMO_MODE=true`, `LIVE_TRADING_DRY_RUN=false` (только меняем флаг, не логику)
- Наблюдаем 1 день — бот реально стучится в Tradernet API, ордера не размещает
- Проверяем логи на 403/timeout/retry
### Phase 1 — Micro-live Week 1 (5–7 дней)
- **$40** (≈30% от $135)
- `GRID_LEVELS=3`, `GRID_STEP_PERCENT=0.005`, `GRID_TAKE_PROFIT_PERCENT=2.0`
- `LIVE_KILL_DD_PCT=5.0`, `LIVE_KILL_CLOSE_POSITION=true`
- `KRONOS_LIVE_ENABLED=false` (default, осторожно)
- 24/7 (по решению @KAndrew)
- Авто-pause при 403 (после код-фикса)
- Ежедневный отчёт в TG (PnL, win-rate, max DD, #trades) — `@Kolp` генерит сам
### Phase 2 — Half Week 2 (5–7 дней)
- **$90** (≈67% от $135)
- Если Phase 1: PnL > 0 И DD < 3% И win-rate > 50%
- Иначе: остаёмся на $40 ещё на неделю
### Phase 3 — Full Week 3-4
- **$135** (100%)
- Если Phase 2 стабильно PnL > 0 И DD < 3%
- Включить `KRONOS_LIVE_ENABLED=true` (опционально, после подтверждения Kronos-метрик в demo)
### Steady state
- 100% ($135), 24/7, full automation
- Weekly отчёт в TG (можно выключить если утомит)
> ❌ **Откатить на Phase N-1 (или в demo) если:** DD > 5%, > 3 API errors подряд, любой phantom-order баг, > 5 trades подряд в убыток, TG spam от kill-switch.
---
## ↩️ Rollback plan (если что-то пошло не так)
```bash
# 1. Остановить бота
kill -9 $(pgrep -f 'python.*main.py')
# 2. Откатить .env на demo
sed -i 's/DEMO_MODE=false/DEMO_MODE=true/' /root/grid-bot/.env
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
# 3. Вручную закрыть все висящие ордера через ЛК cifra.by → «Активные заявки»
# 4. Запустить demo снова
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
# 5. Сделать snapshot кода
cp -r /root/grid-bot /root/grid-bot.live-fail-$(date +%Y%m%d_%H%M%S)
# 6. Проверить убытки в ЛК cifra.by → История сделок
```
---
## ✅ Решения (ответы @KAndrew, 26.06 23:41 GMT+7)
| # | Вопрос | Решение |
|---|---|---|
| 1 | Размер позиции на live | **$135** (целевой финальный размер) |
| 2 | Одновременно ордеров | **3** (как сейчас в state.json) |
| 3 | Kronos live | **PENDING** — @Kolp рекомендовал вариант А (выключить навсегда). @KAndrew не подтвердил, ждём решения |
| 4 | Ночной режим | **24/7** (без QUIET_HOURS) |
| 5 | Если API 403 | **авто-pause** (НЕ kill) — требует код-фикса, см. Code review §6 |
| 6 | TG-отчёты | **на сделку** (каждое BUY/SELL/TP-fill → TG-сообщение) — требует код-фикса, см. Code review §7 |
> Все defaults можно переопределить — скажи явно.
---
## 📊 Текущие метрики (для сравнения с live)
| Метрика | Demo сейчас | Live минимум |
|---|---|---|
| Total trades | 13 | 30+ (закрытых) |
| Total PnL | $3.06 | > $0 за неделю |
| Peak equity | $375.68 | — |
| Max DD | −4.86% | < 5% |
| Win-rate | 1/1 = 100% (n=1) | > 50% (n≥20) |
| Avg TP-fill time | ~2ч | — |
| Avg step fill | ~30 мин | — |
| Days running | ~14 | 14+ |
| Uptime | ~95% (были рестарты) | > 98% |
---
## 🔗 Связанные файлы
- `/root/grid-bot/.env` — основные настройки
- `/root/grid-bot/config.py` — defaults (все через `os.getenv`)
- `/root/grid-bot/main.py` — trading loop + KS + state.json
- `/root/grid-bot/grid.py` — grid logic, TP, fills
- `/root/grid-bot/api.py` — Tradernet REST client
- `~/.openclaw/workspace/MEMORY.md` — патчи, баги, решения
- `~/.openclaw/workspace/memory/2026-06-*.md` — daily notes
---
**Автор:** Kolp (OpenClaw + M3)
**Версия:** 1.1 (обновлено 2026-06-26 23:41 GMT+7)
**История:**
- v1.0 (23:30) — первая версия, open questions
- v1.1 (23:41) — ответы @KAndrew: $135, 3 ордера, 24/7, 403→pause; TODO: код-фикс auto-pause
- v1.2 (23:42) — TG-отчёты на сделку (Pattern 7); Kronos пока PENDING (ждём решение @KAndrew)
**Следующий review:** когда @KAndrew подтвердит Kronos (рекомендация @Kolp: вариант А — выключить навсегда)
+325
View File
@@ -0,0 +1,325 @@
# LIVE TRADING CHECKLIST — grid-bot BTC-USDT.IMEX
**Дата создания:** 2026-06-26 23:30 GMT+7
**Last update:** 2026-06-26 23:41 GMT+7 — ответы @KAndrew на open questions
**Бот:** grid-bot v0.13.x (PID 1552921, mode=demo, peak=$375.68)
**Цель:** выход на реальную торговлю через Cifra Markets (BY) / Tradernet
**Целевой размер позиции (от @KAndrew):** $135
> ⚠️ **Текущий статус: НЕ ГОТОВ.** Ниже — что должно быть сделано перед переключением `LIVE_TRADING_DRY_RUN=false`.
---
## 🚦 Блокеры (без них live = деньги на ветер)
### 1. Подтвердить fees Cifra Markets
- [ ] Зайти в ЛК **cifra.by** → раздел «Тарифы» / «Комиссии»
- [ ] Записать **точные %** для BTC-USDT: maker / taker, market / limit
- [ ] Если fee > 0.25% per side → обновить `FEE_RATE_PERCENT` и пересмотреть `GRID_STEP_PERCENT`
- [ ] Учесть спред bid/ask (сейчас по quote: 0.09% — мелкий, проверить на реальных сделках)
### 2. Накопить demo-метрики
- [ ] Минимум **30 закрытых сделок** (сейчас 13) — не дни, а TP-fill-ы
- [ ] Минимум **14 дней** непрерывной работы в demo без критических багов
- [ ] **Win-rate > 50%** (TP-fill / total fills)
- [ ] **Avg PnL per trade > 0** после учёта fees
- [ ] **Max DD < 8%** за весь период (у нас сейчас −4.86% от peak)
### 3. Настроить kill switch для live
- [ ] `LIVE_KILL_CLOSE_POSITION=true` (сейчас `false` — это риск)
- [ ] `LIVE_KILL_DD_PCT=5.0` (сейчас не задан → fallback на 10%)
- [ ] Проверить TG-polling работает: `/api/kill-switch/status` → `tg_poll_sec: 30`
- [ ] Протестировать `/api/kill-switch/test` с reason=`live_test` (на demo!)
### 4. Code review (см. секцию ниже)
- [ ] Пройтись по всем fix-точкам из MEMORY.md (`*100`-баги, anchor-формулы, percent-конверсии)
- [ ] Сверить все места с `take_profit_percent` (был баг ×4 → anchor $260K)
- [ ] Проверить формулы asymmetric split (3:2 / 2:3)
### 5. Live API smoke test (см. секцию ниже)
- [ ] Тестовый BUY на минимальный lot с ручным отменом через ЛК
- [ ] Тестовый SELL (TP-fill) на минимальный lot
- [ ] Проверить, что `urllib`-запросы реально доходят до Tradernet, нет 403/timeout
---
## ⚙️ Pre-live .env настройки (что поменять)
```bash
# === ОБЯЗАТЕЛЬНО изменить перед live ===
# 1. Trading mode
DEMO_MODE=false # было: true
LIVE_TRADING_DRY_RUN=false # было: true — это переключатель на реальные ордера
# 2. Real Tradernet API credentials (НЕ дефолтные)
TRADERNET_PUBLIC_KEY=<real_key> # отозвать старые demo-ключи
TRADERNET_PRIVATE_KEY=<real_key>
TRADERNET_LOGIN=<real_login>
TRADERNET_PASSWORD=<real_pass>
# 3. Kill switch — ОБЯЗАТЕЛЬНО
LIVE_KILL_CLOSE_POSITION=true # было: false
LIVE_KILL_DD_PCT=5.0 # 5% (было: не задано → 10%)
KILL_DD_PCT=10.0 # для demo остаётся 10%
# 4. Fee rate (после подтверждения в ЛК Cifra)
FEE_RATE_PERCENT=0.2 # ОБНОВИТЬ после проверки точных %
FEE_SAFETY_MULTIPLIER=1.5 # было: 1.25 — увеличить запас
# 5. Strategy — для live понизить агрессивность
STRATEGY_LOCK_PCT=1.5 # было: 2.0 — раньше блокировать
STRATEGY_LOCK_COOLDOWN_SEC=1800 # было: 900 — реже переключаться
# 6. Inactivity rebuild — для live ВЫКЛЮЧИТЬ
INACTIVITY_RESTART_HOURS=999 # было: 6 — на live пусть работает как есть
INACTIVITY_PRICE_DRIFT_PCT=99.0 # было: 0.25 — фактически отключить
# 7. Kronos — пока НЕ включать на live
KRONOS_LIVE_ENABLED=false # оставить false, пока не подтверждена статистика
# === Что НЕ менять ===
GRID_STEP_PERCENT=0.005 # 0.5% — нормально для fee 0.2%
GRID_TAKE_PROFIT_PERCENT=2.0 # 2% TP — нормально
GRID_LEVELS=10 # дефолт (но у нас сейчас 3 в state.json)
STRATEGY_CENTER_MODE=sma # OK
```
> После правок — **обязательно** перезапустить бота с `kill -9 PID` + новый запуск через `nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown`
---
## 🔍 Code review checklist (что перепроверить в коде)
### Pattern 1: Percent vs ratio confusion (исторически баг ×4)
```bash
# Найти все места, где take_profit_percent или step_percent умножаются:
grep -n "take_profit_percent\|step_percent" /root/grid-bot/main.py /root/grid-bot/grid.py
```
**Проверить каждое вхождение:**
- Если переменная хранится в **процентах** (2.0 = 2%), формула должна быть `value / 100.0`
- Если в **долях** (0.02 = 2%), формула должна быть `value` напрямую
- В `grid.py:pnl_percent()` — уже `*100`, значит внутри ratio
### Pattern 2: Anchor calculation
```bash
grep -n "sell_anchor\|entry_price.*1\.\|anchor_for_filled" /root/grid-bot/main.py
```
- Убедиться что **anchor = entry_price × (1 + take_profit_percent / 100)**
- НЕ `entry_price * (1.0 + take_profit_percent)` (это и был баг 17.06, anchor × 4)
### Pattern 3: Live order placement
```bash
grep -n "_place_live_buy\|_place_live_sell\|_live_cancel" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- Проверить, что `LIVE_TRADING_DRY_RUN=false` реально идёт в код, не в логику демо
- Проверить обработку ошибок `403 / 429 / timeout` — бот должен ретраить ИЛИ тихо отменять (не падать)
- Проверить, что при ошибке API бот НЕ дублирует ордер
### Pattern 4: Asymmetric grid split
```bash
grep -n "STRATEGY_ASYMMETRY\|buy.*sell\|split" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- bias=up → 3 BUY / 2 SELL (anchor выше)
- bias=down → 2 BUY / 3 SELL (anchor ниже)
- bias=range → 3 BUY / 2 SELL (default)
### Pattern 5: Peak equity reset
```bash
grep -n "peak_equity\|_peak_lock" /root/grid-bot/main.py
```
- При live-старте вызвать `kill_switch.reset()` — peak начнёт отсчитываться от live-баланса
- Без reset — KS может триггернуться сразу (peak=$375.68 demo, на live у нас меньше)
### Pattern 6: 403 авто-pause (требует НОВОГО кода) ⭐
**Требование @KAndrew:** при HTTP 403 от Tradernet API → авто-pause бота, НЕ kill.
**Сейчас:** KS считает все API errors одинаково (`api_errors_threshold=5` за 600 сек → kill).
**Что нужно сделать:**
- [ ] Разделить счётчики ошибок: `api_errors_403` (auth/perm) vs `api_errors_5xx` (infra)
- [ ] При `api_errors_403 >= 1` → bot_running=false (pause), KS НЕ triggered, reason=`auth_403`
- [ ] При `api_errors_5xx >= 5` за 600 сек → KS trigger (как сейчас)
- [ ] При resume (после ручного `/api/bot/start`) — счётчики сбрасываются
- [ ] TG-алерт при любой 403: «⚠️ 403 от Tradernet — бот поставлен на паузу»
**Файл для правки:** `/root/grid-bot/main.py` — секция API error tracking (см. grep `_api_errors`)
**Smoke test после фикса:**
```bash
# На demo: подменить API ключ на невалидный, дождаться 403 → бот должен встать на паузу
sed -i 's/TRADERNET_PUBLIC_KEY=.*/TRADERNET_PUBLIC_KEY=INVALID_KEY/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py'); cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 60
curl -s -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/status | grep bot_running
# → должно быть bot_running: false, reason: auth_403
# Восстановить ключ!
```
### Pattern 7: TG-уведомления на сделку (требует НОВОГО кода) ⭐
**Требование @KAndrew:** каждая сделка (BUY fill / SELL fill / TP-fill) → TG-сообщение с деталями.
**Сейчас:** TG вызывается только при `tg_notify()` в `main.py:384` (KS-alerts, важные изменения). Сами сделки НЕ спамят.
**Что нужно сделать:**
- [ ] Хук в `check_fill_buy` / `check_fill_sell` / `check_take_profit` после успешного fill → `tg_notify(...)` с шаблоном:
- BUY: `🟢 BUY fill @ $XXX (qty Y, balance $Z)`
- SELL: `🔴 SELL fill @ $XXX (pnl +$W)`
- TP-fill: `🎯 TP @ $XXX (pnl +$W, total trades: N)`
- [ ] Anti-spam: не слать TG если fill дубликат (idempotency по order_id)
- [ ] Quiet hours НЕ применяются (24/7 по решению @KAndrew)
- [ ] Тест: дождаться следующего BUY fill на demo → проверить TG
**Файлы для правки:** `/root/grid-bot/main.py` (buy/sell fill handlers) + `/root/grid-bot/grid.py` (TP logic)
---
## 🧪 Smoke test (перед переключением на live)
### Pre-flight (на demo)
```bash
# 1. Kill switch готов?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/kill-switch/status
# → enabled=true, triggered=false, reason=none, peak=$375.68
# 2. Тест KS в demo (НЕ в live!)
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/test
# → reason="test:..." triggered=true
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/reset
# → reason="none"
# 3. API ключи валидны?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/account
# → account_id, balance > 0, currency=USDT
```
### Live dry-run (минимальная позиция)
```bash
# 1. Переключить на live dry-run
sed -i 's/LIVE_TRADING_DRY_RUN=true/LIVE_TRADING_DRY_RUN=false/' /root/grid-bot/.env
# (НЕ ТРОГАТЬ DEMO_MODE — оставить true чтобы не запускать боевую логику)
# 2. Запустить, наблюдать 1 час
kill -9 $(pgrep -f 'python.*main.py')
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 3600
# 3. Проверить логи
grep -E 'LIVE_|PLACE|ERROR' /root/grid-bot/bot.log | tail -30
# 4. Откатить если что-то не так
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py')
```
---
## 🚀 Phased rollout plan (целевой размер $135)
### Phase 0 — Smoke (1 день)
- `DEMO_MODE=true`, `LIVE_TRADING_DRY_RUN=false` (только меняем флаг, не логику)
- Наблюдаем 1 день — бот реально стучится в Tradernet API, ордера не размещает
- Проверяем логи на 403/timeout/retry
### Phase 1 — Micro-live Week 1 (5–7 дней)
- **$40** (≈30% от $135)
- `GRID_LEVELS=3`, `GRID_STEP_PERCENT=0.005`, `GRID_TAKE_PROFIT_PERCENT=2.0`
- `LIVE_KILL_DD_PCT=5.0`, `LIVE_KILL_CLOSE_POSITION=true`
- `KRONOS_LIVE_ENABLED=false` (default, осторожно)
- 24/7 (по решению @KAndrew)
- Авто-pause при 403 (после код-фикса)
- Ежедневный отчёт в TG (PnL, win-rate, max DD, #trades) — `@Kolp` генерит сам
### Phase 2 — Half Week 2 (5–7 дней)
- **$90** (≈67% от $135)
- Если Phase 1: PnL > 0 И DD < 3% И win-rate > 50%
- Иначе: остаёмся на $40 ещё на неделю
### Phase 3 — Full Week 3-4
- **$135** (100%)
- Если Phase 2 стабильно PnL > 0 И DD < 3%
- Включить `KRONOS_LIVE_ENABLED=true` (опционально, после подтверждения Kronos-метрик в demo)
### Steady state
- 100% ($135), 24/7, full automation
- Weekly отчёт в TG (можно выключить если утомит)
> ❌ **Откатить на Phase N-1 (или в demo) если:** DD > 5%, > 3 API errors подряд, любой phantom-order баг, > 5 trades подряд в убыток, TG spam от kill-switch.
---
## ↩️ Rollback plan (если что-то пошло не так)
```bash
# 1. Остановить бота
kill -9 $(pgrep -f 'python.*main.py')
# 2. Откатить .env на demo
sed -i 's/DEMO_MODE=false/DEMO_MODE=true/' /root/grid-bot/.env
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
# 3. Вручную закрыть все висящие ордера через ЛК cifra.by → «Активные заявки»
# 4. Запустить demo снова
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
# 5. Сделать snapshot кода
cp -r /root/grid-bot /root/grid-bot.live-fail-$(date +%Y%m%d_%H%M%S)
# 6. Проверить убытки в ЛК cifra.by → История сделок
```
---
## ✅ Решения (ответы @KAndrew, 26.06 23:41 GMT+7)
| # | Вопрос | Решение |
|---|---|---|
| 1 | Размер позиции на live | **$135** (целевой финальный размер) |
| 2 | Одновременно ордеров | **3** (как сейчас в state.json) |
| 3 | Kronos live | **Вариант А — выключить навсегда** (подтверждено @KAndrew). Код оставить как legacy, `STRATEGY_BIAS_SOURCE=trend` |
| 4 | Ночной режим | **24/7** (без QUIET_HOURS) |
| 5 | Если API 403 | **авто-pause** (НЕ kill) — требует код-фикса, см. Code review §6 |
| 6 | TG-отчёты | **на сделку** (каждое BUY/SELL/TP-fill → TG-сообщение) — требует код-фикса, см. Code review §7 |
> Все defaults можно переопределить — скажи явно.
---
## 📊 Текущие метрики (для сравнения с live)
| Метрика | Demo сейчас | Live минимум |
|---|---|---|
| Total trades | 13 | 30+ (закрытых) |
| Total PnL | $3.06 | > $0 за неделю |
| Peak equity | $375.68 | — |
| Max DD | −4.86% | < 5% |
| Win-rate | 1/1 = 100% (n=1) | > 50% (n≥20) |
| Avg TP-fill time | ~2ч | — |
| Avg step fill | ~30 мин | — |
| Days running | ~14 | 14+ |
| Uptime | ~95% (были рестарты) | > 98% |
---
## 🔗 Связанные файлы
- `/root/grid-bot/.env` — основные настройки
- `/root/grid-bot/config.py` — defaults (все через `os.getenv`)
- `/root/grid-bot/main.py` — trading loop + KS + state.json
- `/root/grid-bot/grid.py` — grid logic, TP, fills
- `/root/grid-bot/api.py` — Tradernet REST client
- `~/.openclaw/workspace/MEMORY.md` — патчи, баги, решения
- `~/.openclaw/workspace/memory/2026-06-*.md` — daily notes
---
**Автор:** Kolp (OpenClaw + M3)
**Версия:** 1.1 (обновлено 2026-06-26 23:41 GMT+7)
**История:**
- v1.0 (23:30) — первая версия, open questions
- v1.1 (23:41) — ответы @KAndrew: $135, 3 ордера, 24/7, 403→pause; TODO: код-фикс auto-pause
- v1.2 (23:42) — TG-отчёты на сделку (Pattern 7); Kronos пока PENDING
- v1.3 (23:45) — Kronos = Вариант А (выключить); TG-формат уточняется (a/b/c)
**Следующий review:** когда @KAndrew подтвердит формат TG-отчётов (a/b/c)
+326
View File
@@ -0,0 +1,326 @@
# LIVE TRADING CHECKLIST — grid-bot BTC-USDT.IMEX
**Дата создания:** 2026-06-26 23:30 GMT+7
**Last update:** 2026-06-26 23:41 GMT+7 — ответы @KAndrew на open questions
**Бот:** grid-bot v0.13.x (PID 1552921, mode=demo, peak=$375.68)
**Цель:** выход на реальную торговлю через Cifra Markets (BY) / Tradernet
**Целевой размер позиции (от @KAndrew):** $135
> ⚠️ **Текущий статус: НЕ ГОТОВ.** Ниже — что должно быть сделано перед переключением `LIVE_TRADING_DRY_RUN=false`.
---
## 🚦 Блокеры (без них live = деньги на ветер)
### 1. Подтвердить fees Cifra Markets
- [ ] Зайти в ЛК **cifra.by** → раздел «Тарифы» / «Комиссии»
- [ ] Записать **точные %** для BTC-USDT: maker / taker, market / limit
- [ ] Если fee > 0.25% per side → обновить `FEE_RATE_PERCENT` и пересмотреть `GRID_STEP_PERCENT`
- [ ] Учесть спред bid/ask (сейчас по quote: 0.09% — мелкий, проверить на реальных сделках)
### 2. Накопить demo-метрики
- [ ] Минимум **30 закрытых сделок** (сейчас 13) — не дни, а TP-fill-ы
- [ ] Минимум **14 дней** непрерывной работы в demo без критических багов
- [ ] **Win-rate > 50%** (TP-fill / total fills)
- [ ] **Avg PnL per trade > 0** после учёта fees
- [ ] **Max DD < 8%** за весь период (у нас сейчас −4.86% от peak)
### 3. Настроить kill switch для live
- [ ] `LIVE_KILL_CLOSE_POSITION=true` (сейчас `false` — это риск)
- [ ] `LIVE_KILL_DD_PCT=5.0` (сейчас не задан → fallback на 10%)
- [ ] Проверить TG-polling работает: `/api/kill-switch/status` → `tg_poll_sec: 30`
- [ ] Протестировать `/api/kill-switch/test` с reason=`live_test` (на demo!)
### 4. Code review (см. секцию ниже)
- [ ] Пройтись по всем fix-точкам из MEMORY.md (`*100`-баги, anchor-формулы, percent-конверсии)
- [ ] Сверить все места с `take_profit_percent` (был баг ×4 → anchor $260K)
- [ ] Проверить формулы asymmetric split (3:2 / 2:3)
### 5. Live API smoke test (см. секцию ниже)
- [ ] Тестовый BUY на минимальный lot с ручным отменом через ЛК
- [ ] Тестовый SELL (TP-fill) на минимальный lot
- [ ] Проверить, что `urllib`-запросы реально доходят до Tradernet, нет 403/timeout
---
## ⚙️ Pre-live .env настройки (что поменять)
```bash
# === ОБЯЗАТЕЛЬНО изменить перед live ===
# 1. Trading mode
DEMO_MODE=false # было: true
LIVE_TRADING_DRY_RUN=false # было: true — это переключатель на реальные ордера
# 2. Real Tradernet API credentials (НЕ дефолтные)
TRADERNET_PUBLIC_KEY=<real_key> # отозвать старые demo-ключи
TRADERNET_PRIVATE_KEY=<real_key>
TRADERNET_LOGIN=<real_login>
TRADERNET_PASSWORD=<real_pass>
# 3. Kill switch — ОБЯЗАТЕЛЬНО
LIVE_KILL_CLOSE_POSITION=true # было: false
LIVE_KILL_DD_PCT=5.0 # 5% (было: не задано → 10%)
KILL_DD_PCT=10.0 # для demo остаётся 10%
# 4. Fee rate (после подтверждения в ЛК Cifra)
FEE_RATE_PERCENT=0.2 # ОБНОВИТЬ после проверки точных %
FEE_SAFETY_MULTIPLIER=1.5 # было: 1.25 — увеличить запас
# 5. Strategy — для live понизить агрессивность
STRATEGY_LOCK_PCT=1.5 # было: 2.0 — раньше блокировать
STRATEGY_LOCK_COOLDOWN_SEC=1800 # было: 900 — реже переключаться
# 6. Inactivity rebuild — для live ВЫКЛЮЧИТЬ
INACTIVITY_RESTART_HOURS=999 # было: 6 — на live пусть работает как есть
INACTIVITY_PRICE_DRIFT_PCT=99.0 # было: 0.25 — фактически отключить
# 7. Kronos — пока НЕ включать на live
KRONOS_LIVE_ENABLED=false # оставить false, пока не подтверждена статистика
# === Что НЕ менять ===
GRID_STEP_PERCENT=0.005 # 0.5% — нормально для fee 0.2%
GRID_TAKE_PROFIT_PERCENT=2.0 # 2% TP — нормально
GRID_LEVELS=10 # дефолт (но у нас сейчас 3 в state.json)
STRATEGY_CENTER_MODE=sma # OK
```
> После правок — **обязательно** перезапустить бота с `kill -9 PID` + новый запуск через `nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown`
---
## 🔍 Code review checklist (что перепроверить в коде)
### Pattern 1: Percent vs ratio confusion (исторически баг ×4)
```bash
# Найти все места, где take_profit_percent или step_percent умножаются:
grep -n "take_profit_percent\|step_percent" /root/grid-bot/main.py /root/grid-bot/grid.py
```
**Проверить каждое вхождение:**
- Если переменная хранится в **процентах** (2.0 = 2%), формула должна быть `value / 100.0`
- Если в **долях** (0.02 = 2%), формула должна быть `value` напрямую
- В `grid.py:pnl_percent()` — уже `*100`, значит внутри ratio
### Pattern 2: Anchor calculation
```bash
grep -n "sell_anchor\|entry_price.*1\.\|anchor_for_filled" /root/grid-bot/main.py
```
- Убедиться что **anchor = entry_price × (1 + take_profit_percent / 100)**
- НЕ `entry_price * (1.0 + take_profit_percent)` (это и был баг 17.06, anchor × 4)
### Pattern 3: Live order placement
```bash
grep -n "_place_live_buy\|_place_live_sell\|_live_cancel" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- Проверить, что `LIVE_TRADING_DRY_RUN=false` реально идёт в код, не в логику демо
- Проверить обработку ошибок `403 / 429 / timeout` — бот должен ретраить ИЛИ тихо отменять (не падать)
- Проверить, что при ошибке API бот НЕ дублирует ордер
### Pattern 4: Asymmetric grid split
```bash
grep -n "STRATEGY_ASYMMETRY\|buy.*sell\|split" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- bias=up → 3 BUY / 2 SELL (anchor выше)
- bias=down → 2 BUY / 3 SELL (anchor ниже)
- bias=range → 3 BUY / 2 SELL (default)
### Pattern 5: Peak equity reset
```bash
grep -n "peak_equity\|_peak_lock" /root/grid-bot/main.py
```
- При live-старте вызвать `kill_switch.reset()` — peak начнёт отсчитываться от live-баланса
- Без reset — KS может триггернуться сразу (peak=$375.68 demo, на live у нас меньше)
### Pattern 6: 403 авто-pause (требует НОВОГО кода) ⭐
**Требование @KAndrew:** при HTTP 403 от Tradernet API → авто-pause бота, НЕ kill.
**Сейчас:** KS считает все API errors одинаково (`api_errors_threshold=5` за 600 сек → kill).
**Что нужно сделать:**
- [ ] Разделить счётчики ошибок: `api_errors_403` (auth/perm) vs `api_errors_5xx` (infra)
- [ ] При `api_errors_403 >= 1` → bot_running=false (pause), KS НЕ triggered, reason=`auth_403`
- [ ] При `api_errors_5xx >= 5` за 600 сек → KS trigger (как сейчас)
- [ ] При resume (после ручного `/api/bot/start`) — счётчики сбрасываются
- [ ] TG-алерт при любой 403: «⚠️ 403 от Tradernet — бот поставлен на паузу»
**Файл для правки:** `/root/grid-bot/main.py` — секция API error tracking (см. grep `_api_errors`)
**Smoke test после фикса:**
```bash
# На demo: подменить API ключ на невалидный, дождаться 403 → бот должен встать на паузу
sed -i 's/TRADERNET_PUBLIC_KEY=.*/TRADERNET_PUBLIC_KEY=INVALID_KEY/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py'); cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 60
curl -s -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/status | grep bot_running
# → должно быть bot_running: false, reason: auth_403
# Восстановить ключ!
```
### Pattern 7: TG-уведомления на сделку (требует НОВОГО кода) ⭐
**Требование @KAndrew:** каждая сделка (BUY fill / SELL fill / TP-fill) → TG-сообщение с деталями.
**Сейчас:** TG вызывается только при `tg_notify()` в `main.py:384` (KS-alerts, важные изменения). Сами сделки НЕ спамят.
**Что нужно сделать:**
- [ ] Хук в `check_fill_buy` / `check_fill_sell` / `check_take_profit` после успешного fill → `tg_notify(...)` с шаблоном:
- BUY: `🟢 BUY fill @ $XXX (qty Y, balance $Z)`
- SELL: `🔴 SELL fill @ $XXX (pnl +$W)`
- TP-fill: `🎯 TP @ $XXX (pnl +$W, total trades: N)`
- [ ] Anti-spam: не слать TG если fill дубликат (idempotency по order_id)
- [ ] Quiet hours НЕ применяются (24/7 по решению @KAndrew)
- [ ] Тест: дождаться следующего BUY fill на demo → проверить TG
**Файлы для правки:** `/root/grid-bot/main.py` (buy/sell fill handlers) + `/root/grid-bot/grid.py` (TP logic)
---
## 🧪 Smoke test (перед переключением на live)
### Pre-flight (на demo)
```bash
# 1. Kill switch готов?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/kill-switch/status
# → enabled=true, triggered=false, reason=none, peak=$375.68
# 2. Тест KS в demo (НЕ в live!)
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/test
# → reason="test:..." triggered=true
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/reset
# → reason="none"
# 3. API ключи валидны?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/account
# → account_id, balance > 0, currency=USDT
```
### Live dry-run (минимальная позиция)
```bash
# 1. Переключить на live dry-run
sed -i 's/LIVE_TRADING_DRY_RUN=true/LIVE_TRADING_DRY_RUN=false/' /root/grid-bot/.env
# (НЕ ТРОГАТЬ DEMO_MODE — оставить true чтобы не запускать боевую логику)
# 2. Запустить, наблюдать 1 час
kill -9 $(pgrep -f 'python.*main.py')
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 3600
# 3. Проверить логи
grep -E 'LIVE_|PLACE|ERROR' /root/grid-bot/bot.log | tail -30
# 4. Откатить если что-то не так
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py')
```
---
## 🚀 Phased rollout plan (целевой размер $135)
### Phase 0 — Smoke (1 день)
- `DEMO_MODE=true`, `LIVE_TRADING_DRY_RUN=false` (только меняем флаг, не логику)
- Наблюдаем 1 день — бот реально стучится в Tradernet API, ордера не размещает
- Проверяем логи на 403/timeout/retry
### Phase 1 — Micro-live Week 1 (5–7 дней)
- **$40** (≈30% от $135)
- `GRID_LEVELS=3`, `GRID_STEP_PERCENT=0.005`, `GRID_TAKE_PROFIT_PERCENT=2.0`
- `LIVE_KILL_DD_PCT=5.0`, `LIVE_KILL_CLOSE_POSITION=true`
- `KRONOS_LIVE_ENABLED=false` (default, осторожно)
- 24/7 (по решению @KAndrew)
- Авто-pause при 403 (после код-фикса)
- Ежедневный отчёт в TG (PnL, win-rate, max DD, #trades) — `@Kolp` генерит сам
### Phase 2 — Half Week 2 (5–7 дней)
- **$90** (≈67% от $135)
- Если Phase 1: PnL > 0 И DD < 3% И win-rate > 50%
- Иначе: остаёмся на $40 ещё на неделю
### Phase 3 — Full Week 3-4
- **$135** (100%)
- Если Phase 2 стабильно PnL > 0 И DD < 3%
- Включить `KRONOS_LIVE_ENABLED=true` (опционально, после подтверждения Kronos-метрик в demo)
### Steady state
- 100% ($135), 24/7, full automation
- Weekly отчёт в TG (можно выключить если утомит)
> ❌ **Откатить на Phase N-1 (или в demo) если:** DD > 5%, > 3 API errors подряд, любой phantom-order баг, > 5 trades подряд в убыток, TG spam от kill-switch.
---
## ↩️ Rollback plan (если что-то пошло не так)
```bash
# 1. Остановить бота
kill -9 $(pgrep -f 'python.*main.py')
# 2. Откатить .env на demo
sed -i 's/DEMO_MODE=false/DEMO_MODE=true/' /root/grid-bot/.env
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
# 3. Вручную закрыть все висящие ордера через ЛК cifra.by → «Активные заявки»
# 4. Запустить demo снова
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
# 5. Сделать snapshot кода
cp -r /root/grid-bot /root/grid-bot.live-fail-$(date +%Y%m%d_%H%M%S)
# 6. Проверить убытки в ЛК cifra.by → История сделок
```
---
## ✅ Решения (ответы @KAndrew, 26.06 23:41 GMT+7)
| # | Вопрос | Решение |
|---|---|---|
| 1 | Размер позиции на live | **$135** (целевой финальный размер) |
| 2 | Одновременно ордеров | **3** (как сейчас в state.json) |
| 3 | Kronos live | **Вариант А — выключить навсегда** (подтверждено @KAndrew). Код оставить как legacy, `STRATEGY_BIAS_SOURCE=trend` |
| 4 | Ночной режим | **24/7** (без QUIET_HOURS) |
| 5 | Если API 403 | **авто-pause** (НЕ kill) — требует код-фикса, см. Code review §6 |
| 6 | TG-отчёты | **(a) каждое сообщение на сделку** (подтверждено @KAndrew) — BUY/SELL/TP-fill → отдельный TG. Требует код-фикса, см. Code review §7 |
> Все defaults можно переопределить — скажи явно.
---
## 📊 Текущие метрики (для сравнения с live)
| Метрика | Demo сейчас | Live минимум |
|---|---|---|
| Total trades | 13 | 30+ (закрытых) |
| Total PnL | $3.06 | > $0 за неделю |
| Peak equity | $375.68 | — |
| Max DD | −4.86% | < 5% |
| Win-rate | 1/1 = 100% (n=1) | > 50% (n≥20) |
| Avg TP-fill time | ~2ч | — |
| Avg step fill | ~30 мин | — |
| Days running | ~14 | 14+ |
| Uptime | ~95% (были рестарты) | > 98% |
---
## 🔗 Связанные файлы
- `/root/grid-bot/.env` — основные настройки
- `/root/grid-bot/config.py` — defaults (все через `os.getenv`)
- `/root/grid-bot/main.py` — trading loop + KS + state.json
- `/root/grid-bot/grid.py` — grid logic, TP, fills
- `/root/grid-bot/api.py` — Tradernet REST client
- `~/.openclaw/workspace/MEMORY.md` — патчи, баги, решения
- `~/.openclaw/workspace/memory/2026-06-*.md` — daily notes
---
**Автор:** Kolp (OpenClaw + M3)
**Версия:** 1.1 (обновлено 2026-06-26 23:41 GMT+7)
**История:**
- v1.0 (23:30) — первая версия, open questions
- v1.1 (23:41) — ответы @KAndrew: $135, 3 ордера, 24/7, 403→pause; TODO: код-фикс auto-pause
- v1.2 (23:42) — TG-отчёты на сделку (Pattern 7); Kronos пока PENDING
- v1.3 (23:45) — Kronos = Вариант А (выключить); TG-формат уточняется
- v1.4 (23:48) — все 6 open questions закрыты. ✅ ФИНАЛЬНАЯ ВЕРСИЯ. Готовность к go-live = блокер #1 (fees Cifra)
**Следующий шаг:** @KAndrew заходит в ЛК cifra.by → раздел «Тарифы» → записывает fees
+330
View File
@@ -0,0 +1,330 @@
# LIVE TRADING CHECKLIST — grid-bot BTC-USDT.IMEX
**Дата создания:** 2026-06-26 23:30 GMT+7
**Last update:** 2026-06-26 23:41 GMT+7 — ответы @KAndrew на open questions
**Бот:** grid-bot v0.13.x (PID 1552921, mode=demo, peak=$375.68)
**Цель:** выход на реальную торговлю через Cifra Markets (BY) / Tradernet
**Целевой размер позиции (от @KAndrew):** $135
> ⚠️ **Текущий статус: НЕ ГОТОВ.** Ниже — что должно быть сделано перед переключением `LIVE_TRADING_DRY_RUN=false`.
---
## 🚦 Блокеры (без них live = деньги на ветер)
### 1. Подтвердить fees Cifra Markets ✅ ЗАКРЫТ (26.06 23:54)
- [x] Зайти в ЛК **cifra.by** → раздел «Тарифы» / «Комиссии» — @KAndrew подтвердил ~0.15%
- [x] Записать **точные %** для BTC-USDT: **0.15% taker** (по @KAndrew)
- [x] Обновить `.env`: `FEE_RATE_PERCENT=0.15`, `FEE_SAFETY_MULTIPLIER=1.5` (было 0.2 / 1.25)
- [ ] **Уточнить** — это **taker**, **maker** или **общая** цифра? Round-trip = 0.30% (taker×2) или 0.15% (round-trip)
- [ ] При следующем рестарте бота подхватится автоматически
**Расчёт margin:** step=0.5% − round-trip fee=0.30% = **0.20% запаса** (40% step'а). TP=2% − fees=0.30% = **1.70% net profit** на цикл. Безопасно.
### 2. Накопить demo-метрики
- [ ] Минимум **30 закрытых сделок** (сейчас 13) — не дни, а TP-fill-ы
- [ ] Минимум **14 дней** непрерывной работы в demo без критических багов
- [ ] **Win-rate > 50%** (TP-fill / total fills)
- [ ] **Avg PnL per trade > 0** после учёта fees
- [ ] **Max DD < 8%** за весь период (у нас сейчас −4.86% от peak)
### 3. Настроить kill switch для live
- [ ] `LIVE_KILL_CLOSE_POSITION=true` (сейчас `false` — это риск)
- [ ] `LIVE_KILL_DD_PCT=5.0` (сейчас не задан → fallback на 10%)
- [ ] Проверить TG-polling работает: `/api/kill-switch/status` → `tg_poll_sec: 30`
- [ ] Протестировать `/api/kill-switch/test` с reason=`live_test` (на demo!)
### 4. Code review (см. секцию ниже)
- [ ] Пройтись по всем fix-точкам из MEMORY.md (`*100`-баги, anchor-формулы, percent-конверсии)
- [ ] Сверить все места с `take_profit_percent` (был баг ×4 → anchor $260K)
- [ ] Проверить формулы asymmetric split (3:2 / 2:3)
### 5. Live API smoke test (см. секцию ниже)
- [ ] Тестовый BUY на минимальный lot с ручным отменом через ЛК
- [ ] Тестовый SELL (TP-fill) на минимальный lot
- [ ] Проверить, что `urllib`-запросы реально доходят до Tradernet, нет 403/timeout
---
## ⚙️ Pre-live .env настройки (что поменять)
```bash
# === ОБЯЗАТЕЛЬНО изменить перед live ===
# 1. Trading mode
DEMO_MODE=false # было: true
LIVE_TRADING_DRY_RUN=false # было: true — это переключатель на реальные ордера
# 2. Real Tradernet API credentials (НЕ дефолтные)
TRADERNET_PUBLIC_KEY=<real_key> # отозвать старые demo-ключи
TRADERNET_PRIVATE_KEY=<real_key>
TRADERNET_LOGIN=<real_login>
TRADERNET_PASSWORD=<real_pass>
# 3. Kill switch — ОБЯЗАТЕЛЬНО
LIVE_KILL_CLOSE_POSITION=true # было: false
LIVE_KILL_DD_PCT=5.0 # 5% (было: не задано → 10%)
KILL_DD_PCT=10.0 # для demo остаётся 10%
# 4. Fee rate (после подтверждения в ЛК Cifra)
FEE_RATE_PERCENT=0.2 # ОБНОВИТЬ после проверки точных %
FEE_SAFETY_MULTIPLIER=1.5 # было: 1.25 — увеличить запас
# 5. Strategy — для live понизить агрессивность
STRATEGY_LOCK_PCT=1.5 # было: 2.0 — раньше блокировать
STRATEGY_LOCK_COOLDOWN_SEC=1800 # было: 900 — реже переключаться
# 6. Inactivity rebuild — для live ВЫКЛЮЧИТЬ
INACTIVITY_RESTART_HOURS=999 # было: 6 — на live пусть работает как есть
INACTIVITY_PRICE_DRIFT_PCT=99.0 # было: 0.25 — фактически отключить
# 7. Kronos — пока НЕ включать на live
KRONOS_LIVE_ENABLED=false # оставить false, пока не подтверждена статистика
# === Что НЕ менять ===
GRID_STEP_PERCENT=0.005 # 0.5% — нормально для fee 0.2%
GRID_TAKE_PROFIT_PERCENT=2.0 # 2% TP — нормально
GRID_LEVELS=10 # дефолт (но у нас сейчас 3 в state.json)
STRATEGY_CENTER_MODE=sma # OK
```
> После правок — **обязательно** перезапустить бота с `kill -9 PID` + новый запуск через `nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown`
---
## 🔍 Code review checklist (что перепроверить в коде)
### Pattern 1: Percent vs ratio confusion (исторически баг ×4)
```bash
# Найти все места, где take_profit_percent или step_percent умножаются:
grep -n "take_profit_percent\|step_percent" /root/grid-bot/main.py /root/grid-bot/grid.py
```
**Проверить каждое вхождение:**
- Если переменная хранится в **процентах** (2.0 = 2%), формула должна быть `value / 100.0`
- Если в **долях** (0.02 = 2%), формула должна быть `value` напрямую
- В `grid.py:pnl_percent()` — уже `*100`, значит внутри ratio
### Pattern 2: Anchor calculation
```bash
grep -n "sell_anchor\|entry_price.*1\.\|anchor_for_filled" /root/grid-bot/main.py
```
- Убедиться что **anchor = entry_price × (1 + take_profit_percent / 100)**
- НЕ `entry_price * (1.0 + take_profit_percent)` (это и был баг 17.06, anchor × 4)
### Pattern 3: Live order placement
```bash
grep -n "_place_live_buy\|_place_live_sell\|_live_cancel" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- Проверить, что `LIVE_TRADING_DRY_RUN=false` реально идёт в код, не в логику демо
- Проверить обработку ошибок `403 / 429 / timeout` — бот должен ретраить ИЛИ тихо отменять (не падать)
- Проверить, что при ошибке API бот НЕ дублирует ордер
### Pattern 4: Asymmetric grid split
```bash
grep -n "STRATEGY_ASYMMETRY\|buy.*sell\|split" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- bias=up → 3 BUY / 2 SELL (anchor выше)
- bias=down → 2 BUY / 3 SELL (anchor ниже)
- bias=range → 3 BUY / 2 SELL (default)
### Pattern 5: Peak equity reset
```bash
grep -n "peak_equity\|_peak_lock" /root/grid-bot/main.py
```
- При live-старте вызвать `kill_switch.reset()` — peak начнёт отсчитываться от live-баланса
- Без reset — KS может триггернуться сразу (peak=$375.68 demo, на live у нас меньше)
### Pattern 6: 403 авто-pause (требует НОВОГО кода) ⭐
**Требование @KAndrew:** при HTTP 403 от Tradernet API → авто-pause бота, НЕ kill.
**Сейчас:** KS считает все API errors одинаково (`api_errors_threshold=5` за 600 сек → kill).
**Что нужно сделать:**
- [ ] Разделить счётчики ошибок: `api_errors_403` (auth/perm) vs `api_errors_5xx` (infra)
- [ ] При `api_errors_403 >= 1` → bot_running=false (pause), KS НЕ triggered, reason=`auth_403`
- [ ] При `api_errors_5xx >= 5` за 600 сек → KS trigger (как сейчас)
- [ ] При resume (после ручного `/api/bot/start`) — счётчики сбрасываются
- [ ] TG-алерт при любой 403: «⚠️ 403 от Tradernet — бот поставлен на паузу»
**Файл для правки:** `/root/grid-bot/main.py` — секция API error tracking (см. grep `_api_errors`)
**Smoke test после фикса:**
```bash
# На demo: подменить API ключ на невалидный, дождаться 403 → бот должен встать на паузу
sed -i 's/TRADERNET_PUBLIC_KEY=.*/TRADERNET_PUBLIC_KEY=INVALID_KEY/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py'); cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 60
curl -s -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/status | grep bot_running
# → должно быть bot_running: false, reason: auth_403
# Восстановить ключ!
```
### Pattern 7: TG-уведомления на сделку (требует НОВОГО кода) ⭐
**Требование @KAndrew:** каждая сделка (BUY fill / SELL fill / TP-fill) → TG-сообщение с деталями.
**Сейчас:** TG вызывается только при `tg_notify()` в `main.py:384` (KS-alerts, важные изменения). Сами сделки НЕ спамят.
**Что нужно сделать:**
- [ ] Хук в `check_fill_buy` / `check_fill_sell` / `check_take_profit` после успешного fill → `tg_notify(...)` с шаблоном:
- BUY: `🟢 BUY fill @ $XXX (qty Y, balance $Z)`
- SELL: `🔴 SELL fill @ $XXX (pnl +$W)`
- TP-fill: `🎯 TP @ $XXX (pnl +$W, total trades: N)`
- [ ] Anti-spam: не слать TG если fill дубликат (idempotency по order_id)
- [ ] Quiet hours НЕ применяются (24/7 по решению @KAndrew)
- [ ] Тест: дождаться следующего BUY fill на demo → проверить TG
**Файлы для правки:** `/root/grid-bot/main.py` (buy/sell fill handlers) + `/root/grid-bot/grid.py` (TP logic)
---
## 🧪 Smoke test (перед переключением на live)
### Pre-flight (на demo)
```bash
# 1. Kill switch готов?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/kill-switch/status
# → enabled=true, triggered=false, reason=none, peak=$375.68
# 2. Тест KS в demo (НЕ в live!)
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/test
# → reason="test:..." triggered=true
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/reset
# → reason="none"
# 3. API ключи валидны?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/account
# → account_id, balance > 0, currency=USDT
```
### Live dry-run (минимальная позиция)
```bash
# 1. Переключить на live dry-run
sed -i 's/LIVE_TRADING_DRY_RUN=true/LIVE_TRADING_DRY_RUN=false/' /root/grid-bot/.env
# (НЕ ТРОГАТЬ DEMO_MODE — оставить true чтобы не запускать боевую логику)
# 2. Запустить, наблюдать 1 час
kill -9 $(pgrep -f 'python.*main.py')
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 3600
# 3. Проверить логи
grep -E 'LIVE_|PLACE|ERROR' /root/grid-bot/bot.log | tail -30
# 4. Откатить если что-то не так
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py')
```
---
## 🚀 Phased rollout plan (целевой размер $135)
### Phase 0 — Smoke (1 день)
- `DEMO_MODE=true`, `LIVE_TRADING_DRY_RUN=false` (только меняем флаг, не логику)
- Наблюдаем 1 день — бот реально стучится в Tradernet API, ордера не размещает
- Проверяем логи на 403/timeout/retry
### Phase 1 — Micro-live Week 1 (5–7 дней)
- **$40** (≈30% от $135)
- `GRID_LEVELS=3`, `GRID_STEP_PERCENT=0.005`, `GRID_TAKE_PROFIT_PERCENT=2.0`
- `LIVE_KILL_DD_PCT=5.0`, `LIVE_KILL_CLOSE_POSITION=true`
- `KRONOS_LIVE_ENABLED=false` (default, осторожно)
- 24/7 (по решению @KAndrew)
- Авто-pause при 403 (после код-фикса)
- Ежедневный отчёт в TG (PnL, win-rate, max DD, #trades) — `@Kolp` генерит сам
### Phase 2 — Half Week 2 (5–7 дней)
- **$90** (≈67% от $135)
- Если Phase 1: PnL > 0 И DD < 3% И win-rate > 50%
- Иначе: остаёмся на $40 ещё на неделю
### Phase 3 — Full Week 3-4
- **$135** (100%)
- Если Phase 2 стабильно PnL > 0 И DD < 3%
- Включить `KRONOS_LIVE_ENABLED=true` (опционально, после подтверждения Kronos-метрик в demo)
### Steady state
- 100% ($135), 24/7, full automation
- Weekly отчёт в TG (можно выключить если утомит)
> ❌ **Откатить на Phase N-1 (или в demo) если:** DD > 5%, > 3 API errors подряд, любой phantom-order баг, > 5 trades подряд в убыток, TG spam от kill-switch.
---
## ↩️ Rollback plan (если что-то пошло не так)
```bash
# 1. Остановить бота
kill -9 $(pgrep -f 'python.*main.py')
# 2. Откатить .env на demo
sed -i 's/DEMO_MODE=false/DEMO_MODE=true/' /root/grid-bot/.env
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
# 3. Вручную закрыть все висящие ордера через ЛК cifra.by → «Активные заявки»
# 4. Запустить demo снова
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
# 5. Сделать snapshot кода
cp -r /root/grid-bot /root/grid-bot.live-fail-$(date +%Y%m%d_%H%M%S)
# 6. Проверить убытки в ЛК cifra.by → История сделок
```
---
## ✅ Решения (ответы @KAndrew, 26.06 23:41 GMT+7)
| # | Вопрос | Решение |
|---|---|---|
| 1 | Размер позиции на live | **$135** (целевой финальный размер) |
| 2 | Одновременно ордеров | **3** (как сейчас в state.json) |
| 3 | Kronos live | **Вариант А — выключить навсегда** (подтверждено @KAndrew). Код оставить как legacy, `STRATEGY_BIAS_SOURCE=trend` |
| 4 | Ночной режим | **24/7** (без QUIET_HOURS) |
| 5 | Если API 403 | **авто-pause** (НЕ kill) — требует код-фикса, см. Code review §6 |
| 6 | TG-отчёты | **(a) каждое сообщение на сделку** (подтверждено @KAndrew) — BUY/SELL/TP-fill → отдельный TG. Требует код-фикса, см. Code review §7 |
> Все defaults можно переопределить — скажи явно.
---
## 📊 Текущие метрики (для сравнения с live)
| Метрика | Demo сейчас | Live минимум |
|---|---|---|
| Total trades | 13 | 30+ (закрытых) |
| Total PnL | $3.06 | > $0 за неделю |
| Peak equity | $375.68 | — |
| Max DD | −4.86% | < 5% |
| Win-rate | 1/1 = 100% (n=1) | > 50% (n≥20) |
| Avg TP-fill time | ~2ч | — |
| Avg step fill | ~30 мин | — |
| Days running | ~14 | 14+ |
| Uptime | ~95% (были рестарты) | > 98% |
---
## 🔗 Связанные файлы
- `/root/grid-bot/.env` — основные настройки
- `/root/grid-bot/config.py` — defaults (все через `os.getenv`)
- `/root/grid-bot/main.py` — trading loop + KS + state.json
- `/root/grid-bot/grid.py` — grid logic, TP, fills
- `/root/grid-bot/api.py` — Tradernet REST client
- `~/.openclaw/workspace/MEMORY.md` — патчи, баги, решения
- `~/.openclaw/workspace/memory/2026-06-*.md` — daily notes
---
**Автор:** Kolp (OpenClaw + M3)
**Версия:** 1.1 (обновлено 2026-06-26 23:41 GMT+7)
**История:**
- v1.0 (23:30) — первая версия, open questions
- v1.1 (23:41) — ответы @KAndrew: $135, 3 ордера, 24/7, 403→pause; TODO: код-фикс auto-pause
- v1.2 (23:42) — TG-отчёты на сделку (Pattern 7); Kronos пока PENDING
- v1.3 (23:45) — Kronos = Вариант А (выключить); TG-формат уточняется
- v1.4 (23:48) — все 6 open questions закрыты. Готовность к go-live = блокер #1 (fees Cifra)
- v1.5 (23:54) — **блокер #1 закрыт**: fees=0.15%, обновлён `.env` (подхватится при рестарте)
**Следующий шаг:** уточнить у @KAndrew maker/taker/round-trip + стартовать код-фиксы (Pattern 6 + Pattern 7)
+337
View File
@@ -0,0 +1,337 @@
# LIVE TRADING CHECKLIST — grid-bot BTC-USDT.IMEX
**Дата создания:** 2026-06-26 23:30 GMT+7
**Last update:** 2026-06-26 23:41 GMT+7 — ответы @KAndrew на open questions
**Бот:** grid-bot v0.13.x (PID 1552921, mode=demo, peak=$375.68)
**Цель:** выход на реальную торговлю через Cifra Markets (BY) / Tradernet
**Целевой размер позиции (от @KAndrew):** $135
> ⚠️ **Текущий статус: НЕ ГОТОВ.** Ниже — что должно быть сделано перед переключением `LIVE_TRADING_DRY_RUN=false`.
---
## 🚦 Блокеры (без них live = деньги на ветер)
### 1. Подтвердить fees Cifra Markets ✅ ЗАКРЫТ (27.06 00:03)
- [x] Зайти в ЛК **cifra.by** → раздел «Тарифы» — @KAndrew подтвердил
- [x] Тариф: **«Консультационный»** (ДЛЯ СТАРТА). Возможно есть другие tiers по объёму — уточнить при выходе на стабильный оборот
- [x] Записать **точные %** для BTC-USDT: **0.5% per side** (round-trip = **1.0%**)
- [x] Обновить `.env`: `FEE_RATE_PERCENT=0.5`, `FEE_SAFETY_MULTIPLIER=1.5` (было 0.2 / 1.25)
- [ ] **Открытый вопрос**: «Списывается только в рублях» — реальный fee может быть выше из-за скрытого RUB/USD спреда (~0.1–0.2%)
- [ ] При следующем рестарте бота подхватится автоматически
**Расчёт margin (fee=0.5% per side):**
- Round-trip fee: **1.00%**
- Step 0.5% − fee 1.00% = **−0.50%** — УБЫТОК ❌
- Validator `fee × safety=1.5`: 1.00% × 1.5 = **1.50%** → step должен быть ≥ 1.5%
- TP 2% − fees 1.00% = **1.00% net profit** на полный цикл ✅
- **Step обязательно поднимать** до 1.5–2.0%
### 2. Накопить demo-метрики
- [ ] Минимум **30 закрытых сделок** (сейчас 13) — не дни, а TP-fill-ы
- [ ] Минимум **14 дней** непрерывной работы в demo без критических багов
- [ ] **Win-rate > 50%** (TP-fill / total fills)
- [ ] **Avg PnL per trade > 0** после учёта fees
- [ ] **Max DD < 8%** за весь период (у нас сейчас −4.86% от peak)
### 3. Настроить kill switch для live
- [ ] `LIVE_KILL_CLOSE_POSITION=true` (сейчас `false` — это риск)
- [ ] `LIVE_KILL_DD_PCT=5.0` (сейчас не задан → fallback на 10%)
- [ ] Проверить TG-polling работает: `/api/kill-switch/status` → `tg_poll_sec: 30`
- [ ] Протестировать `/api/kill-switch/test` с reason=`live_test` (на demo!)
### 4. Code review (см. секцию ниже)
- [ ] Пройтись по всем fix-точкам из MEMORY.md (`*100`-баги, anchor-формулы, percent-конверсии)
- [ ] Сверить все места с `take_profit_percent` (был баг ×4 → anchor $260K)
- [ ] Проверить формулы asymmetric split (3:2 / 2:3)
### 5. Live API smoke test (см. секцию ниже)
- [ ] Тестовый BUY на минимальный lot с ручным отменом через ЛК
- [ ] Тестовый SELL (TP-fill) на минимальный lot
- [ ] Проверить, что `urllib`-запросы реально доходят до Tradernet, нет 403/timeout
---
## ⚙️ Pre-live .env настройки (что поменять)
```bash
# === ОБЯЗАТЕЛЬНО изменить перед live ===
# 1. Trading mode
DEMO_MODE=false # было: true
LIVE_TRADING_DRY_RUN=false # было: true — это переключатель на реальные ордера
# 2. Real Tradernet API credentials (НЕ дефолтные)
TRADERNET_PUBLIC_KEY=<real_key> # отозвать старые demo-ключи
TRADERNET_PRIVATE_KEY=<real_key>
TRADERNET_LOGIN=<real_login>
TRADERNET_PASSWORD=<real_pass>
# 3. Kill switch — ОБЯЗАТЕЛЬНО
LIVE_KILL_CLOSE_POSITION=true # было: false
LIVE_KILL_DD_PCT=5.0 # 5% (было: не задано → 10%)
KILL_DD_PCT=10.0 # для demo остаётся 10%
# 4. Fee rate (после подтверждения в ЛК Cifra)
FEE_RATE_PERCENT=0.2 # ОБНОВИТЬ после проверки точных %
FEE_SAFETY_MULTIPLIER=1.5 # было: 1.25 — увеличить запас
# 5. Strategy — для live понизить агрессивность
STRATEGY_LOCK_PCT=1.5 # было: 2.0 — раньше блокировать
STRATEGY_LOCK_COOLDOWN_SEC=1800 # было: 900 — реже переключаться
# 6. Inactivity rebuild — для live ВЫКЛЮЧИТЬ
INACTIVITY_RESTART_HOURS=999 # было: 6 — на live пусть работает как есть
INACTIVITY_PRICE_DRIFT_PCT=99.0 # было: 0.25 — фактически отключить
# 7. Kronos — пока НЕ включать на live
KRONOS_LIVE_ENABLED=false # оставить false, пока не подтверждена статистика
# === Что НЕ менять ===
GRID_STEP_PERCENT=0.005 # 0.5% — нормально для fee 0.2%
GRID_TAKE_PROFIT_PERCENT=2.0 # 2% TP — нормально
GRID_LEVELS=10 # дефолт (но у нас сейчас 3 в state.json)
STRATEGY_CENTER_MODE=sma # OK
```
> После правок — **обязательно** перезапустить бота с `kill -9 PID` + новый запуск через `nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown`
---
## 🔍 Code review checklist (что перепроверить в коде)
### Pattern 1: Percent vs ratio confusion (исторически баг ×4)
```bash
# Найти все места, где take_profit_percent или step_percent умножаются:
grep -n "take_profit_percent\|step_percent" /root/grid-bot/main.py /root/grid-bot/grid.py
```
**Проверить каждое вхождение:**
- Если переменная хранится в **процентах** (2.0 = 2%), формула должна быть `value / 100.0`
- Если в **долях** (0.02 = 2%), формула должна быть `value` напрямую
- В `grid.py:pnl_percent()` — уже `*100`, значит внутри ratio
### Pattern 2: Anchor calculation
```bash
grep -n "sell_anchor\|entry_price.*1\.\|anchor_for_filled" /root/grid-bot/main.py
```
- Убедиться что **anchor = entry_price × (1 + take_profit_percent / 100)**
- НЕ `entry_price * (1.0 + take_profit_percent)` (это и был баг 17.06, anchor × 4)
### Pattern 3: Live order placement
```bash
grep -n "_place_live_buy\|_place_live_sell\|_live_cancel" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- Проверить, что `LIVE_TRADING_DRY_RUN=false` реально идёт в код, не в логику демо
- Проверить обработку ошибок `403 / 429 / timeout` — бот должен ретраить ИЛИ тихо отменять (не падать)
- Проверить, что при ошибке API бот НЕ дублирует ордер
### Pattern 4: Asymmetric grid split
```bash
grep -n "STRATEGY_ASYMMETRY\|buy.*sell\|split" /root/grid-bot/main.py /root/grid-bot/grid.py
```
- bias=up → 3 BUY / 2 SELL (anchor выше)
- bias=down → 2 BUY / 3 SELL (anchor ниже)
- bias=range → 3 BUY / 2 SELL (default)
### Pattern 5: Peak equity reset
```bash
grep -n "peak_equity\|_peak_lock" /root/grid-bot/main.py
```
- При live-старте вызвать `kill_switch.reset()` — peak начнёт отсчитываться от live-баланса
- Без reset — KS может триггернуться сразу (peak=$375.68 demo, на live у нас меньше)
### Pattern 6: 403 авто-pause (требует НОВОГО кода) ⭐
**Требование @KAndrew:** при HTTP 403 от Tradernet API → авто-pause бота, НЕ kill.
**Сейчас:** KS считает все API errors одинаково (`api_errors_threshold=5` за 600 сек → kill).
**Что нужно сделать:**
- [ ] Разделить счётчики ошибок: `api_errors_403` (auth/perm) vs `api_errors_5xx` (infra)
- [ ] При `api_errors_403 >= 1` → bot_running=false (pause), KS НЕ triggered, reason=`auth_403`
- [ ] При `api_errors_5xx >= 5` за 600 сек → KS trigger (как сейчас)
- [ ] При resume (после ручного `/api/bot/start`) — счётчики сбрасываются
- [ ] TG-алерт при любой 403: «⚠️ 403 от Tradernet — бот поставлен на паузу»
**Файл для правки:** `/root/grid-bot/main.py` — секция API error tracking (см. grep `_api_errors`)
**Smoke test после фикса:**
```bash
# На demo: подменить API ключ на невалидный, дождаться 403 → бот должен встать на паузу
sed -i 's/TRADERNET_PUBLIC_KEY=.*/TRADERNET_PUBLIC_KEY=INVALID_KEY/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py'); cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 60
curl -s -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/status | grep bot_running
# → должно быть bot_running: false, reason: auth_403
# Восстановить ключ!
```
### Pattern 7: TG-уведомления на сделку (требует НОВОГО кода) ⭐
**Требование @KAndrew:** каждая сделка (BUY fill / SELL fill / TP-fill) → TG-сообщение с деталями.
**Сейчас:** TG вызывается только при `tg_notify()` в `main.py:384` (KS-alerts, важные изменения). Сами сделки НЕ спамят.
**Что нужно сделать:**
- [ ] Хук в `check_fill_buy` / `check_fill_sell` / `check_take_profit` после успешного fill → `tg_notify(...)` с шаблоном:
- BUY: `🟢 BUY fill @ $XXX (qty Y, balance $Z)`
- SELL: `🔴 SELL fill @ $XXX (pnl +$W)`
- TP-fill: `🎯 TP @ $XXX (pnl +$W, total trades: N)`
- [ ] Anti-spam: не слать TG если fill дубликат (idempotency по order_id)
- [ ] Quiet hours НЕ применяются (24/7 по решению @KAndrew)
- [ ] Тест: дождаться следующего BUY fill на demo → проверить TG
**Файлы для правки:** `/root/grid-bot/main.py` (buy/sell fill handlers) + `/root/grid-bot/grid.py` (TP logic)
---
## 🧪 Smoke test (перед переключением на live)
### Pre-flight (на demo)
```bash
# 1. Kill switch готов?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/kill-switch/status
# → enabled=true, triggered=false, reason=none, peak=$375.68
# 2. Тест KS в demo (НЕ в live!)
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/test
# → reason="test:..." triggered=true
curl -u kolpakov:GrinderBot1090 -X POST https://gridbot.kolp.pro/api/kill-switch/reset
# → reason="none"
# 3. API ключи валидны?
curl -u kolpakov:GrinderBot1090 https://gridbot.kolp.pro/api/account
# → account_id, balance > 0, currency=USDT
```
### Live dry-run (минимальная позиция)
```bash
# 1. Переключить на live dry-run
sed -i 's/LIVE_TRADING_DRY_RUN=true/LIVE_TRADING_DRY_RUN=false/' /root/grid-bot/.env
# (НЕ ТРОГАТЬ DEMO_MODE — оставить true чтобы не запускать боевую логику)
# 2. Запустить, наблюдать 1 час
kill -9 $(pgrep -f 'python.*main.py')
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
sleep 3600
# 3. Проверить логи
grep -E 'LIVE_|PLACE|ERROR' /root/grid-bot/bot.log | tail -30
# 4. Откатить если что-то не так
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
kill -9 $(pgrep -f 'python.*main.py')
```
---
## 🚀 Phased rollout plan (целевой размер $135)
### Phase 0 — Smoke (1 день)
- `DEMO_MODE=true`, `LIVE_TRADING_DRY_RUN=false` (только меняем флаг, не логику)
- Наблюдаем 1 день — бот реально стучится в Tradernet API, ордера не размещает
- Проверяем логи на 403/timeout/retry
### Phase 1 — Micro-live Week 1 (5–7 дней)
- **$40** (≈30% от $135)
- `GRID_LEVELS=3`, `GRID_STEP_PERCENT=0.005`, `GRID_TAKE_PROFIT_PERCENT=2.0`
- `LIVE_KILL_DD_PCT=5.0`, `LIVE_KILL_CLOSE_POSITION=true`
- `KRONOS_LIVE_ENABLED=false` (default, осторожно)
- 24/7 (по решению @KAndrew)
- Авто-pause при 403 (после код-фикса)
- Ежедневный отчёт в TG (PnL, win-rate, max DD, #trades) — `@Kolp` генерит сам
### Phase 2 — Half Week 2 (5–7 дней)
- **$90** (≈67% от $135)
- Если Phase 1: PnL > 0 И DD < 3% И win-rate > 50%
- Иначе: остаёмся на $40 ещё на неделю
### Phase 3 — Full Week 3-4
- **$135** (100%)
- Если Phase 2 стабильно PnL > 0 И DD < 3%
- Включить `KRONOS_LIVE_ENABLED=true` (опционально, после подтверждения Kronos-метрик в demo)
### Steady state
- 100% ($135), 24/7, full automation
- Weekly отчёт в TG (можно выключить если утомит)
> ❌ **Откатить на Phase N-1 (или в demo) если:** DD > 5%, > 3 API errors подряд, любой phantom-order баг, > 5 trades подряд в убыток, TG spam от kill-switch.
---
## ↩️ Rollback plan (если что-то пошло не так)
```bash
# 1. Остановить бота
kill -9 $(pgrep -f 'python.*main.py')
# 2. Откатить .env на demo
sed -i 's/DEMO_MODE=false/DEMO_MODE=true/' /root/grid-bot/.env
sed -i 's/LIVE_TRADING_DRY_RUN=false/LIVE_TRADING_DRY_RUN=true/' /root/grid-bot/.env
# 3. Вручную закрыть все висящие ордера через ЛК cifra.by → «Активные заявки»
# 4. Запустить demo снова
cd /root/grid-bot && nohup ./venv/bin/python main.py >> bot.log 2>&1 < /dev/null & disown
# 5. Сделать snapshot кода
cp -r /root/grid-bot /root/grid-bot.live-fail-$(date +%Y%m%d_%H%M%S)
# 6. Проверить убытки в ЛК cifra.by → История сделок
```
---
## ✅ Решения (ответы @KAndrew, 26.06 23:41 GMT+7)
| # | Вопрос | Решение |
|---|---|---|
| 1 | Размер позиции на live | **$135** (целевой финальный размер) |
| 2 | Одновременно ордеров | **3** (как сейчас в state.json) |
| 3 | Kronos live | **Вариант А — выключить навсегда** (подтверждено @KAndrew). Код оставить как legacy, `STRATEGY_BIAS_SOURCE=trend` |
| 4 | Ночной режим | **24/7** (без QUIET_HOURS) |
| 5 | Если API 403 | **авто-pause** (НЕ kill) — требует код-фикса, см. Code review §6 |
| 6 | TG-отчёты | **(a) каждое сообщение на сделку** (подтверждено @KAndrew) — BUY/SELL/TP-fill → отдельный TG. Требует код-фикса, см. Code review §7 |
> Все defaults можно переопределить — скажи явно.
---
## 📊 Текущие метрики (для сравнения с live)
| Метрика | Demo сейчас | Live минимум |
|---|---|---|
| Total trades | 13 | 30+ (закрытых) |
| Total PnL | $3.06 | > $0 за неделю |
| Peak equity | $375.68 | — |
| Max DD | −4.86% | < 5% |
| Win-rate | 1/1 = 100% (n=1) | > 50% (n≥20) |
| Avg TP-fill time | ~2ч | — |
| Avg step fill | ~30 мин | — |
| Days running | ~14 | 14+ |
| Uptime | ~95% (были рестарты) | > 98% |
---
## 🔗 Связанные файлы
- `/root/grid-bot/.env` — основные настройки
- `/root/grid-bot/config.py` — defaults (все через `os.getenv`)
- `/root/grid-bot/main.py` — trading loop + KS + state.json
- `/root/grid-bot/grid.py` — grid logic, TP, fills
- `/root/grid-bot/api.py` — Tradernet REST client
- `~/.openclaw/workspace/MEMORY.md` — патчи, баги, решения
- `~/.openclaw/workspace/memory/2026-06-*.md` — daily notes
---
**Автор:** Kolp (OpenClaw + M3)
**Версия:** 1.1 (обновлено 2026-06-26 23:41 GMT+7)
**История:**
- v1.0 (23:30) — первая версия, open questions
- v1.1 (23:41) — ответы @KAndrew: $135, 3 ордера, 24/7, 403→pause
- v1.2 (23:42) — TG-отчёты на сделку (Pattern 7)
- v1.3 (23:45) — Kronos = Вариант А (выключить)
- v1.4 (23:48) — все 6 open questions закрыты
- v1.5 (23:54) — fees=0.15% (первая оценка @KAndrew)
- v1.6 (27.06 00:03) — fees=**0.5% per side** (тариф «Консультационный», round-trip=1.0%). Требует пересмотра step
**Следующий шаг:** @KAndrew выбирает step (1.5% / 2.0%) — пока grid-стратегия убыточна на live
+254
View File
@@ -0,0 +1,254 @@
# 🚀 Локальный стенд — 3 варианта от простого к полному
Все команды выполняются из `/tmp/stand/grid-bot/` (или твоей копии пакета).
---
## A) Только дашборд (30 сек)
**Что увидишь:** UI с графиками и блоком 🧠 Kronos. Совет подгрузится из `kronos_advice.json` (статичный). Торговый цикл и Kronos-advisor **не запущены**.
```bash
cd /tmp/stand/grid-bot
# Минимальный .env для UI (без реальных ключей)
cat > .env <<'EOF'
# === Web auth ===
WEB_USERNAME=admin
WEB_PASSWORD=admin
# === Kronos — выключен, совет подгрузится из файла ===
KRONOS_ENABLED=false
KRONOS_LIVE_ENABLED=false
KRONOS_MIN_CONFIDENCE=0.4
# === Grid — DEMO режим ===
DEMO_MODE=true
GRID_LEVELS=10
GRID_STEP_PERCENT=0.005
# === API ключи пустые (DEMO) ===
TRADERNET_PUBLIC_KEY=
TRADERNET_SECRET_KEY=
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
EOF
# Используем существующий venv
export PYTHONPATH=/tmp/stand/Kronos-src:$PYTHONPATH
# Запуск
python3 main.py
```
Открой: **http://127.0.0.1:5555/**
Логин: `admin` / `admin`
**Что работает:**
- ✅ Дашборд с графиками (статичные)
- ✅ Блок 🧠 Kronos показывает последний совет из `kronos_advice.json`
- ✅ Trade log (пустой)
- ✅ Настройки
- ❌ Торговый цикл **НЕ запущен** (Kronos отключён)
- ❌ Нет автообновления советов
---
## B) Дашборд + живой Kronos advisor (5 мин)
**Что увидишь:** то же + Kronos генерирует советы в фоне, можно жать «⟳ Обновить совет» в дашборде.
```bash
cd /tmp/stand/grid-bot
# В .env (см. выше) поменяй:
# KRONOS_ENABLED=true
# KRONOS_LIVE_ENABLED=false # ← бот НЕ применяет советы, только показывает
# Запусти advisor в отдельном терминале:
export PYTHONPATH=/tmp/stand/Kronos-src:$PYTHONPATH
kronos-venv/bin/python kronos_advisor_main.py --source binance --symbol BTC-USDT.IMEX --tf 60 --lookback 400 --pred-len 24 --model kronos-mini
# В другом терминале — дашборд:
export PYTHONPATH=/tmp/stand/Kronos-src:$PYTHONPATH
python3 main.py
```
Или жми кнопку **⟳ Обновить совет** в дашборде — она сама запустит advisor.
**Что работает:**
- ✅ Всё из (A)
- ✅ Совет обновляется по кнопке или вручную
- ✅ Можно видеть, как Kronos реагирует на текущий рынок
- ❌ Торговый цикл **НЕ запущен**
---
## C) Полный стенд — grid-bot в DEMO + Kronos (10 мин)
**Что увидишь:** grid-bot в DEMO-режиме (виртуальные ордера) + Kronos-советник + всё обновляется автоматически каждые 15 мин (через timer) или по кнопке.
```bash
cd /tmp/stand/grid-bot
# В .env:
cat > .env <<'EOF'
# === Web auth ===
WEB_USERNAME=admin
WEB_PASSWORD=admin
# === Kronos — включён, но НЕ применяет в LIVE ===
KRONOS_ENABLED=true
KRONOS_LIVE_ENABLED=true
KRONOS_MODEL=kronos-mini
KRONOS_TF_MIN=60
KRONOS_LOOKBACK=400
KRONOS_PRED_LEN=24
KRONOS_MIN_CONFIDENCE=0.4
KRONOS_ADVICE_FILE=/tmp/stand/grid-bot/kronos_advice.json
# === Grid — DEMO ===
DEMO_MODE=true
GRID_LEVELS=10
GRID_STEP_PERCENT=0.005
GRID_TAKE_PROFIT_PERCENT=0.5
# === API ключи пустые (DEMO) ===
TRADERNET_PUBLIC_KEY=
TRADERNET_SECRET_KEY=
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
EOF
# Запустить grid-bot (он сам поднимет Flask + trading loop + cron advisor)
export PYTHONPATH=/tmp/stand/Kronos-src:$PYTHONPATH
python3 main.py
```
**Что работает:**
- ✅ Всё из (B)
- ✅ Торговый цикл крутится в DEMO (виртуальные ордера, BTC-USDT.IMEX по симулированной цене)
- ✅ Kronos реально влияет на шаг/центр/паузу grid-а
- ✅ Telegram-уведомления о смене совета (если `TELEGRAM_BOT_TOKEN` задан)
- ✅ Автообновление совета каждые ~10 мин (через Flask-внутренний cron в `trading_loop`)
---
## D) С systemd timer (15 мин, «как в проде»)
**Что увидишь:** полный прод-сценарий. Timer запускает `kronos_advisor_main.py` каждые 15 мин, бот сам читает свежий совет.
```bash
cd /tmp/stand/grid-bot
# 1) Подправить пути в unit (по умолчанию они /root/grid-bot)
sed -i 's|/root/grid-bot|/tmp/stand/grid-bot|g; s|User=root|User=andr|g; s|/root/.openclaw/kronos_advice.json|/tmp/stand/grid-bot/kronos_advice.json|g' kronos-advisor.service
# 2) Установить (без sudo — пользовательский systemd)
mkdir -p ~/.config/systemd/user
cp kronos-advisor.{service,timer} ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now kronos-advisor.timer
systemctl --user list-timers kronos-advisor.timer
# 3) Запустить grid-bot
export PYTHONPATH=/tmp/stand/Kronos-src:$PYTHONPATH
python3 main.py
```
**Что работает:**
- ✅ Полный прод-сценарий локально
- ✅ Timer запускает advisor как отдельный процесс
- ✅ Можно смотреть `journalctl --user -u kronos-advisor`
---
## 🔍 Что смотреть в браузере
Открой **http://127.0.0.1:5555/**, логин `admin`/`admin`.
| Раздел | Что там |
|---|---|
| Графики | Цена + Grid (buy/sell уровни) |
| Настройки | GRID_LEVELS, GRID_STEP_PERCENT, GRID_TAKE_PROFIT_PERCENT |
| **🧠 Kronos Advisor** | **Совет от Kronos: step/center/range/pause, источник, время генерации, кнопка ⟳ Обновить совет** |
| Trade log | Последние сделки (виртуальные в DEMO) |
| Status bar | Текущая цена, время, режим |
---
## 🛑 Остановка
```bash
# В терминале, где крутится бот: Ctrl+C
# Если запускал timer:
systemctl --user disable --now kronos-advisor.timer
# Если остались процессы:
pkill -f "python3 main.py"
pkill -f "kronos_advisor_main"
```
---
## 🐛 Если что-то не работает
### "Address already in use" (порт 5555 занят)
```bash
lsof -i:5555
# Убей старый процесс или поменяй порт в main.py
```
### "ModuleNotFoundError: No module named 'kronos'"
```bash
# Запусти из корня проекта, не из подпапки:
cd /tmp/stand/grid-bot
python3 main.py
```
### "No module named 'model'" (Kronos не найден)
```bash
export PYTHONPATH=/tmp/stand/Kronos-src:$PYTHONPATH
```
### Kronos падает с OOM
```bash
# Модель не влезла в RAM. Закрой Chrome, IDE и попробуй снова.
free -h # должно быть >2GB free
```
### Бот не видит Kronos
```bash
# Проверь .env:
grep -E "KRONOS_(ENABLED|LIVE_ENABLED|MIN_CONFIDENCE)" .env
# И лог:
python3 main.py 2>&1 | grep -i kronos
```
---
## 📊 Какие API можно дёргать руками
```bash
# Текущий совет
curl -u admin:admin http://127.0.0.1:5555/api/kronos
# Запустить advisor
curl -u admin:admin -X POST http://127.0.0.1:5555/api/kronos/refresh
# Статус advisor
curl -u admin:admin http://127.0.0.1:5555/api/kronos/refresh/status
# Состояние grid
curl -u admin:admin http://127.0.0.1:5555/api/state
```
---
## 🎯 Что я рекомендую для первого раза
1. Начни с **(A)** — посмотри UI, заглушку Kronos
2. Потом **(B)** — нажми «⟳ Обновить совет» несколько раз, посмотри как меняется
3. Потом **(C)** — увидь, как Kronos влияет на grid в DEMO
4. Если всё ОК → деплой на сервер по `DEPLOY.md`
+109
View File
@@ -0,0 +1,109 @@
# Grid Trading Bot — Tradernet / Cifra.by
Grid-торговый робот для криптобиржи через платформу Cifra.by (Tradernet API).
## Установка
```bash
cd /root/grid-bot
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
## Настройка
1. Скопировать `.env.example` → `.env`
2. Заполнить ключи API:
```bash
TRADERNET_PUBLIC_KEY=ваш_публичный_ключ
TRADERNET_PRIVATE_KEY=ваш_приватный_ключ
```
**Как получить ключи:**
1. Зарегистрироваться на [cifra.by](https://cifra.by)
2. Открыть счёт → перенаправляет на [tradernet.by](https://tradernet.by)
3. Настройки → API-ключи → создать пару
## Запуск
```bash
# Dry-run (без реальных ордеров)
python main.py --dry-run
# Один цикл (для тестирования)
python main.py --once
# Production
python main.py
```
## Systemd (автозапуск)
```bash
cp grid-bot.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable grid-bot
systemctl start grid-bot
systemctl status grid-bot
```
## Структура файлов
```
grid-bot/
├── config.py # Параметры торговли
├── api.py # Обёртка Tradernet API
├── grid.py # Логика сетки
├── main.py # Точка входа
├── requirements.txt
├── .env.example # Шаблон переменных окружения
└── grid-bot.service # systemd unit
```
## Параметры (config.py)
| Параметр | По умолчанию | Описание |
|---|---|---|
| `SYMBOL` | BTC | Торгуемая пара |
| `GRID_LEVELS` | 10 | Количество уровней сетки |
| `GRID_STEP_PERCENT` | 0.3 | Шаг сетки (% от цены) |
| `ORDER_AMOUNT_USD` | 100 | Объём каждого ордера (USD) |
| `MAX_POSITION` | 1000 | Макс. позиция в USD |
| `UPDATE_INTERVAL_SECONDS` | 30 | Частота цикла |
| `TRADING_ENABLED` | True | False = dry-run |
## Как работает grid
```
Цена BTC = $67,500
Сетка (шаг 0.3% = $202.50):
BUY @ $66,600 ← уровень -4
BUY @ $66,802 ← уровень -3
BUY @ $67,005 ← уровень -2
BUY @ $67,208 ← уровень -1
───────────────────────── center (не выставляем)
SELL @ $67,612 ← уровень +1
SELL @ $67,815 ← уровень +2
SELL @ $68,018 ← уровень +3
SELL @ $68,220 ← уровень +4
BUY исполняется → выставляем SELL на уровень выше
SELL исполняется → выставляем BUY на уровень ниже
Каждый round-trip = profit = шаг_сетки × количество
```
## Telegram-уведомления
```bash
TELEGRAM_BOT_TOKEN=... # через @BotFather
TELEGRAM_CHAT_ID=... # через @userinfobot
```
## Важно
⚠️ **Всегда тестируйте в dry-run режиме перед реальной торговлей.**
⚠️ **Инвестируйте только те средства, которые готовы потерять.**
+333
View File
@@ -0,0 +1,333 @@
"""
Grid Bot — Async API wrapper for Tradernet (market data + trading).
Uses format: POST https://tradernet.by/api/{cmd} with HMAC-SHA256.
"""
import logging
import time
import json
import hmac
import hashlib
import asyncio
import urllib.request
import urllib.error
from typing import Optional
try:
import aiohttp
except ImportError:
raise ImportError("pip install aiohttp")
logger = logging.getLogger(__name__)
DOMAIN = "api.tadernet.by"
BASE_URL = f"https://{DOMAIN}"
class TradernetAPI:
def __init__(self, public_key: str, private_key: str, login: str, password: str,
base_url: str = BASE_URL):
self.public_key = public_key
self.private_key = private_key
self.login = login
self.password = password
self.base_url = base_url.rstrip("/")
self._session: Optional[aiohttp.ClientSession] = None
async def _get_session(self) -> aiohttp.ClientSession:
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
def _sign(self, payload: str) -> tuple[str, str]:
ts = str(int(time.time()))
msg = payload + ts
sig = hmac.new(
self.private_key.encode(),
msg.encode(),
hashlib.sha256
).hexdigest()
return ts, sig
async def _post(self, cmd: str, params: Optional[dict] = None) -> dict:
params = params or {}
payload = json.dumps(params, separators=(',', ':'))
ts, sig = self._sign(payload)
headers = {
"Content-Type": "application/json",
"X-NtApi-PublicKey": self.public_key,
"X-NtApi-Timestamp": ts,
"X-NtApi-Sig": sig,
}
url = f"{self.base_url}/api/{cmd}"
session = await self._get_session()
try:
async with session.post(
url, data=payload, headers=headers,
timeout=aiohttp.ClientTimeout(total=30)
) as resp:
data = await resp.json()
logger.debug(f"REST {cmd} → {resp.status}: {str(data)[:100]}")
if resp.status == 403:
logger.warning(f"[REST 403] {cmd} → {data}")
return {}
return data or {}
except asyncio.TimeoutError:
logger.warning(f"REST {cmd} timed out")
return {}
except Exception as e:
logger.warning(f"REST {cmd} failed: {e}")
return {}
async def close(self):
if self._session and not self._session.closed:
await self._session.close()
# ─── Market data ──────────────────────────────────────────────────────────
async def get_quotes(self, tickers: list[str]) -> dict:
return await self._post("getStockQuotesJson", {"tickers": ",".join(tickers)})
async def get_quote(self, ticker: str) -> dict:
return await self._post("getStockQuotesJson", {"tickers": ticker})
async def get_market_status(self) -> dict:
return await self._post("getMarketStatus", {})
async def get_orderbook(self, ticker: str, depth: int = 20) -> dict:
"""Get order book (market depth) for a ticker."""
return await self._post("getOrderBook", {"ticker": ticker, "depth": depth})
# ─── Portfolio ────────────────────────────────────────────────────────────
async def account_get_summary(self) -> dict:
"""DEPRECATED: Tradernet returns 'Command not found' for this. Use get_position_json()."""
return await self._post("accountGetSummary", {})
async def get_opq(self) -> dict:
"""Initial user data (account info, portfolio summary, main currency)."""
return await self._post("getOPQ", {})
async def get_position_json(self) -> dict:
"""Current account balances + open positions.
Response shape:
{
"result": {
"ps": {
"acc": [{"curr": "USD", "currval": 71.02, "s": 0, ...}, ...],
"pos": [...], # open positions
"key": "user@email"
}
}
}
"""
return await self._post("getPositionJson", {})
def get_position_json_sync(self, timeout: float = 15.0) -> dict:
"""Synchronous version of get_position_json. Safe to call from any context (e.g. Flask handler)."""
return self._post_sync("getPositionJson", {}, timeout)
def get_opq_sync(self, timeout: float = 10.0) -> dict:
"""Synchronous getOPQ: account info, main currency, brief info.
Response shape (top level):
{"OPQ": {"rev": int, "init_margin": int, "brief_nm": str, "main_curr": "USDT",
"active": int, "reception": int, "f_kval": "0", "quotes": {"q": [...]}}}
"""
return self._post_sync("getOPQ", {}, timeout)
def get_security_info_sync(self, ticker: str, timeout: float = 10.0) -> dict:
"""Synchronous getSecurityInfo: lot size, min step, currency, market.
Response:
{"id": "BTC-USDT.IMEX", "short_name": "BTC-USDT", "default_ticker": "BTC-IMEX",
"code_nm": "BTC-IMEX", "currency": "USDT", "min_step": "0.10000000",
"lot": "0.00001000", "mkt_name": "IMEX", "firstDate": "01.05.1970",
"mrkt": {"tz": "Europe/Minsk", "o": "05:00:00", "c": "02:00:00"}}
"""
return self._post_sync("getSecurityInfo", {"ticker": ticker, "sup": True}, timeout)
def get_hloc_sync(self, ticker: str, timeframe_min: int = 60,
date_from: str = "", date_to: str = "",
count: int = 0, timeout: float = 15.0) -> dict:
"""Synchronous getHloc: historical OHLCV candlesticks.
Args:
ticker: e.g. "BTC-USDT.IMEX"
timeframe_min: candle interval in minutes (1, 5, 15, 60, 1440)
date_from: "DD.MM.YYYY hh:mm" or empty
date_to: "DD.MM.YYYY hh:mm" or empty
count: extra candles beyond the date range (0 = none)
Response:
{"hloc": {"BTC-USDT.IMEX": [[open, high, low, close], ...]},
"vl": {"BTC-USDT.IMEX": [vol, vol, ...]},
"xSeries": {"BTC-USDT.IMEX": [unix_ts, ...]},
"info": {"BTC-USDT.IMEX": {...security info...}},
"maxSeries": {"BTC-USDT.IMEX": ts}}
"""
params = {
"id": ticker,
"timeframe": timeframe_min,
"date_from": date_from,
"date_to": date_to,
"count": count,
"intervalMode": "ClosedRay",
}
return self._post_sync("getHloc", params, timeout)
def _post_sync(self, command: str, params: dict, timeout: float = 10.0) -> dict:
"""Generic synchronous POST to Tradernet REST API. Returns parsed JSON or {} on error.
Used for: getOPQ, getPositionJson, getSecurityInfo, getHloc.
"""
payload = json.dumps(params, separators=(',', ':'))
ts, sig = self._sign(payload)
headers = {
"Content-Type": "application/json",
"X-NtApi-PublicKey": self.public_key,
"X-NtApi-Timestamp": ts,
"X-NtApi-Sig": sig,
}
url = f"{self.base_url}/api/{command}"
req = urllib.request.Request(url, data=payload.encode(), headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode())
if resp.status == 200:
return data
logger.warning(f"REST sync {command} status={resp.status}: {str(data)[:100]}")
return {}
except urllib.error.HTTPError as e:
logger.warning(f"REST sync {command} HTTPError {e.code}: {e.reason}")
return {}
except urllib.error.URLError as e:
logger.warning(f"REST sync {command} URLError: {e.reason}")
return {}
except Exception as e:
logger.warning(f"REST sync {command} error: {e}")
return {}
async def portfolio_get_changes(self) -> dict:
return await self._post("portfolio-get-changes", {})
# ─── Orders ───────────────────────────────────────────────────────────────
async def orders_get_current_history(self) -> dict:
return await self._post("getNotifyOrderJson", {"active_only": 1})
async def orders_get_history(self, date_from: str = "", date_to: str = "") -> dict:
params = {}
if date_from:
params["from"] = date_from
if date_to:
params["till"] = date_to
return await self._post("getOrdersHistory", params)
async def put_order(self, ticker: str, price: float, qty: float,
action: str = "BUY", order_type: str = "LIMIT",
client_id: str = "") -> dict:
params = {
"instr_name": ticker,
"price": str(price),
"qty": str(qty if qty < 1 else int(qty)),
"action_id": "2" if action.upper() == "BUY" else "4",
"order_type_id": "2" if order_type == "LIMIT" else "1",
"expiration_id": "1",
}
if client_id:
params["user_order_id"] = str(client_id)
return await self._post("putTradeOrder", params)
async def cancel_order(self, order_id: str) -> dict:
return await self._post("delTradeOrder", {"order_id": int(order_id)})
async def cancel_all_orders(self) -> dict:
return await self._post("delAllTradeOrders", {})
# ─── Sync versions for live trading (called from async trading_loop) ──────
# These are needed because the API's aiohttp session gets bound to the loop
# it was created in, but our trading_loop runs in its own loop. Calling
# aiohttp from a different loop fails. urllib is safe in any context.
def put_order_sync(self, ticker: str, price: float, qty: float,
action: str = "BUY", order_type: str = "LIMIT",
client_id: str = "", timeout: float = 15.0) -> dict:
"""Place a LIMIT order. Returns parsed JSON or {} on error.
Response shape on success:
{"order_id": <int>, "order": {<order fields>}, "warning": ""}
On rejection:
{"error": "<reason>", "code": 0}
"""
params = {
"instr_name": ticker,
"price": str(price),
"qty": str(qty if qty < 1 else int(qty)), # BTC float for sub-unit
"action_id": "2" if action.upper() == "BUY" else "4", # 2=BUY, 4=SELL
"order_type_id": "2" if order_type == "LIMIT" else "1", # 2=LIMIT, 1=MARKET
"expiration_id": "1", # DAY
}
if client_id:
params["user_order_id"] = str(client_id)
return self._post_sync("putTradeOrder", params, timeout)
def cancel_order_sync(self, order_id: int, timeout: float = 10.0) -> dict:
"""Cancel an active order by its ID. Returns parsed JSON or {} on error."""
return self._post_sync("delTradeOrder", {"order_id": int(order_id)}, timeout)
def cancel_all_orders_sync(self, timeout: float = 15.0) -> dict:
"""Cancel all active orders. Returns parsed JSON or {} on error."""
return self._post_sync("delAllTradeOrders", {}, timeout)
def get_active_orders_sync(self, timeout: float = 10.0) -> list:
"""Get list of currently active orders for this account.
Response:
{"result": {"orders": {"key": "...", <order_id>: {<order fields>}, ...}}}
Returns a list of order dicts (extracted from the key'd map), empty list on error.
"""
data = self._post_sync("getNotifyOrderJson", {"active_only": 1}, timeout)
out = []
if not data:
return out
orders_map = data.get("result", {}).get("orders", {})
# The order map has a "key" field plus order_id keys
for k, v in orders_map.items():
if k == "key":
continue
if isinstance(v, dict):
out.append(v)
return out
def get_orders_history_sync(self, date_from: str = "", date_to: str = "",
timeout: float = 15.0) -> list:
"""Get list of orders for the given date range.
Args:
date_from: "YYYY-MM-DD" or empty (no lower bound)
date_to: "YYYY-MM-DD" or empty (no upper bound)
Response:
{"orders": {"order": [<order dict>, ...]}}
"""
params = {}
if date_from:
params["from"] = date_from
if date_to:
params["till"] = date_to
data = self._post_sync("getOrdersHistory", params, timeout)
if not data:
return []
orders_obj = data.get("orders", {})
order_list = orders_obj.get("order", [])
if isinstance(order_list, dict):
# API can return single order as dict too — wrap in list
order_list = [order_list]
return order_list or []
+332
View File
@@ -0,0 +1,332 @@
"""
Grid Bot — Async API wrapper for Tradernet (market data + trading).
Uses format: POST https://tradernet.by/api/{cmd} with HMAC-SHA256.
"""
import logging
import time
import json
import hmac
import hashlib
import asyncio
import urllib.request
import urllib.error
from typing import Optional
try:
import aiohttp
except ImportError:
raise ImportError("pip install aiohttp")
logger = logging.getLogger(__name__)
DOMAIN = "api.tadernet.by"
BASE_URL = f"https://{DOMAIN}"
class TradernetAPI:
def __init__(self, public_key: str, private_key: str, login: str, password: str,
base_url: str = BASE_URL):
self.public_key = public_key
self.private_key = private_key
self.login = login
self.password = password
self.base_url = base_url.rstrip("/")
self._session: Optional[aiohttp.ClientSession] = None
async def _get_session(self) -> aiohttp.ClientSession:
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
def _sign(self, payload: str) -> tuple[str, str]:
ts = str(int(time.time()))
msg = payload + ts
sig = hmac.new(
self.private_key.encode(),
msg.encode(),
hashlib.sha256
).hexdigest()
return ts, sig
async def _post(self, cmd: str, params: Optional[dict] = None) -> dict:
params = params or {}
payload = json.dumps(params, separators=(',', ':'))
ts, sig = self._sign(payload)
headers = {
"Content-Type": "application/json",
"X-NtApi-PublicKey": self.public_key,
"X-NtApi-Timestamp": ts,
"X-NtApi-Sig": sig,
}
url = f"{self.base_url}/api/{cmd}"
session = await self._get_session()
try:
async with session.post(
url, data=payload, headers=headers,
timeout=aiohttp.ClientTimeout(total=30)
) as resp:
data = await resp.json()
logger.debug(f"REST {cmd} → {resp.status}: {str(data)[:100]}")
if resp.status == 403:
logger.warning(f"[REST 403] {cmd} → {data}")
return {}
return data or {}
except asyncio.TimeoutError:
logger.warning(f"REST {cmd} timed out")
return {}
except Exception as e:
logger.warning(f"REST {cmd} failed: {e}")
return {}
async def close(self):
if self._session and not self._session.closed:
await self._session.close()
# ─── Market data ──────────────────────────────────────────────────────────
async def get_quotes(self, tickers: list[str]) -> dict:
return await self._post("getStockQuotesJson", {"tickers": ",".join(tickers)})
async def get_quote(self, ticker: str) -> dict:
return await self._post("getStockQuotesJson", {"tickers": ticker})
async def get_market_status(self) -> dict:
return await self._post("getMarketStatus", {})
async def get_orderbook(self, ticker: str, depth: int = 20) -> dict:
"""Get order book (market depth) for a ticker."""
return await self._post("getOrderBook", {"ticker": ticker, "depth": depth})
# ─── Portfolio ────────────────────────────────────────────────────────────
async def account_get_summary(self) -> dict:
"""DEPRECATED: Tradernet returns 'Command not found' for this. Use get_position_json()."""
return await self._post("accountGetSummary", {})
async def get_opq(self) -> dict:
"""Initial user data (account info, portfolio summary, main currency)."""
return await self._post("getOPQ", {})
async def get_position_json(self) -> dict:
"""Current account balances + open positions.
Response shape:
{
"result": {
"ps": {
"acc": [{"curr": "USD", "currval": 71.02, "s": 0, ...}, ...],
"pos": [...], # open positions
"key": "user@email"
}
}
}
"""
return await self._post("getPositionJson", {})
def get_position_json_sync(self, timeout: float = 15.0) -> dict:
"""Synchronous version of get_position_json. Safe to call from any context (e.g. Flask handler)."""
return self._post_sync("getPositionJson", {}, timeout)
def get_opq_sync(self, timeout: float = 10.0) -> dict:
"""Synchronous getOPQ: account info, main currency, brief info.
Response shape (top level):
{"OPQ": {"rev": int, "init_margin": int, "brief_nm": str, "main_curr": "USDT",
"active": int, "reception": int, "f_kval": "0", "quotes": {"q": [...]}}}
"""
return self._post_sync("getOPQ", {}, timeout)
def get_security_info_sync(self, ticker: str, timeout: float = 10.0) -> dict:
"""Synchronous getSecurityInfo: lot size, min step, currency, market.
Response:
{"id": "BTC-USDT.IMEX", "short_name": "BTC-USDT", "default_ticker": "BTC-IMEX",
"code_nm": "BTC-IMEX", "currency": "USDT", "min_step": "0.10000000",
"lot": "0.00001000", "mkt_name": "IMEX", "firstDate": "01.05.1970",
"mrkt": {"tz": "Europe/Minsk", "o": "05:00:00", "c": "02:00:00"}}
"""
return self._post_sync("getSecurityInfo", {"ticker": ticker, "sup": True}, timeout)
def get_hloc_sync(self, ticker: str, timeframe_min: int = 60,
date_from: str = "", date_to: str = "",
count: int = 0, timeout: float = 15.0) -> dict:
"""Synchronous getHloc: historical OHLCV candlesticks.
Args:
ticker: e.g. "BTC-USDT.IMEX"
timeframe_min: candle interval in minutes (1, 5, 15, 60, 1440)
date_from: "DD.MM.YYYY hh:mm" or empty
date_to: "DD.MM.YYYY hh:mm" or empty
count: extra candles beyond the date range (0 = none)
Response:
{"hloc": {"BTC-USDT.IMEX": [[open, high, low, close], ...]},
"vl": {"BTC-USDT.IMEX": [vol, vol, ...]},
"xSeries": {"BTC-USDT.IMEX": [unix_ts, ...]},
"info": {"BTC-USDT.IMEX": {...security info...}},
"maxSeries": {"BTC-USDT.IMEX": ts}}
"""
params = {
"id": ticker,
"timeframe": timeframe_min,
"date_from": date_from,
"date_to": date_to,
"count": count,
"intervalMode": "ClosedRay",
}
return self._post_sync("getHloc", params, timeout)
def _post_sync(self, command: str, params: dict, timeout: float = 10.0) -> dict:
"""Generic synchronous POST to Tradernet REST API. Returns parsed JSON or {} on error.
Used for: getOPQ, getPositionJson, getSecurityInfo, getHloc.
"""
payload = json.dumps(params, separators=(',', ':'))
ts, sig = self._sign(payload)
headers = {
"Content-Type": "application/json",
"X-NtApi-PublicKey": self.public_key,
"X-NtApi-Timestamp": ts,
"X-NtApi-Sig": sig,
}
url = f"{self.base_url}/api/{command}"
req = urllib.request.Request(url, data=payload.encode(), headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode())
if resp.status == 200:
return data
logger.warning(f"REST sync {command} status={resp.status}: {str(data)[:100]}")
return {}
except urllib.error.HTTPError as e:
logger.warning(f"REST sync {command} HTTPError {e.code}: {e.reason}")
return {}
except urllib.error.URLError as e:
logger.warning(f"REST sync {command} URLError: {e.reason}")
return {}
except Exception as e:
logger.warning(f"REST sync {command} error: {e}")
return {}
async def portfolio_get_changes(self) -> dict:
return await self._post("portfolio-get-changes", {})
# ─── Orders ───────────────────────────────────────────────────────────────
async def orders_get_current_history(self) -> dict:
return await self._post("getNotifyOrderJson", {"active_only": 1})
async def orders_get_history(self, date_from: str = "", date_to: str = "") -> dict:
params = {}
if date_from:
params["from"] = date_from
if date_to:
params["till"] = date_to
return await self._post("getOrdersHistory", params)
async def put_order(self, ticker: str, price: float, qty: float,
action: str = "BUY", order_type: str = "LIMIT",
client_id: str = "") -> dict:
params = {
"instr_name": ticker,
"price": str(price),
"qty": str(int(qty)),
"action_id": "2" if action.upper() == "BUY" else "4",
"order_type_id": "2" if order_type == "LIMIT" else "1",
"expiration_id": "1",
}
if client_id:
params["user_order_id"] = str(client_id)
return await self._post("putTradeOrder", params)
async def cancel_order(self, order_id: str) -> dict:
return await self._post("delTradeOrder", {"order_id": int(order_id)})
async def cancel_all_orders(self) -> dict:
return await self._post("delAllTradeOrders", {})
# ─── Sync versions for live trading (called from async trading_loop) ──────
# These are needed because the API's aiohttp session gets bound to the loop
# it was created in, but our trading_loop runs in its own loop. Calling
# aiohttp from a different loop fails. urllib is safe in any context.
def put_order_sync(self, ticker: str, price: float, qty: float,
action: str = "BUY", order_type: str = "LIMIT",
client_id: str = "", timeout: float = 15.0) -> dict:
"""Place a LIMIT order. Returns parsed JSON or {} on error.
Response shape on success:
{"order_id": <int>, "order": {<order fields>}, "warning": ""}
On rejection:
{"error": "<reason>", "code": 0}
"""
params = {
"instr_name": ticker,
"price": str(price),
"qty": str(int(qty)),
"action_id": "2" if action.upper() == "BUY" else "4", # 2=BUY, 4=SELL
"order_type_id": "2" if order_type == "LIMIT" else "1", # 2=LIMIT, 1=MARKET
"expiration_id": "1", # DAY
}
if client_id:
params["user_order_id"] = str(client_id)
return self._post_sync("putTradeOrder", params, timeout)
def cancel_order_sync(self, order_id: int, timeout: float = 10.0) -> dict:
"""Cancel an active order by its ID. Returns parsed JSON or {} on error."""
return self._post_sync("delTradeOrder", {"order_id": int(order_id)}, timeout)
def cancel_all_orders_sync(self, timeout: float = 15.0) -> dict:
"""Cancel all active orders. Returns parsed JSON or {} on error."""
return self._post_sync("delAllTradeOrders", {}, timeout)
def get_active_orders_sync(self, timeout: float = 10.0) -> list:
"""Get list of currently active orders for this account.
Response:
{"result": {"orders": {"key": "...", <order_id>: {<order fields>}, ...}}}
Returns a list of order dicts (extracted from the key'd map), empty list on error.
"""
data = self._post_sync("getNotifyOrderJson", {"active_only": 1}, timeout)
out = []
if not data:
return out
orders_map = data.get("result", {}).get("orders", {})
# The order map has a "key" field plus order_id keys
for k, v in orders_map.items():
if k == "key":
continue
if isinstance(v, dict):
out.append(v)
return out
def get_orders_history_sync(self, date_from: str = "", date_to: str = "",
timeout: float = 15.0) -> list:
"""Get list of orders for the given date range.
Args:
date_from: "YYYY-MM-DD" or empty (no lower bound)
date_to: "YYYY-MM-DD" or empty (no upper bound)
Response:
{"orders": {"order": [<order dict>, ...]}}
"""
params = {}
if date_from:
params["from"] = date_from
if date_to:
params["till"] = date_to
data = self._post_sync("getOrdersHistory", params, timeout)
if not data:
return []
orders_obj = data.get("orders", {})
order_list = orders_obj.get("order", [])
if isinstance(order_list, dict):
# API can return single order as dict too — wrap in list
order_list = [order_list]
return order_list or []
+332
View File
@@ -0,0 +1,332 @@
"""
Margin Grid Bot — Async API wrapper for Tradernet (market data + trading).
Uses format: POST https://tradernet.by/api/{cmd} with HMAC-SHA256.
"""
import logging
import time
import json
import hmac
import hashlib
import asyncio
import urllib.request
import urllib.error
from typing import Optional
try:
import aiohttp
except ImportError:
raise ImportError("pip install aiohttp")
logger = logging.getLogger(__name__)
DOMAIN = "api.tadernet.by"
BASE_URL = f"https://{DOMAIN}"
class TradernetAPI:
def __init__(self, public_key: str, private_key: str, login: str, password: str,
base_url: str = BASE_URL):
self.public_key = public_key
self.private_key = private_key
self.login = login
self.password = password
self.base_url = base_url.rstrip("/")
self._session: Optional[aiohttp.ClientSession] = None
async def _get_session(self) -> aiohttp.ClientSession:
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
def _sign(self, payload: str) -> tuple[str, str]:
ts = str(int(time.time()))
msg = payload + ts
sig = hmac.new(
self.private_key.encode(),
msg.encode(),
hashlib.sha256
).hexdigest()
return ts, sig
async def _post(self, cmd: str, params: Optional[dict] = None) -> dict:
params = params or {}
payload = json.dumps(params, separators=(',', ':'))
ts, sig = self._sign(payload)
headers = {
"Content-Type": "application/json",
"X-NtApi-PublicKey": self.public_key,
"X-NtApi-Timestamp": ts,
"X-NtApi-Sig": sig,
}
url = f"{self.base_url}/api/{cmd}"
session = await self._get_session()
try:
async with session.post(
url, data=payload, headers=headers,
timeout=aiohttp.ClientTimeout(total=30)
) as resp:
data = await resp.json()
logger.debug(f"REST {cmd} → {resp.status}: {str(data)[:100]}")
if resp.status == 403:
logger.warning(f"[REST 403] {cmd} → {data}")
return {}
return data or {}
except asyncio.TimeoutError:
logger.warning(f"REST {cmd} timed out")
return {}
except Exception as e:
logger.warning(f"REST {cmd} failed: {e}")
return {}
async def close(self):
if self._session and not self._session.closed:
await self._session.close()
# ─── Market data ──────────────────────────────────────────────────────────
async def get_quotes(self, tickers: list[str]) -> dict:
return await self._post("getStockQuotesJson", {"tickers": ",".join(tickers)})
async def get_quote(self, ticker: str) -> dict:
return await self._post("getStockQuotesJson", {"tickers": ticker})
async def get_market_status(self) -> dict:
return await self._post("getMarketStatus", {})
async def get_orderbook(self, ticker: str, depth: int = 20) -> dict:
"""Get order book (market depth) for a ticker."""
return await self._post("getOrderBook", {"ticker": ticker, "depth": depth})
# ─── Portfolio ────────────────────────────────────────────────────────────
async def account_get_summary(self) -> dict:
"""DEPRECATED: Tradernet returns 'Command not found' for this. Use get_position_json()."""
return await self._post("accountGetSummary", {})
async def get_opq(self) -> dict:
"""Initial user data (account info, portfolio summary, main currency)."""
return await self._post("getOPQ", {})
async def get_position_json(self) -> dict:
"""Current account balances + open positions.
Response shape:
{
"result": {
"ps": {
"acc": [{"curr": "USD", "currval": 71.02, "s": 0, ...}, ...],
"pos": [...], # open positions
"key": "user@email"
}
}
}
"""
return await self._post("getPositionJson", {})
def get_position_json_sync(self, timeout: float = 15.0) -> dict:
"""Synchronous version of get_position_json. Safe to call from any context (e.g. Flask handler)."""
return self._post_sync("getPositionJson", {}, timeout)
def get_opq_sync(self, timeout: float = 10.0) -> dict:
"""Synchronous getOPQ: account info, main currency, brief info.
Response shape (top level):
{"OPQ": {"rev": int, "init_margin": int, "brief_nm": str, "main_curr": "USDT",
"active": int, "reception": int, "f_kval": "0", "quotes": {"q": [...]}}}
"""
return self._post_sync("getOPQ", {}, timeout)
def get_security_info_sync(self, ticker: str, timeout: float = 10.0) -> dict:
"""Synchronous getSecurityInfo: lot size, min step, currency, market.
Response:
{"id": "BTC-USDT.IMEX", "short_name": "BTC-USDT", "default_ticker": "BTC-IMEX",
"code_nm": "BTC-IMEX", "currency": "USDT", "min_step": "0.10000000",
"lot": "0.00001000", "mkt_name": "IMEX", "firstDate": "01.05.1970",
"mrkt": {"tz": "Europe/Minsk", "o": "05:00:00", "c": "02:00:00"}}
"""
return self._post_sync("getSecurityInfo", {"ticker": ticker, "sup": True}, timeout)
def get_hloc_sync(self, ticker: str, timeframe_min: int = 60,
date_from: str = "", date_to: str = "",
count: int = 0, timeout: float = 15.0) -> dict:
"""Synchronous getHloc: historical OHLCV candlesticks.
Args:
ticker: e.g. "BTC-USDT.IMEX"
timeframe_min: candle interval in minutes (1, 5, 15, 60, 1440)
date_from: "DD.MM.YYYY hh:mm" or empty
date_to: "DD.MM.YYYY hh:mm" or empty
count: extra candles beyond the date range (0 = none)
Response:
{"hloc": {"BTC-USDT.IMEX": [[open, high, low, close], ...]},
"vl": {"BTC-USDT.IMEX": [vol, vol, ...]},
"xSeries": {"BTC-USDT.IMEX": [unix_ts, ...]},
"info": {"BTC-USDT.IMEX": {...security info...}},
"maxSeries": {"BTC-USDT.IMEX": ts}}
"""
params = {
"id": ticker,
"timeframe": timeframe_min,
"date_from": date_from,
"date_to": date_to,
"count": count,
"intervalMode": "ClosedRay",
}
return self._post_sync("getHloc", params, timeout)
def _post_sync(self, command: str, params: dict, timeout: float = 10.0) -> dict:
"""Generic synchronous POST to Tradernet REST API. Returns parsed JSON or {} on error.
Used for: getOPQ, getPositionJson, getSecurityInfo, getHloc.
"""
payload = json.dumps(params, separators=(',', ':'))
ts, sig = self._sign(payload)
headers = {
"Content-Type": "application/json",
"X-NtApi-PublicKey": self.public_key,
"X-NtApi-Timestamp": ts,
"X-NtApi-Sig": sig,
}
url = f"{self.base_url}/api/{command}"
req = urllib.request.Request(url, data=payload.encode(), headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode())
if resp.status == 200:
return data
logger.warning(f"REST sync {command} status={resp.status}: {str(data)[:100]}")
return {}
except urllib.error.HTTPError as e:
logger.warning(f"REST sync {command} HTTPError {e.code}: {e.reason}")
return {}
except urllib.error.URLError as e:
logger.warning(f"REST sync {command} URLError: {e.reason}")
return {}
except Exception as e:
logger.warning(f"REST sync {command} error: {e}")
return {}
async def portfolio_get_changes(self) -> dict:
return await self._post("portfolio-get-changes", {})
# ─── Orders ───────────────────────────────────────────────────────────────
async def orders_get_current_history(self) -> dict:
return await self._post("getNotifyOrderJson", {"active_only": 1})
async def orders_get_history(self, date_from: str = "", date_to: str = "") -> dict:
params = {}
if date_from:
params["from"] = date_from
if date_to:
params["till"] = date_to
return await self._post("getOrdersHistory", params)
async def put_order(self, ticker: str, price: float, qty: float,
action: str = "BUY", order_type: str = "LIMIT",
client_id: str = "") -> dict:
params = {
"instr_name": ticker,
"price": str(price),
"qty": str(int(qty)),
"action_id": "2" if action.upper() == "BUY" else "4",
"order_type_id": "2" if order_type == "LIMIT" else "1",
"expiration_id": "1",
}
if client_id:
params["user_order_id"] = str(client_id)
return await self._post("putTradeOrder", params)
async def cancel_order(self, order_id: str) -> dict:
return await self._post("delTradeOrder", {"order_id": int(order_id)})
async def cancel_all_orders(self) -> dict:
return await self._post("delAllTradeOrders", {})
# ─── Sync versions for live trading (called from async trading_loop) ──────
# These are needed because the API's aiohttp session gets bound to the loop
# it was created in, but our trading_loop runs in its own loop. Calling
# aiohttp from a different loop fails. urllib is safe in any context.
def put_order_sync(self, ticker: str, price: float, qty: float,
action: str = "BUY", order_type: str = "LIMIT",
client_id: str = "", timeout: float = 15.0) -> dict:
"""Place a LIMIT order. Returns parsed JSON or {} on error.
Response shape on success:
{"order_id": <int>, "order": {<order fields>}, "warning": ""}
On rejection:
{"error": "<reason>", "code": 0}
"""
params = {
"instr_name": ticker,
"price": str(price),
"qty": str(int(qty)),
"action_id": "2" if action.upper() == "BUY" else "4", # 2=BUY, 4=SELL
"order_type_id": "2" if order_type == "LIMIT" else "1", # 2=LIMIT, 1=MARKET
"expiration_id": "1", # DAY
}
if client_id:
params["user_order_id"] = str(client_id)
return self._post_sync("putTradeOrder", params, timeout)
def cancel_order_sync(self, order_id: int, timeout: float = 10.0) -> dict:
"""Cancel an active order by its ID. Returns parsed JSON or {} on error."""
return self._post_sync("delTradeOrder", {"order_id": int(order_id)}, timeout)
def cancel_all_orders_sync(self, timeout: float = 15.0) -> dict:
"""Cancel all active orders. Returns parsed JSON or {} on error."""
return self._post_sync("delAllTradeOrders", {}, timeout)
def get_active_orders_sync(self, timeout: float = 10.0) -> list:
"""Get list of currently active orders for this account.
Response:
{"result": {"orders": {"key": "...", <order_id>: {<order fields>}, ...}}}
Returns a list of order dicts (extracted from the key'd map), empty list on error.
"""
data = self._post_sync("getNotifyOrderJson", {"active_only": 1}, timeout)
out = []
if not data:
return out
orders_map = data.get("result", {}).get("orders", {})
# The order map has a "key" field plus order_id keys
for k, v in orders_map.items():
if k == "key":
continue
if isinstance(v, dict):
out.append(v)
return out
def get_orders_history_sync(self, date_from: str = "", date_to: str = "",
timeout: float = 15.0) -> list:
"""Get list of orders for the given date range.
Args:
date_from: "YYYY-MM-DD" or empty (no lower bound)
date_to: "YYYY-MM-DD" or empty (no upper bound)
Response:
{"orders": {"order": [<order dict>, ...]}}
"""
params = {}
if date_from:
params["from"] = date_from
if date_to:
params["till"] = date_to
data = self._post_sync("getOrdersHistory", params, timeout)
if not data:
return []
orders_obj = data.get("orders", {})
order_list = orders_obj.get("order", [])
if isinstance(order_list, dict):
# API can return single order as dict too — wrap in list
order_list = [order_list]
return order_list or []
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
2026-06-21 10:45:47,609 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-21 10:45:47,609 INFO __main__ Tradernet API: https://tradernet.by
2026-06-21 10:45:47,609 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-21 10:45:47,609 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-21 10:45:47,609 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-21 10:45:47,611 INFO __main__ [bot-thread] starting trading_loop...
2026-06-21 10:45:47,611 INFO __main__ [main] trading_loop thread started: 136112699864768, alive=True
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-21 10:45:47,622 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-21 10:45:47,627 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
2026-06-21 10:45:47,636 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-21 10:45:47,636 INFO werkzeug Press CTRL+C to quit
2026-06-21 10:45:48,358 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-21 10:45:48,607 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-21 10:45:48,608 INFO __main__ [SMA] SMA(24, 60m) = $63,921.15 (140 candles)
2026-06-21 10:45:48,608 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,394.60 (bias=range, asymmetry=3:2, sma=$63,921.15)
2026-06-21 10:45:54,705 INFO werkzeug 127.0.0.1 - - [21/Jun/2026 10:45:54] "GET /ecosystem.config.js HTTP/1.1" 404 -
+20
View File
@@ -0,0 +1,20 @@
2026-06-21 10:47:12,698 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-21 10:47:12,699 INFO __main__ Tradernet API: https://tradernet.by
2026-06-21 10:47:12,699 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-21 10:47:12,699 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-21 10:47:12,699 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-21 10:47:12,700 INFO __main__ [bot-thread] starting trading_loop...
2026-06-21 10:47:12,700 INFO __main__ [main] trading_loop thread started: 140542795052736, alive=True
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-21 10:47:12,714 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-21 10:47:12,714 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
2026-06-21 10:47:12,720 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-21 10:47:12,720 INFO werkzeug Press CTRL+C to quit
2026-06-21 10:47:13,554 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-21 10:47:13,881 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-21 10:47:13,881 INFO __main__ [SMA] SMA(24, 60m) = $63,921.15 (140 candles)
2026-06-21 10:47:13,881 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,394.60 (bias=range, asymmetry=3:2, sma=$63,921.15)
+26
View File
@@ -0,0 +1,26 @@
2026-06-21 10:48:00,441 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-21 10:48:00,441 INFO __main__ Tradernet API: https://tradernet.by
2026-06-21 10:48:00,441 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-21 10:48:00,441 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-21 10:48:00,441 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-21 10:48:00,442 INFO __main__ [bot-thread] starting trading_loop...
2026-06-21 10:48:00,442 INFO __main__ [main] trading_loop thread started: 131886046246592, alive=True
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-21 10:48:00,452 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-21 10:48:00,452 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
2026-06-21 10:48:00,457 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-21 10:48:00,457 INFO werkzeug Press CTRL+C to quit
2026-06-21 10:48:01,165 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-21 10:48:01,427 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-21 10:48:01,427 INFO __main__ [SMA] SMA(24, 60m) = $63,921.15 (140 candles)
2026-06-21 10:48:01,427 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,394.60 (bias=range, asymmetry=3:2, sma=$63,921.15)
2026-06-21 10:48:33,539 INFO werkzeug 127.0.0.1 - - [21/Jun/2026 10:48:33] "GET /api/status HTTP/1.1" 200 -
2026-06-21 10:48:33,649 INFO werkzeug 127.0.0.1 - - [21/Jun/2026 10:48:33] "GET /api/grid HTTP/1.1" 200 -
2026-06-21 10:48:58,391 INFO werkzeug 127.0.0.1 - - [21/Jun/2026 10:48:58] "GET /api/grid HTTP/1.1" 200 -
2026-06-21 10:50:01,247 INFO werkzeug 127.0.0.1 - - [21/Jun/2026 10:50:01] "GET /api/status HTTP/1.1" 200 -
2026-06-21 10:50:01,264 INFO werkzeug 127.0.0.1 - - [21/Jun/2026 10:50:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-21 10:50:01,281 INFO werkzeug 127.0.0.1 - - [21/Jun/2026 10:50:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
File diff suppressed because it is too large Load Diff
+863
View File
@@ -0,0 +1,863 @@
2026-06-22 06:00:42,378 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:00:42,378 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:00:42,379 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:00:42,379 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:00:42,379 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:00:42,379 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:00:42,379 INFO __main__ [main] trading_loop thread started: 131271728559808, alive=True
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:00:42,391 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:00:42,392 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
2026-06-22 06:00:42,396 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-22 06:00:42,397 INFO werkzeug Press CTRL+C to quit
2026-06-22 06:00:43,207 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-22 06:00:43,492 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:00:43,492 INFO __main__ [SMA] SMA(24, 60m) = $64,160.80 (140 candles)
2026-06-22 06:00:43,492 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,262.00 (bias=range, asymmetry=3:2, sma=$64,160.80)
2026-06-22 06:01:16,881 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:16] "GET /.env HTTP/1.1" 404 -
2026-06-22 06:01:16,941 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:16] "GET /.env.php HTTP/1.1" 404 -
2026-06-22 06:01:17,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /.env.sample.php HTTP/1.1" 404 -
2026-06-22 06:01:17,061 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /.env.local.php HTTP/1.1" 404 -
2026-06-22 06:01:17,122 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /.env.production.php HTTP/1.1" 404 -
2026-06-22 06:01:17,182 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /config.dev.php HTTP/1.1" 404 -
2026-06-22 06:01:17,242 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /config.env HTTP/1.1" 404 -
2026-06-22 06:01:17,304 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /twilio/.env.php HTTP/1.1" 404 -
2026-06-22 06:01:17,364 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /config/.env.php HTTP/1.1" 404 -
2026-06-22 06:01:17,424 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /sendgrid/.env.php HTTP/1.1" 404 -
2026-06-22 06:01:17,484 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /.env.php.bak HTTP/1.1" 404 -
2026-06-22 06:01:17,544 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /.env.php-bak HTTP/1.1" 404 -
2026-06-22 06:01:17,604 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /.env.php-backup HTTP/1.1" 404 -
2026-06-22 06:01:17,663 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /.env.php.backup HTTP/1.1" 404 -
2026-06-22 06:01:17,723 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /test/.env.php HTTP/1.1" 404 -
2026-06-22 06:01:17,783 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /.env.example.php HTTP/1.1" 404 -
2026-06-22 06:01:17,843 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /app_dev.php/_profiler/.env HTTP/1.1" 404 -
2026-06-22 06:01:17,904 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /var/.env.local.php HTTP/1.1" 404 -
2026-06-22 06:01:17,964 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "GET /api/.env HTTP/1.1" 401 -
2026-06-22 06:01:18,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /app/.env HTTP/1.1" 404 -
2026-06-22 06:01:18,084 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /admin/.env HTTP/1.1" 404 -
2026-06-22 06:01:18,144 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /.env.production HTTP/1.1" 404 -
2026-06-22 06:01:18,203 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /.env.save HTTP/1.1" 404 -
2026-06-22 06:01:18,264 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /backend/.env HTTP/1.1" 404 -
2026-06-22 06:01:18,324 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /core/.env HTTP/1.1" 404 -
2026-06-22 06:01:18,384 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /laravel/.env HTTP/1.1" 404 -
2026-06-22 06:01:18,444 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /.env.example HTTP/1.1" 404 -
2026-06-22 06:01:18,505 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /.env.prod HTTP/1.1" 404 -
2026-06-22 06:01:18,565 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /.env.local HTTP/1.1" 404 -
2026-06-22 06:01:18,625 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /.env.bak HTTP/1.1" 404 -
2026-06-22 06:01:18,684 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /public/.env HTTP/1.1" 404 -
2026-06-22 06:01:18,745 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /.env.dev HTTP/1.1" 404 -
2026-06-22 06:01:18,805 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /.env.backup HTTP/1.1" 404 -
2026-06-22 06:01:18,866 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /local/.env HTTP/1.1" 404 -
2026-06-22 06:01:18,926 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /config/.env HTTP/1.1" 404 -
2026-06-22 06:01:18,986 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "GET /.env.old HTTP/1.1" 404 -
2026-06-22 06:01:19,045 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /old/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,105 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /apps/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,169 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /new/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,229 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /vendor/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,289 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /web/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,349 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /dev/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,408 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /wp-content/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,469 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /crm/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,529 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /.env.development HTTP/1.1" 404 -
2026-06-22 06:01:19,589 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /www/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,649 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /library/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,709 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /.env.stage HTTP/1.1" 404 -
2026-06-22 06:01:19,769 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /src/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,828 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /application/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,888 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /prod/.env HTTP/1.1" 404 -
2026-06-22 06:01:19,948 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "GET /staging/.env HTTP/1.1" 404 -
2026-06-22 06:01:20,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "GET /.env.test HTTP/1.1" 404 -
2026-06-22 06:01:20,068 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "GET /.env.dist HTTP/1.1" 404 -
2026-06-22 06:01:20,128 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "GET /.env.production.local HTTP/1.1" 404 -
2026-06-22 06:01:20,189 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "GET /.env_sample HTTP/1.1" 404 -
2026-06-22 06:01:20,248 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "GET /wp-admin/.env HTTP/1.1" 404 -
2026-06-22 06:01:20,308 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "GET /conf/.env HTTP/1.1" 404 -
2026-06-22 06:01:20,370 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "GET /.env.development.local HTTP/1.1" 404 -
2026-06-22 06:01:37,050 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:01:37,050 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:01:37,053 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:01:37,057 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:01:37,059 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:02:04,159 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:04] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:05,739 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:05] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:05,740 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:05] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:05,741 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:05] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:05,745 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:05] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:05,752 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:05] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:02:07,104 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "GET / HTTP/1.1" 200 -
2026-06-22 06:02:07,381 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "GET /static/chartjs-plugin-annotation.min.js HTTP/1.1" 304 -
2026-06-22 06:02:07,381 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "GET /static/chart.umd.min.js HTTP/1.1" 304 -
2026-06-22 06:02:07,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:07,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "GET /api/settings HTTP/1.1" 200 -
2026-06-22 06:02:07,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:07,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:02:07,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:07,516 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:11,046 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:11] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:11,046 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:11] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:11,048 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:11] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:11,051 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:11] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:14,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:14] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:14,040 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:14] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:14,042 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:14] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:14,044 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:14] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:17,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:17] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:17,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:17] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:17,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:17] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:18,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:18] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:02:20,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:20] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:20,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:20] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:20,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:20] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:20,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:20] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:23,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:23] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:23,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:23] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:23,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:23] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:23,041 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:23] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:26,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:26] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:26,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:26] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:26,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:26] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:28,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:28] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:02:29,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:29] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:29,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:29] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:29,042 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:29] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:29,043 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:29] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:32,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:32] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:32,040 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:32] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:32,040 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:32] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:32,043 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:32] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:35,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:35,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:35,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:38,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:38] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:38,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:38] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:38,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:38] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:38,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:38] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:38,040 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:38] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:02:41,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:41] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:41,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:41] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:41,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:41] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:44,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:44] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:44,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:44] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:44,039 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:44] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:44,041 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:44] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:47,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:47] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:47,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:47] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:47,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:47] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:48,049 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:48] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:02:50,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:50] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:50,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:50,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:50,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:53,040 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:53] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:53,042 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:53] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:53,045 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:53,046 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:53] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:02:56,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:56,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:56] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:56,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:56] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:58,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:58] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:02:59,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:59] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:02:59,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:59] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:02:59,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:59] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:02:59,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:59] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:01,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:01,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:01] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:01,468 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:01] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:05,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:05] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:05,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:05] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:05,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:05] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:05,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:05] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:08,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:08] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:03:08,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:08] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:08,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:08] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:08,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:08] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:11,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:11] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:11,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:11] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:11,039 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:11] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:11,039 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:11] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:14,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:14] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:14,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:14] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:14,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:14] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:14,041 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:14] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:17,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:17] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:17,040 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:17] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:17,044 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:17] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:17,045 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:17] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:18,045 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:18] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:03:20,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:20] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:20,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:20] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:20,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:20] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:23,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:23] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:23,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:23] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:23,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:23] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:23,041 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:23] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:26,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:26] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:26,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:26] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:26,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:26] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:26,041 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:26] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:28,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:28] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:03:29,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:29] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:29,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:29] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:29,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:29] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:32,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:32] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:32,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:32] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:32,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:32] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:32,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:32] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:35,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:35,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:35,040 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:35,041 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:35] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:38,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:38] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:38,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:38] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:03:38,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:38] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:38,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:38] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:41,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:41] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:41,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:41] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:41,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:41] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:41,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:41] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:44,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:44] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:44,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:44] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:44,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:44] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:44,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:44] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:47,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:47] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:47,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:47] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:47,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:47] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:48,042 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:48] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:03:50,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:50,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:50,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:50] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:50,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:53,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:53] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:53,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:53] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:53,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:53] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:53,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:56,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:56] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:03:56,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:56,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:56] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:03:56,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:56] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:58,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:58] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:03:59,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:59] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:03:59,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:59] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:03:59,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:03:59] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:04:37,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:04:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:04:37,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:04:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:04:37,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:04:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:04:37,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:04:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:04:37,093 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:04:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:05:01,786 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:05:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:05:01,804 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:05:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:05:01,838 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:05:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:05:37,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:05:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:05:37,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:05:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:05:37,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:05:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:05:37,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:05:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:05:37,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:05:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:05:43,600 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:05:43,600 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:06:37,043 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:06:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:06:37,048 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:06:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:06:37,049 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:06:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:06:37,052 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:06:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:06:37,054 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:06:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:07:37,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:07:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:07:37,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:07:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:07:37,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:07:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:07:37,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:07:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:07:37,041 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:07:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:07:59,149 ERROR werkzeug 35.194.204.9 - - [22/Jun/2026 06:07:59] code 400, message Bad request syntax ('CNXN\x00\x00\x00\x01\x00\x00\x04\x00\x1b\x00\x00\x00M')
2026-06-22 06:07:59,149 INFO werkzeug 35.194.204.9 - - [22/Jun/2026 06:07:59] "CNXN\x00\x00\x00\x01\x00\x00\x04\x00\x1b\x00\x00\x00M" 400 -
2026-06-22 06:08:37,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:08:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:08:37,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:08:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:08:37,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:08:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:08:37,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:08:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:08:37,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:08:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:09:37,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:09:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:09:37,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:09:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:09:37,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:09:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:09:37,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:09:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:09:37,039 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:09:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:10:01,973 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:10:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:10:01,992 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:10:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:10:02,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:10:02] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:10:37,141 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:10:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:10:37,146 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:10:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:10:37,148 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:10:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:10:37,149 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:10:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:10:37,152 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:10:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:10:45,290 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:10:45,290 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:11:35,257 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:11:35,260 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:11:35,264 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:35] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:11:35,265 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:11:35,267 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:35] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:11:37,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:11:37,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:11:37,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:11:37,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:11:40,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:40] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:11:40,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:40] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:11:40,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:40] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:11:40,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:40] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:11:43,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:43] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:11:43,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:43] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:11:43,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:43] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:11:46,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:46] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:11:46,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:46] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:11:46,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:46] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:11:46,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:46] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:11:47,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:47] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:11:49,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:49] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:11:49,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:49] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:11:49,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:49] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:11:49,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:49] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:11:52,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:52] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:11:52,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:52] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:11:52,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:52] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:11:55,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:55] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:11:55,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:55] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:11:55,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:55] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:11:55,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:55] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:11:57,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:57] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:11:58,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:58] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:11:58,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:58] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:11:58,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:58] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:11:58,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:11:58] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:01,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:01] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:01,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:01,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:01] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:04,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:04] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:04,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:04] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:04,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:04] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:04,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:04] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:07,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:07] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:07,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:07] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:07,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:07] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:12:07,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:07] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:07,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:07] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:10,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:10] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:10,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:10] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:10,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:10] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:13,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:13] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:13,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:13] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:13,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:13] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:13,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:13] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:16,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:16] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:16,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:16] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:16,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:16] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:17,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:17] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:12:19,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:19] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:19,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:19] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:19,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:19] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:19,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:19] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:22,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:22] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:22,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:22] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:22,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:22] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:22,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:22] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:25,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:25] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:25,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:25] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:25,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:25] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:25,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:25] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:27,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:27] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:12:28,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:28] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:28,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:28] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:28,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:28] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:31,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:31] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:31,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:31] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:31,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:31] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:31,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:31] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:34,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:34] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:34,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:34] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:34,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:34] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:34,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:34] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:37,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:37,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:37,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:37,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:12:37,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:40,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:40] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:40,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:40] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:40,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:40] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:43,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:43] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:43,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:43] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:43,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:43] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:43,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:43] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:46,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:46] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:46,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:46] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:46,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:46] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:47,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:47] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:12:49,445 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:49] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:49,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:49] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:49,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:49] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:49,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:49] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:52,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:52] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:52,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:52] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:52,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:52] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:52,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:52] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:55,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:55] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:55,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:55] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:55,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:55] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:12:55,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:55] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:12:57,470 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:57] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:12:58,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:58] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:12:58,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:58] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:12:58,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:12:58] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:01,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:01,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:01] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:01,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:01] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:01,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:01] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:04,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:04] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:04,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:04] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:04,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:04] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:07,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:07] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:07,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:07] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:07,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:07] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:07,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:07] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:07,466 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:07] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:13:10,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:10] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:10,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:10] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:10,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:10] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:13,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:13] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:13,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:13] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:13,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:13] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:13,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:13] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:16,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:16] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:16,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:16] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:16,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:16] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:16,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:16] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:17,444 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:17] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:13:19,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:19] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:19,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:19] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:19,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:19] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:19,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:19] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:22,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:22] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:22,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:22] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:22,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:22] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:22,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:22] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:25,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:25] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:25,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:25] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:25,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:25] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:25,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:25] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:27,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:27] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:13:28,445 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:28] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:28,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:28] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:28,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:28] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:31,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:31] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:31,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:31] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:31,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:31] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:31,466 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:31] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:34,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:34] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:34,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:34] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:34,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:34] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:37,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:37,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:37,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:13:37,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:37,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:40,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:40] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:40,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:40] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:40,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:40] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:40,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:40] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:43,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:43] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:43,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:43] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:43,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:43] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:46,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:46] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:46,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:46] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:46,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:46] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:46,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:46] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:47,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:47] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:13:49,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:49] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:49,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:49] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:49,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:49] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:49,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:49] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:52,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:52] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:52,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:52] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:52,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:52] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:13:52,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:52] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:55,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:55] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:55,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:55] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:55,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:55] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:57,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:57] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:13:58,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:58] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:13:58,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:58] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:13:58,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:58] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:13:58,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:13:58] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:01,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:01,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:01] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:01,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:01] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:04,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:04] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:04,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:04] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:04,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:04] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:04,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:04] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:07,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:07] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:07,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:07] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:07,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:07] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:07,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:07] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:14:07,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:07] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:10,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:10] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:10,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:10] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:10,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:10] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:13,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:13] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:13,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:13] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:13,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:13] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:13,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:13] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:16,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:16] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:16,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:16] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:16,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:16] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:16,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:16] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:17,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:17] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:14:19,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:19] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:19,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:19] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:19,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:19] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:22,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:22] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:22,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:22] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:22,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:22] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:22,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:22] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:25,444 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:25] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:25,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:25] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:25,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:25] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:27,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:27] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:14:28,445 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:28] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:28,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:28] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:28,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:28] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:28,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:28] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:31,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:31] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:31,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:31] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:31,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:31] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:31,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:31] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:34,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:34] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:34,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:34] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:34,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:34] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:37,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:37,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:37,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:14:37,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:37,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:40,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:40] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:40,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:40] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:40,468 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:40] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:40,469 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:40] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:43,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:43] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:43,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:43] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:43,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:43] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:43,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:43] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:46,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:46] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:46,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:46] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:46,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:46] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:46,466 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:46] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:47,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:47] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:14:49,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:49] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:49,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:49] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:49,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:49] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:52,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:52] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:52,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:52] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:52,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:52] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:52,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:52] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:55,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:55] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:55,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:55] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:14:55,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:55] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:55,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:55] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:14:57,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:57] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:14:58,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:58] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:14:58,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:58] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:14:58,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:14:58] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:01,167 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:01,188 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:15:01,203 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:15:01,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:01,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:01] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:01,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:01] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:01,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:01] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:04,445 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:04] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:04,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:04] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:04,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:04] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:07,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:07] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:07,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:07] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:07,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:07] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:15:07,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:07] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:07,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:07] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:10,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:10] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:10,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:10] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:10,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:10] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:10,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:10] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:13,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:13] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:13,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:13] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:13,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:13] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:13,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:13] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:16,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:16] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:16,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:16] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:16,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:16] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:16,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:16] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:17,443 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:17] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:15:19,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:19] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:19,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:19] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:19,468 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:19] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:19,469 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:19] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:22,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:22] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:22,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:22] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:22,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:22] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:25,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:25] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:25,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:25] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:25,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:25] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:25,466 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:25] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:27,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:27] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:15:28,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:28] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:28,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:28] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:28,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:28] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:28,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:28] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:31,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:31] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:31,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:31] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:31,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:31] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:34,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:34] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:34,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:34] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:34,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:34] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:34,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:34] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:37,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:37,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:37,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:37,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:37,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:15:40,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:40] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:40,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:40] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:40,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:40] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:43,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:43] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:43,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:43] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:43,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:43] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:43,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:43] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:45,252 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:15:45,252 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:15:46,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:46] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:46,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:46] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:46,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:46] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:46,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:46] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:47,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:47] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:15:49,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:49] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:49,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:49] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:49,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:49] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:49,466 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:49] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:52,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:52] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:52,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:52] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:52,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:52] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:55,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:55] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:55,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:55] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:55,466 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:55] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:15:55,467 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:55] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:15:57,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:57] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:15:58,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:58] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:15:58,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:58] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:15:58,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:15:58] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:01,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:01] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:01,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:01,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:01] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:01,461 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:01] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:04,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:04] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:04,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:04] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:04,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:04] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:07,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:07] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:07,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:07] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:07,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:07] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:07,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:07] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:16:07,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:07] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:10,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:10] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:10,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:10] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:10,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:10] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:10,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:10] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:13,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:13] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:13,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:13] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:13,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:13] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:16,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:16] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:16,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:16] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:16,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:16] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:16,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:16] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:17,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:17] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:16:19,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:19] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:19,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:19] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:19,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:19] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:19,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:19] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:22,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:22] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:22,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:22] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:22,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:22] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:22,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:22] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:25,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:25] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:25,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:25] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:25,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:25] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:27,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:27] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:16:28,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:28] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:28,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:28] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:28,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:28] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:28,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:28] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:31,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:31] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:31,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:31] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:31,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:31] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:31,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:31] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:34,454 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:34] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:34,456 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:34] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:34,457 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:34] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:37,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:37,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:37,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:37,462 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:16:37,464 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:40,463 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:40] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:40,465 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:40] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:40,468 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:40] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:40,468 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:40] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:43,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:43] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:43,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:43] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:43,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:43] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:46,445 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:46] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:46,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:46] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:46,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:46] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:46,459 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:46] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:47,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:47] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:16:49,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:49] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:49,449 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:49] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:49,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:49] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:49,453 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:49] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:52,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:52] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:52,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:52] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:52,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:52] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:55,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:55] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:55,447 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:55] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:55,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:55] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:16:55,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:55] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:57,450 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:57] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:16:59,037 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:59] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:16:59,039 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:59] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:16:59,042 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:59] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:16:59,044 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:16:59] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:02,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:02] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:02,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:02] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:02,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:02] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:02,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:02] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:05,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:05] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:05,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:05] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:05,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:05] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:05,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:05] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:08,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:08] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:08,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:08] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:08,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:08] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:17:08,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:08] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:11,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:11] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:11,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:11] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:11,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:11] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:11,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:11] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:14,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:14] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:14,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:14] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:14,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:14] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:14,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:14] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:17,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:17] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:17,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:17] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:17,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:17] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:17,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:17] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:18,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:18] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:17:20,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:20] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:20,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:20] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:20,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:20] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:20,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:20] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:23,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:23] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:23,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:23] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:23,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:23] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:26,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:26] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:26,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:26] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:26,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:26] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:26,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:26] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:28,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:28] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:17:29,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:29] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:29,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:29] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:29,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:29] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:29,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:29] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:32,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:32] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:32,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:32] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:32,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:32] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:35,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:35,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:35,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:35] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:35,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:38,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:38] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:38,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:38] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:38,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:38] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:38,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:38] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:17:38,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:38] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:41,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:41] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:41,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:41] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:41,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:41] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:44,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:44] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:44,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:44] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:44,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:44] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:44,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:44] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:47,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:47] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:47,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:47] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:47,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:47] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:47,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:47] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:48,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:48] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:17:50,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:50,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:50,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:53,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:53] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:53,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:53] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:17:53,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:53,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:53] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:56,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:17:56,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:56] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:17:56,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:56] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:17:56,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:17:56] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:18:37,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:18:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:18:37,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:18:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:18:37,034 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:18:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:18:37,035 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:18:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:18:37,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:18:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:19:37,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:19:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:19:37,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:19:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:19:37,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:19:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:19:37,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:19:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:19:37,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:19:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:20:01,353 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:20:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:20:01,370 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:20:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:20:01,391 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:20:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:20:37,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:20:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:20:37,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:20:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:20:37,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:20:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:20:37,039 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:20:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:20:37,041 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:20:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:20:45,987 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:20:45,987 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:21:37,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:21:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:21:37,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:21:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:21:37,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:21:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:21:37,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:21:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:21:37,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:21:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:22:37,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:22:37,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:22:37,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:22:37,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:22:37,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:22:49,683 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:49] "GET / HTTP/1.1" 401 -
2026-06-22 06:22:49,797 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:49] "GET /.env.staging HTTP/1.1" 404 -
2026-06-22 06:22:49,905 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:49] "GET /.env.backup HTTP/1.1" 404 -
2026-06-22 06:22:50,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "GET /.env.save HTTP/1.1" 404 -
2026-06-22 06:22:50,140 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "GET /env HTTP/1.1" 404 -
2026-06-22 06:22:50,248 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "GET /admin/.env HTTP/1.1" 404 -
2026-06-22 06:22:50,358 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "GET /server/.env.production HTTP/1.1" 404 -
2026-06-22 06:22:50,470 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "GET /.env.old HTTP/1.1" 404 -
2026-06-22 06:22:50,577 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "GET /.dotenv HTTP/1.1" 404 -
2026-06-22 06:22:50,696 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "GET /.env.prod HTTP/1.1" 404 -
2026-06-22 06:22:50,806 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "GET /.env.development HTTP/1.1" 404 -
2026-06-22 06:22:50,911 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "GET /.env.stage HTTP/1.1" 404 -
2026-06-22 06:22:51,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "GET /config/.env.local HTTP/1.1" 404 -
2026-06-22 06:22:51,152 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "GET /web/.env HTTP/1.1" 404 -
2026-06-22 06:22:51,262 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "GET /repository/.env HTTP/1.1" 404 -
2026-06-22 06:22:51,378 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "GET /.env.dev HTTP/1.1" 404 -
2026-06-22 06:22:51,487 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "GET /.env.testing HTTP/1.1" 404 -
2026-06-22 06:22:51,595 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "GET /.envrc HTTP/1.1" 404 -
2026-06-22 06:22:51,706 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "GET /.envrc.local HTTP/1.1" 404 -
2026-06-22 06:23:08,261 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:23:08] "GET /api/kill-switch/status HTTP/1.1" 404 -
2026-06-22 06:23:37,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:23:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:23:37,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:23:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:23:37,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:23:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:23:37,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:23:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:23:37,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:23:37] "GET /api/logs HTTP/1.1" 200 -
+13
View File
@@ -0,0 +1,13 @@
2026-06-22 06:01:36,062 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:01:36,062 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:01:36,062 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:01:36,062 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:01:36,062 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:01:36,063 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:01:36,064 INFO __main__ [main] trading_loop thread started: 135699520026304, alive=True
2026-06-22 06:01:36,071 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:01:36,072 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
* Serving Flask app 'grid-bot'
* Debug mode: off
Address already in use
Port 5555 is in use by another program. Either identify and stop that program, or start the server with a different port.
+19
View File
@@ -0,0 +1,19 @@
2026-06-22 06:22:30,152 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:22:30,152 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:22:30,152 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:22:30,152 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:22:30,152 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:22:30,153 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:22:30,153 INFO __main__ [main] trading_loop thread started: 124692629288640, alive=True
2026-06-22 06:22:30,153 ERROR __main__ [bot-thread] crashed: name 'kill_switch' is not defined
Traceback (most recent call last):
File "/root/grid-bot/main.py", line 3244, in _safe_start
start_bot_async()
File "/root/grid-bot/main.py", line 2337, in start_bot_async
kill_switch.start() # Запускаем мониторинг (если enabled)
^^^^^^^^^^^
NameError: name 'kill_switch' is not defined
* Serving Flask app 'grid-bot'
* Debug mode: off
Address already in use
Port 5555 is in use by another program. Either identify and stop that program, or start the server with a different port.
+41
View File
@@ -0,0 +1,41 @@
2026-06-22 06:24:36,951 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:24:36,951 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:24:36,952 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:24:36,952 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:24:36,952 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:24:36,952 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:24:36,952 INFO __main__ [main] trading_loop thread started: 133517001168576, alive=True
2026-06-22 06:24:36,953 ERROR __main__ [bot-thread] crashed: name 'kill_switch' is not defined
Traceback (most recent call last):
File "/root/grid-bot/main.py", line 3278, in _safe_start
start_bot_async()
File "/root/grid-bot/main.py", line 2337, in start_bot_async
kill_switch.start() # Запускаем мониторинг (если enabled)
^^^^^^^^^^^
NameError: name 'kill_switch' is not defined
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:24:36,987 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-22 06:24:36,987 INFO werkzeug Press CTRL+C to quit
2026-06-22 06:24:37,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:24:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:24:37,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:24:37] "GET /api/grid HTTP/1.1" 503 -
2026-06-22 06:24:37,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:24:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:24:37,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:24:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:24:37,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:24:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:25:01,538 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:25:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:25:01,558 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:25:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:25:01,584 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:25:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:25:37,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:25:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:25:37,030 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:25:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:25:37,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:25:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:25:37,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:25:37] "GET /api/grid HTTP/1.1" 503 -
2026-06-22 06:25:37,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:25:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:26:37,038 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:26:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:26:37,042 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:26:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:26:37,043 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:26:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:26:37,045 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:26:37] "GET /api/grid HTTP/1.1" 503 -
2026-06-22 06:26:37,048 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:26:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:26:38,348 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:26:38] "GET / HTTP/1.1" 401 -
+19
View File
@@ -0,0 +1,19 @@
2026-06-22 06:25:14,724 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:25:14,724 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:25:14,724 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:25:14,724 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:25:14,725 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:25:14,726 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:25:14,726 INFO __main__ [main] trading_loop thread started: 137990061946560, alive=True
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:25:14,726 ERROR __main__ [bot-thread] crashed: name 'kill_switch' is not defined
Traceback (most recent call last):
File "/root/grid-bot/main.py", line 3278, in _safe_start
start_bot_async()
File "/root/grid-bot/main.py", line 2337, in start_bot_async
kill_switch.start() # Запускаем мониторинг (если enabled)
^^^^^^^^^^^
NameError: name 'kill_switch' is not defined
Address already in use
Port 5555 is in use by another program. Either identify and stop that program, or start the server with a different port.
+4
View File
@@ -0,0 +1,4 @@
File "/root/grid-bot/main.py", line 249
tg_notify("🚨 KILL SWITCH TRIGGERED
^
SyntaxError: unterminated string literal (detected at line 249)
+273
View File
@@ -0,0 +1,273 @@
2026-06-22 06:27:40,439 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:27:40,439 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:27:40,439 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:27:40,439 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:27:40,439 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:27:40,440 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:27:40,440 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:27:40,440 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
2026-06-22 06:27:40,440 INFO __main__ [main] trading_loop thread started: 130272511129280, alive=True
2026-06-22 06:27:40,443 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:27:40,450 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:27:40,466 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-22 06:27:40,466 INFO werkzeug Press CTRL+C to quit
2026-06-22 06:27:41,416 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-22 06:27:41,754 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:27:41,755 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:27:41,755 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,090.00 (bias=range, asymmetry=3:2, sma=$64,161.54)
2026-06-22 06:28:37,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:28:37,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:28:37,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:28:37,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:28:37,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:28:41,749 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:41] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:28:42,513 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:42] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:28:51,970 CRITICAL __main__ [KILL-SWITCH] TRIGGERED: test:test_by_kolp
2026-06-22 06:28:52,356 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:52] "POST /api/kill-switch/test HTTP/1.1" 200 -
2026-06-22 06:28:53,778 CRITICAL __main__ [KILL-SWITCH] loop exiting: test:test_by_kolp
2026-06-22 06:28:53,778 INFO grid [DEMO] cancelled 3 open BUY order(s) and refunded $19.06; reason=kill_switch:test:test_by_kolp
2026-06-22 06:28:53,778 WARNING __main__ [KILL-SWITCH] cancelled 3 demo BUY orders
2026-06-22 06:28:56,111 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:56] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:28:56,881 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:28:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:29:04,828 WARNING __main__ [KILL-SWITCH] RESET - kill event cleared, peak=0
2026-06-22 06:29:04,828 WARNING __main__ [KILL-SWITCH] RESET via /api/kill-switch/reset (manual)
2026-06-22 06:29:05,156 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:05] "POST /api/kill-switch/reset HTTP/1.1" 200 -
2026-06-22 06:29:05,923 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:05] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:29:06,171 ERROR werkzeug 35.225.153.65 - - [22/Jun/2026 06:29:06] code 400, message Bad request syntax ('CNXN\x00\x00\x00\x01\x00\x00\x04\x00\x1b\x00\x00\x00M')
2026-06-22 06:29:06,172 INFO werkzeug 35.225.153.65 - - [22/Jun/2026 06:29:06] "CNXN\x00\x00\x00\x01\x00\x00\x04\x00\x1b\x00\x00\x00M" 400 -
2026-06-22 06:29:06,682 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:06] "POST /api/bot/start HTTP/1.1" 200 -
2026-06-22 06:29:06,684 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:29:06,685 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
2026-06-22 06:29:07,412 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-22 06:29:07,412 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,109.60 (bias=range, asymmetry=3:2, sma=$64,161.54)
2026-06-22 06:29:11,445 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:11] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:29:12,205 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:12] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:29:21,902 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:21] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:29:37,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:29:37,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:29:37,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:29:37,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:29:37,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:29:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:30:01,745 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:30:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:30:01,774 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:30:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:30:01,798 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:30:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:30:37,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:30:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:30:37,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:30:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:30:37,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:30:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:30:37,033 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:30:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:30:37,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:30:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:31:13,896 ERROR werkzeug 176.65.139.140 - - [22/Jun/2026 06:31:13] Request timed out: TimeoutError('timed out')
2026-06-22 06:31:37,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:31:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:31:37,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:31:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:31:37,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:31:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:31:37,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:31:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:31:37,032 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:31:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:32:37,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:32:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:32:37,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:32:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:32:37,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:32:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:32:37,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:32:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:32:37,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:32:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:32:42,136 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:32:42,136 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:33:37,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:33:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:33:37,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:33:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:33:37,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:33:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:33:37,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:33:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:33:37,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:33:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:34:37,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:34:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:34:37,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:34:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:34:37,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:34:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:34:37,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:34:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:34:37,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:34:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:01,953 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:01,973 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:35:01,991 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:35:20,552 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:20] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:20,555 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:20] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:20,558 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:20] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:20,559 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:20] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:35:20,564 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:20] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:22,432 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:22] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:22,435 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:22] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:22,435 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:22] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:25,435 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:25] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:25,436 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:25] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:25,441 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:25] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:25,443 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:25] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:27,443 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:27] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:35:28,428 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:28] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:28,431 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:28] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:28,432 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:28] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:31,437 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:31] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:31,439 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:31] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:31,442 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:31] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:31,442 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:31] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:35,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:35,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:35,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:35] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:35,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:38,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:38] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:38,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:38] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:38,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:38] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:35:38,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:38] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:38,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:38] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:41,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:41] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:41,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:41] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:41,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:41] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:44,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:44] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:44,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:44] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:44,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:44] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:44,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:44] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:47,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:47] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:47,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:47] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:47,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:47] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:47,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:47] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:48,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:48] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:35:50,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:50,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:50,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:50,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:50] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:53,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:53,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:53] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:53,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:53] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:53,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:53] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:56,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:56] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:56,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:56,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:56] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:56,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:56] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:35:58,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:58] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:35:59,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:59] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:35:59,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:59] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:35:59,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:59] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:35:59,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:35:59] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:36:02,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:02] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:02,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:02] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:02,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:02] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:05,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:05] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:05,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:05] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:05,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:05] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:36:05,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:05] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:08,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:08] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:36:08,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:08] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:08,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:08] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:08,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:08] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:36:08,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:08] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:11,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:11] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:11,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:11] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:11,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:11] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:14,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:14] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:14,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:14] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:14,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:14] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:14,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:14] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:36:17,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:17] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:17,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:17] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:17,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:17] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:36:17,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:17] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:18,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:18] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:36:20,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:20] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:20,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:20] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:20,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:20] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:23,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:23] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:23,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:23] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:23,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:23] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:36:23,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:23] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:26,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:26] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:26,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:26] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:26,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:26] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:26,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:26] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:36:28,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:28] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:36:29,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:29] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:29,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:29] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:29,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:29] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:36:29,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:29] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:36:37,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:36:37,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:36:37,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:36:37,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:36:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:17,319 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:17] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:17,320 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:17] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:17,323 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:17] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:17,325 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:17] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:37:17,328 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:17] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:19,442 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:19] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:19,443 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:19] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:19,445 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:19] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:22,431 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:22] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:22,433 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:22] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:22,434 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:22] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:22,437 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:22] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:25,435 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:25] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:25,439 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:25] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:25,440 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:25] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:25,441 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:25] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:27,436 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:27] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:37:29,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:29] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:29,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:29] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:29,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:29] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:29,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:29] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:32,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:32] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:32,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:32] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:32,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:32] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:35,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:35] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:35,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:35,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:35,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:38,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:38] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:38,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:38] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:38,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:38] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:38,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:38] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:37:38,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:38] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:41,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:41] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:41,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:41] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:41,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:41] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:41,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:41] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:42,657 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:37:42,657 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:37:44,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:44] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:44,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:44] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:44,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:44] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:47,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:47] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:47,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:47] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:47,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:47] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:47,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:47] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:48,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:48] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:37:50,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:50,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:50,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:50,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:50] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:53,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:53,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:53] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:53,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:53] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:56,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:56] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:56,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:37:56,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:56] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:56,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:56] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:37:58,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:58] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:37:59,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:59] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:37:59,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:59] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:37:59,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:37:59] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:02,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:02] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:02,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:02] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:02,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:02] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:02,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:02] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:05,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:05] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:05,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:05] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:05,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:05] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:05,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:05] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:08,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:08] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:08,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:08] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:08,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:08] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:08,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:08] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:08,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:08] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:38:11,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:11] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:11,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:11] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:11,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:11] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:14,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:14] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:14,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:14] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:14,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:14] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:14,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:14] "GET /api/kronos HTTP/1.1" 200 -
+15
View File
@@ -0,0 +1,15 @@
2026-06-22 06:28:16,624 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:28:16,625 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:28:16,625 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:28:16,625 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:28:16,625 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:28:16,625 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:28:16,626 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:28:16,626 INFO __main__ [main] trading_loop thread started: 140245703063232, alive=True
2026-06-22 06:28:16,626 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:28:16,642 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:28:16,642 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
Address already in use
Port 5555 is in use by another program. Either identify and stop that program, or start the server with a different port.
+15
View File
@@ -0,0 +1,15 @@
2026-06-22 06:37:58,340 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:37:58,340 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:37:58,340 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:37:58,340 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:37:58,340 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:37:58,341 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:37:58,341 INFO __main__ [main] trading_loop thread started: 137500082304704, alive=True
2026-06-22 06:37:58,342 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:37:58,342 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:37:58,356 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:37:58,356 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
Address already in use
Port 5555 is in use by another program. Either identify and stop that program, or start the server with a different port.
+506
View File
@@ -0,0 +1,506 @@
2026-06-22 06:38:17,247 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:38:17,248 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:38:17,248 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:38:17,248 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:38:17,248 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:38:17,249 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:38:17,249 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:38:17,249 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
2026-06-22 06:38:17,249 INFO __main__ [main] trading_loop thread started: 128171043190464, alive=True
2026-06-22 06:38:17,252 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:38:17,259 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:38:17,275 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-22 06:38:17,277 INFO werkzeug Press CTRL+C to quit
2026-06-22 06:38:17,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:17] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:38:17,965 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-22 06:38:18,304 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:38:18,305 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:38:18,305 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,087.80 (bias=range, asymmetry=3:2, sma=$64,161.54)
2026-06-22 06:38:19,437 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:19] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:19,440 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:19] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:19,443 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:19] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:19,444 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:19] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:22,434 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:22] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:22,436 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:22] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:22,438 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:22] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:25,442 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:25] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:25,445 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:25] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:25,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:25] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:25,448 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:25] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:27,427 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:27] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:38:28,429 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:28] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:28,430 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:28] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:28,432 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:28] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:32,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:32] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:32,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:32] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:32,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:32] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:32,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:32] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:35,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:35,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:35,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:38,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:38] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:38,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:38] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:38,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:38] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:38,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:38] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:38,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:38] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:38:41,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:41] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:41,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:41] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:41,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:41] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:41,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:41] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:44,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:44] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:44,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:44] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:44,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:44] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:44,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:44] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:47,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:47] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:47,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:47] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:47,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:47] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:48,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:48] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:38:50,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:50,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:50,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:50,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:50] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:53,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:53,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:53] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:53,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:53] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:56,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:38:56,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:56] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:56,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:56] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:56,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:56] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:38:58,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:58] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:38:59,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:59] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:38:59,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:59] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:38:59,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:38:59] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:01,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:01] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:01,455 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:01] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:01,458 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:01,460 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:01] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:04,442 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:04] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:04,444 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:04] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:04,446 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:04] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:07,432 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:07] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:07,433 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:07] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:07,436 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:07] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:07,438 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:07] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:07,439 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:07] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:39:11,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:11] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:11,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:11] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:11,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:11] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:11,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:11] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:14,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:14] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:14,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:14] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:14,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:14] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:14,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:14] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:17,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:17] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:17,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:17] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:17,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:17] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:18,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:18] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:39:20,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:20] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:20,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:20] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:20,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:20] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:20,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:20] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:23,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:23] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:23,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:23] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:23,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:23] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:23,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:23] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:26,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:26] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:26,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:26] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:26,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:26] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:28,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:28] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:39:29,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:29] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:29,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:29] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:29,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:29] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:29,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:29] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:32,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:32] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:32,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:32] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:32,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:32] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:32,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:32] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:35,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:35,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:35,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:38,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:38] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:38,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:38] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:38,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:38] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:39:38,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:38] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:38,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:38] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:41,127 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:41] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:41,130 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:41] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:41,131 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:41] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:44,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:44] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:44,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:44] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:44,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:44] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:44,070 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:44] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:47,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:47] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:47,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:47] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:47,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:47] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:48,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:48] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:39:48,194 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:48] "GET / HTTP/1.1" 200 -
2026-06-22 06:39:48,958 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:48] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:39:50,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:50,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:50] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:50,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:50,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:53,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:53,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:53] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:53,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:53] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:53,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:53] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:56,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:56] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:56,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:56,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:56] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:57,997 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:57] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:39:59,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:59] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:39:59,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:59] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:39:59,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:59] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:39:59,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:59] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:39:59,398 CRITICAL __main__ [KILL-SWITCH] TRIGGERED: test:dashboard_e2e
2026-06-22 06:39:59,733 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:39:59] "POST /api/kill-switch/test HTTP/1.1" 200 -
2026-06-22 06:39:59,829 INFO __main__ [KILL-SWITCH] monitor thread stopped
2026-06-22 06:39:59,871 CRITICAL __main__ [KILL-SWITCH] loop exiting: test:dashboard_e2e
2026-06-22 06:39:59,871 INFO grid [DEMO] cancelled 3 open BUY order(s) and refunded $19.06; reason=kill_switch:test:dashboard_e2e
2026-06-22 06:39:59,871 WARNING __main__ [KILL-SWITCH] cancelled 3 demo BUY orders
2026-06-22 06:40:01,135 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:40:01,153 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:40:01,170 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:40:02,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:02] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:40:02,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:02] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:40:02,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:02] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:40:02,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:02] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:40:03,509 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:03] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:40:04,262 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:04] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:40:05,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:05] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:40:05,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:05] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:40:05,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:05] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:40:05,826 WARNING __main__ [KILL-SWITCH] RESET - kill event cleared, peak=0
2026-06-22 06:40:05,826 WARNING __main__ [KILL-SWITCH] RESET via /api/kill-switch/reset (manual)
2026-06-22 06:40:06,195 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:06] "POST /api/kill-switch/reset HTTP/1.1" 200 -
2026-06-22 06:40:06,959 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:06] "POST /api/bot/start HTTP/1.1" 200 -
2026-06-22 06:40:06,961 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:40:06,961 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
2026-06-22 06:40:07,650 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-22 06:40:07,651 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,064.60 (bias=range, asymmetry=3:2, sma=$64,161.54)
2026-06-22 06:40:08,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:08] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:40:08,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:08] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:40:08,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:08] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:40:08,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:08] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:40:08,075 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:08] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:40:11,735 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:11] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:40:12,513 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:12] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:40:37,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:40:37,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:40:37,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:40:37,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:40:37,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:40:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:41:28,630 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:28] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:41:28,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:28] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:28,636 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:28] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:41:28,637 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:28] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:28,638 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:28] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:31,435 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:31] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:31,437 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:31] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:31,441 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:31] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:32,252 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET / HTTP/1.1" 200 -
2026-06-22 06:41:32,411 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /static/chartjs-plugin-annotation.min.js HTTP/1.1" 200 -
2026-06-22 06:41:32,413 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /static/chart.umd.min.js HTTP/1.1" 200 -
2026-06-22 06:41:32,622 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:41:32,624 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /api/settings HTTP/1.1" 200 -
2026-06-22 06:41:32,627 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:32,630 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:32,630 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:32,685 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:41:32,690 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /favicon.ico HTTP/1.1" 404 -
2026-06-22 06:41:32,691 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:32] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:41:35,637 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:35,637 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:35,638 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:35,642 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:35] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:41:35,646 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:35] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:41:38,629 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:38] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:41:38,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:38] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:38,633 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:38] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:38,635 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:38] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:41,630 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:41] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:41:41,634 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:41] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:41,635 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:41] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:41:41,637 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:41] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:41,639 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:41] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:42,628 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:42] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:41:44,637 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:44] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:41:44,639 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:44] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:44,643 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:44] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:41:44,645 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:44] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:44,646 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:44] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:49,358 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:49] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:49,359 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:49] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:49,360 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:49] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:41:49,362 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:49] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:49,366 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:49] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:41:50,630 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:50] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:41:50,631 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:50,634 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:50,636 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:52,627 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:52] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:41:53,626 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:53] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:53,627 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:53] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:41:53,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:53,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:53] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:41:53,635 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:53] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:41:56,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:56] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:41:56,633 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:41:56,637 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:56] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:41:56,640 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:56] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:41:56,641 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:41:56] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:00,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:00] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:00,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:00] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:00,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:00] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:00,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:00] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:00,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:00] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:03,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:03] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:03,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:03] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:03,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:03] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:03,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:03] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:03,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:03] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:42:03,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:03] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:06,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:06] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:06,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:06] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:06,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:06] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:06,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:06] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:06,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:06] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:08,626 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:08] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:08,628 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:08] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:08,629 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:08] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:08,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:08] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:11,631 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:11] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:11,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:11] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:11,637 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:11] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:11,638 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:11] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:11,639 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:11] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:12,623 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:12] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:42:14,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:14] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:15,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:15] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:15,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:15] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:15,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:15] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:15,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:15] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:18,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:18] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:18,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:18] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:18,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:18] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:18,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:18] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:18,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:18] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:21,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:21] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:21,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:21] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:21,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:21] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:21,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:21] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:23,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:23] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:42:24,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:24] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:24,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:24] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:24,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:24] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:24,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:24] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:24,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:24] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:27,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:27] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:27,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:27] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:27,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:27] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:27,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:27] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:30,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:30] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:30,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:30] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:30,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:30] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:30,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:30] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:30,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:30] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:32,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:32] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:33,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:33] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:42:33,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:33] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:33,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:33] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:33,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:33] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:36,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:36] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:36,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:36] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:36,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:36] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:36,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:36] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:36,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:36] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:38,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:38] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:39,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:39] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:39,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:39] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:39,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:39] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:39,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:39] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:42,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:42] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:42,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:42] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:42,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:42] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:42,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:42] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:42,994 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:42] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:42:45,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:45] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:45,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:45] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:45,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:45] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:45,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:45] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:45,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:45] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:48,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:48] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:48,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:48] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:48,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:48] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:48,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:48] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:48,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:48] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:51,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:51] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:51,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:51] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:51,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:51] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:51,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:51] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:53,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:53] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:42:54,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:54] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:54,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:54] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:54,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:54] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:54,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:54] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:42:54,031 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:54] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:42:56,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:42:56,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:56] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:42:57,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:57] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:42:57,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:42:57] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:00,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:00] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:00,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:00] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:00,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:00] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:00,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:00] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:00,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:00] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:43:03,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:03] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:03,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:03] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:03,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:03] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:03,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:03] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:43:03,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:03] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:03,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:03] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:43:05,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:05] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:05,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:05] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:06,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:06] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:06,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:06] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:09,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:09] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:09,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:09] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:09,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:09] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:09,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:09] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:43:09,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:09] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:12,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:12] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:12,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:12] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:12,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:12] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:12,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:12] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:12,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:12] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:43:13,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:13] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:43:19,016 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:43:19,016 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:43:37,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:37,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:37,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:37] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:37,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:37,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:43:37,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:43:49,485 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:49] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:49,488 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:49] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:49,491 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:49] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:49,495 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:49] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:43:49,496 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:49] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:49,497 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:49] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:43:50,628 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:50,629 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:50] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:50,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:50,635 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:53,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:52] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:43:53,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:53] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:43:54,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:54] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:43:54,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:54] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:54,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:54] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:54,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:54] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:57,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:57] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:43:57,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:57] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:43:57,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:57] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:43:57,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:57] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:43:57,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:43:57] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:00,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:00] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:00,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:00] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:00,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:00] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:00,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:00] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:03,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:03] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:03,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:03] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:03,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:03] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:03,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:03] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:44:03,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:03] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:03,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:03] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:06,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:06] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:06,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:06] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:06,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:06] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:06,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:06] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:06,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:06] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:08,994 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:08] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:08,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:08] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:08,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:08] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:08,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:08] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:12,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:12] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:12,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:12] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:12,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:12] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:12,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:12] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:12,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:12] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:13,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:13] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:44:15,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:15] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:15,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:15] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:15,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:15] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:15,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:15] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:15,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:15] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:17,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:17] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:17,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:17] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:18,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:18] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:18,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:18] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:21,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:21] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:21,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:21] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:21,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:21] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:21,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:21] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:21,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:21] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:23,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:23] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:44:24,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:24] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:24,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:24] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:24,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:24] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:24,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:24] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:26,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:26] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:26,997 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:26] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:27,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:27] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:27,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:27] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:27,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:27] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:30,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:30] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:30,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:30] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:30,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:30] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:30,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:30] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:30,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:30] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:32,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:32] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:33,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:33] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:33,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:33] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:33,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:33] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:33,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:33] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:44:36,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:36] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:36,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:36] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:36,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:36] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:36,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:36] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:36,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:36] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:39,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:39] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:39,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:39] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:39,026 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:39] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:39,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:39] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:39,029 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:39] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:41,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:41] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:41,997 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:41] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:42,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:42] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:42,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:42] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:43,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:43] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:44:44,994 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:44] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:44,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:44] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:45,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:45] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:45,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:45] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:45,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:45] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:48,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:48] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:48,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:48] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:48,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:48] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:44:48,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:48] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:48,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:48] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:44:51,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:51] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:44:51,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:51] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:44:51,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:51] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:44:51,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:44:51] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:45:01,297 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:45:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:45:01,320 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:45:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:45:01,342 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:45:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:45:37,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:45:37] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:45:37,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:45:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:45:37,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:45:37] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:45:37,027 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:45:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:45:37,028 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:45:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:45:37,075 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:45:37] "GET /api/kronos HTTP/1.1" 200 -
+15
View File
@@ -0,0 +1,15 @@
2026-06-22 06:39:14,521 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:39:14,521 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:39:14,521 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:39:14,521 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:39:14,522 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:39:14,522 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:39:14,522 INFO __main__ [main] trading_loop thread started: 139987624392384, alive=True
2026-06-22 06:39:14,523 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:39:14,523 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:39:14,537 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
Address already in use
2026-06-22 06:39:14,539 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
Port 5555 is in use by another program. Either identify and stop that program, or start the server with a different port.
+211
View File
@@ -0,0 +1,211 @@
2026-06-22 06:46:02,301 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:46:02,302 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:46:02,302 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:46:02,302 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:46:02,302 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:46:02,303 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:46:02,303 INFO __main__ [main] trading_loop thread started: 131873664661184, alive=True
2026-06-22 06:46:02,304 INFO __main__ [KILL-SWITCH] monitor thread running
* Serving Flask app 'grid-bot'
2026-06-22 06:46:02,304 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
* Debug mode: off
2026-06-22 06:46:02,321 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:46:02,321 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
2026-06-22 06:46:02,333 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-22 06:46:02,334 INFO werkzeug Press CTRL+C to quit
2026-06-22 06:46:02,639 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:02] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:02,642 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:02] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:02,643 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:02] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:02,646 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:02] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:46:02,652 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:02] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:03,262 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-22 06:46:03,472 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:46:03,472 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:46:03,473 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,064.60 (bias=range, asymmetry=3:2, sma=$64,161.54)
2026-06-22 06:46:05,635 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:05] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:05,639 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:05] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:05,640 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:05] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:05,641 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:05] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:05,645 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:05] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:08,620 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:08] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:08,626 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:08] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:08,631 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:08] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:08,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:08] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:11,623 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:11] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:11,625 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:11] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:11,628 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:11] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:11,629 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:11] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:11,630 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:11] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:12,622 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:12] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:46:14,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:14] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:14,635 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:14] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:14,638 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:14] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:14,640 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:14] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:14,641 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:14] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:17,623 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:17] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:17,627 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:17] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:17,632 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:17] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:17,634 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:17] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:20,622 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:20] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:20,627 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:20] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:20,629 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:20] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:20,630 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:20] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:20,633 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:20] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:22,622 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:22] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:46:23,630 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:23] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:23,633 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:23] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:23,636 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:23] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:23,637 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:23] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:23,642 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:23] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:26,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:26] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:27,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:27] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:27,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:27] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:27,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:27] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:27,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:27] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:29,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:29] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:30,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:30] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:30,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:30] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:30,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:30] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:30,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:30] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:33,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:33] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:33,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:33] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:33,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:33] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:33,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:33] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:33,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:33] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:46:33,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:33] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:35,992 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:35] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:35,993 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:35] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:35,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:35] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:35,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:35] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:38,997 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:38] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:39,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:38] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:39,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:39] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:39,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:39] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:39,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:39] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:42,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:42] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:42,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:42] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:42,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:42] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:42,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:42] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:42,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:42] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:43,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:43] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:46:45,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:45] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:45,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:45] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:45,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:45] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:45,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:45] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:48,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:48] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:48,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:48] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:48,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:48] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:48,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:48] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:48,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:48] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:49,481 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:49] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:51,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:51] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:51,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:51] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:51,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:51] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:51,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:51] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:52,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:52] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:46:53,994 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:53,997 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:53] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:53,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:53] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:54,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:54] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:54,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:54] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:56,966 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:56] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:57,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:57] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:57,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:57] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:57,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:57] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:46:57,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:57] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:46:57,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:57] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:46:59,994 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:59] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:46:59,995 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:59] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:46:59,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:46:59] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:00,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:00] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:03,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:03] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:03,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:03] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:03,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:03] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:03,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:03] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:03,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:03] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:03,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:03] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:47:06,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:06] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:06,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:06] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:06,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:06] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:06,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:06] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:06,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:06] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:08,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:08] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:09,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:09] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:09,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:09] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:09,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:09] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:11,231 CRITICAL __main__ [KILL-SWITCH] TRIGGERED: test:final_demo_test
2026-06-22 06:47:11,558 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:11] "POST /api/kill-switch/test HTTP/1.1" 200 -
2026-06-22 06:47:11,740 CRITICAL __main__ [KILL-SWITCH] loop exiting (mode=DEMO): test:final_demo_test
2026-06-22 06:47:11,740 INFO grid [DEMO] cancelled 3 open BUY order(s) and refunded $19.06; reason=kill_switch:test:final_demo_test
2026-06-22 06:47:11,740 WARNING __main__ [KILL-SWITCH] cancelled 3 demo BUY orders
2026-06-22 06:47:12,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:12] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:12,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:12] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:12,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:12] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:12,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:12] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:12,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:12] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:13,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:13] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:47:14,995 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:14] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:14,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:14] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:14,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:14] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:15,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:15] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:15,343 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:15] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:16,178 WARNING __main__ [KILL-SWITCH] RESET - kill event cleared, peak=0
2026-06-22 06:47:16,179 WARNING __main__ [KILL-SWITCH] RESET via /api/kill-switch/reset (manual)
2026-06-22 06:47:16,451 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:16] "POST /api/kill-switch/reset HTTP/1.1" 200 -
2026-06-22 06:47:16,471 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:16] "POST /api/bot/start HTTP/1.1" 200 -
2026-06-22 06:47:16,474 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:47:16,475 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
2026-06-22 06:47:17,252 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-22 06:47:17,252 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,035.50 (bias=range, asymmetry=3:2, sma=$64,161.54)
2026-06-22 06:47:17,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:17] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:18,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:18] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:18,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:18] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:18,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:18] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:18,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:18] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:20,623 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:20] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:20,625 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:20] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:20,626 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:20] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:20,628 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:20] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:22,626 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:22] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:47:23,631 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:23] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:23,633 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:23] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:23,636 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:23] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:23,638 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:23] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:23,640 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:23] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:26,992 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:26] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:26,995 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:26] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:26,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:26] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:27,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:27] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:27,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:27] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:30,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:30] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:30,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:30] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:30,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:30] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:30,019 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:30] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:30,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:30] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:32,633 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:32] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:32,635 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:32] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:32,638 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:32] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:47:32,641 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:32] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:32,643 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:32] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:33,151 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET / HTTP/1.1" 200 -
2026-06-22 06:47:33,307 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET /static/chart.umd.min.js HTTP/1.1" 304 -
2026-06-22 06:47:33,308 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET /static/chartjs-plugin-annotation.min.js HTTP/1.1" 304 -
2026-06-22 06:47:33,388 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET /api/settings HTTP/1.1" 200 -
2026-06-22 06:47:33,390 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:33,391 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:33,395 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:33,396 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:33,397 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:47:33,452 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:36,403 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:36] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:36,406 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:36] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:36,408 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:36] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:36,411 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:36] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:36,413 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:36] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:39,391 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:39] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:39,396 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:39] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:39,397 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:39] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:39,400 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:39] "GET /api/logs HTTP/1.1" 200 -
+118
View File
@@ -0,0 +1,118 @@
2026-06-22 06:47:52,035 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:47:52,036 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:47:52,036 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:47:52,036 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:47:52,036 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:47:52,037 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:47:52,037 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:47:52,037 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
2026-06-22 06:47:52,037 INFO __main__ [main] trading_loop thread started: 123421766710976, alive=True
2026-06-22 06:47:52,049 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:47:52,049 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:47:52,069 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-22 06:47:52,069 INFO werkzeug Press CTRL+C to quit
2026-06-22 06:47:53,008 INFO __main__ [DEBUG] state.demo_mode=True resolved demo_mode=True branch=DEMO
2026-06-22 06:47:53,292 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:47:53,293 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:47:53,293 INFO __main__ [DEMO] Placed 3 BUY / 0 SELL at price $64,035.50 (bias=range, asymmetry=3:2, sma=$64,161.54)
2026-06-22 06:47:54,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:54] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:47:55,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:55] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:47:55,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:55] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:55,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:55] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:47:55,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:55] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:55,025 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:55] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:57,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:57] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:47:57,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:57] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:47:58,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:58] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:47:58,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:58] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:01,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:01] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:01,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:01,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:01] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:01,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:01] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:01,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:01] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:04,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:04] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:04,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:04] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:48:04,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:04] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:04,017 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:04] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:04,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:04] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:07,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:07] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:07,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:07] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:07,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:07] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:07,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:07] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:07,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:07] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:10,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:10] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:10,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:10] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:10,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:10] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:10,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:10] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:10,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:10] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:13,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:13] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:13,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:13] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:13,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:13] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:13,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:13] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:13,021 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:13] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:14,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:14] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:48:15,993 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:15] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:16,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:16] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:16,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:16] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:16,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:16] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:18,994 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:18] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:18,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:18] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:18,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:18] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:19,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:19] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:19,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:19] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:22,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:22] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:22,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:22] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:22,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:22] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:22,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:22] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:22,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:22] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:23,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:23] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:48:24,994 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:24] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:24,997 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:24] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:25,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:25] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:25,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:25] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:28,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:28] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:28,015 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:28] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:28,018 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:28] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:28,022 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:28] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:28,023 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:28] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:31,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:31] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:31,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:31] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:31,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:31] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:31,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:31] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:31,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:31] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:34,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:34] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:34,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:34] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:34,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:34] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:34,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:34] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:34,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:34] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:48:37,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:37] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:37,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:37,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:37,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:37,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:40,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:40] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:48:40,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:40] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:48:40,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:40] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:48:40,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:40] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:48:40,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:48:40] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:48:50,448 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $62557.50 (fake_id=1782110930443)
2026-06-22 06:48:50,449 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $62878.31 (fake_id=1782110930445)
2026-06-22 06:48:50,449 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $63199.11 (fake_id=1782110930446)
2026-06-22 06:48:50,449 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $63519.92 (fake_id=1782110930447)
2026-06-22 06:48:50,449 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $63840.73 (fake_id=1782110930448)
2026-06-22 06:48:50,449 INFO __main__ [LIVE] Placed 5 BUY orders, 0 failed (price ~64161.54)
2026-06-22 06:48:50,738 INFO __main__ Wallet refreshed: 3 currencies, USDT=72.5052
2026-06-22 06:48:50,739 CRITICAL __main__ [KILL-SWITCH] TRIGGERED: drawdown[live]:79.7%>=10.0%
2026-06-22 06:48:51,053 CRITICAL __main__ [KILL-SWITCH] loop exiting (mode=LIVE): drawdown[live]:79.7%>=10.0%
2026-06-22 06:48:51,054 INFO __main__ [DRY-RUN] would cancel order 1782110930443 (BUY @ 62557.50)
2026-06-22 06:48:51,054 INFO __main__ [DRY-RUN] would cancel order 1782110930445 (BUY @ 62878.31)
2026-06-22 06:48:51,054 INFO __main__ [DRY-RUN] would cancel order 1782110930446 (BUY @ 63199.11)
2026-06-22 06:48:51,054 INFO __main__ [DRY-RUN] would cancel order 1782110930447 (BUY @ 63519.92)
2026-06-22 06:48:51,054 INFO __main__ [DRY-RUN] would cancel order 1782110930448 (BUY @ 63840.73)
2026-06-22 06:48:51,055 INFO __main__ [LIVE] cancel_all: cancelled=5, errors=0
2026-06-22 06:48:51,055 CRITICAL __main__ [KILL-SWITCH] live cancelled=0, position_closed=False, reason=drawdown[live]:79.7%>=10.0%
+15
View File
@@ -0,0 +1,15 @@
2026-06-22 06:48:27,712 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:48:27,712 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:48:27,712 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:48:27,712 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:48:27,712 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:48:27,713 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:48:27,714 INFO __main__ [main] trading_loop thread started: 128339379484352, alive=True
2026-06-22 06:48:27,714 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:48:27,714 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
2026-06-22 06:48:27,723 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:48:27,723 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
* Serving Flask app 'grid-bot'
* Debug mode: off
Address already in use
Port 5555 is in use by another program. Either identify and stop that program, or start the server with a different port.
+234
View File
@@ -0,0 +1,234 @@
2026-06-22 06:49:13,897 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:49:13,897 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:49:13,897 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:49:13,897 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:49:13,897 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:49:13,898 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:49:13,898 INFO __main__ [main] trading_loop thread started: 123787669403328, alive=True
2026-06-22 06:49:13,899 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:49:13,899 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
2026-06-22 06:49:13,908 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:49:13,909 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
* Serving Flask app 'grid-bot'
* Debug mode: off
2026-06-22 06:49:13,917 INFO werkzeug WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5555
* Running on http://46.29.164.31:5555
2026-06-22 06:49:13,918 INFO werkzeug Press CTRL+C to quit
2026-06-22 06:49:14,333 INFO __main__ Wallet refreshed: 3 currencies, USDT=72.5052
2026-06-22 06:49:14,905 INFO __main__ [DEBUG] state.demo_mode=False resolved demo_mode=False branch=LIVE
2026-06-22 06:49:15,346 INFO __main__ Candles refreshed: tf=60min, 140 candles
2026-06-22 06:49:15,347 INFO __main__ [SMA] SMA(24, 60m) = $64,161.54 (140 candles)
2026-06-22 06:49:15,347 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $62557.50 (fake_id=1782110955342)
2026-06-22 06:49:15,347 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $62878.31 (fake_id=1782110955343)
2026-06-22 06:49:15,347 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $63199.11 (fake_id=1782110955344)
2026-06-22 06:49:15,347 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $63519.92 (fake_id=1782110955345)
2026-06-22 06:49:15,347 INFO __main__ [DRY-RUN] would place BUY 1e-05 BTC @ $63840.73 (fake_id=1782110955346)
2026-06-22 06:49:15,347 INFO __main__ [LIVE] Placed 5 BUY orders, 0 failed (price ~64161.54)
2026-06-22 06:49:15,398 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:15] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:15,402 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:15] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:15,404 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:15] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:16,025 INFO __main__ Account info refreshed: brief_nm=451903, main_curr=USDT
2026-06-22 06:49:16,160 INFO __main__ Quote info refreshed: lot=0.00001000, min_step=0.10000000
2026-06-22 06:49:16,160 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:16] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:18,401 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:18] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:18,405 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:18] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:18,409 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:18] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:18,413 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:18] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:18,415 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:18] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:49:21,398 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:21] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:21,403 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:21] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:21,406 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:21] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:21,407 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:21] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:23,389 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:23] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:49:24,403 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:24] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:24,405 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:24] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:24,407 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:24] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:24,411 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:24] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:24,411 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:24] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:49:27,393 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:27] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:27,396 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:27] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:27,397 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:27] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:27,398 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:27] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:30,395 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:30] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:30,396 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:30] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:30,399 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:30] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:30,402 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:30] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:30,411 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:30] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:49:34,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:34] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:34,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:34] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:34,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:34] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:34,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:34] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:34,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:34] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:49:34,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:34] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:49:36,994 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:36] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:36,997 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:36] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:36,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:36] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:37,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:39,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:39] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:39,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:39] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:40,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:40] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:40,003 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:40] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:40,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:40] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:49:42,997 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:42] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:43,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:43] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:43,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:43] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:43,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:43] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:44,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:44] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:49:44,626 INFO __main__ Wallet refreshed: 3 currencies, USDT=72.5052
2026-06-22 06:49:45,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:45] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:49:45,996 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:45] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:45,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:45] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:45,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:45] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:46,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:46] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:48,997 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:48] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:49,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:49] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:49,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:49] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:49,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:49] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:49,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:49] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:49:52,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:52] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:52,006 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:52] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:52,007 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:52] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:52,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:52] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:49:52,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:52] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:54,000 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:54] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:49:55,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:55] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:55,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:55] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:55,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:55] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:55,013 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:55] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:49:57,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:57] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:49:58,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:58] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:49:58,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:58] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:49:58,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:58] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:49:58,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:49:58] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:00,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:00] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:00,999 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:00] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:01,002 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:01,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:01] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:01,492 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:01] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:01,511 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:01] "GET /api/_debug/demo_orders HTTP/1.1" 200 -
2026-06-22 06:50:01,529 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:01] "GET /api/logs?limit=5 HTTP/1.1" 200 -
2026-06-22 06:50:03,998 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:03] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:04,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:04] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:04,004 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:04] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:04,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:04] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:04,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:04] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:50:04,010 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:04] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:06,393 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:06] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:06,395 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:06] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:06,398 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:06] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:06,399 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:06] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:09,401 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:09] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:09,405 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:09] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:09,406 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:09] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:09,412 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:09] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:09,413 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:09] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:10,196 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:10] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:10,960 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:10] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:12,392 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:12] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:12,397 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:12] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:12,398 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:12] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:12,399 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:12] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:13,399 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:13] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:50:15,399 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:15] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:15,402 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:15] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:15,407 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:15] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:15,408 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:15] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:15,562 INFO __main__ Wallet refreshed: 3 currencies, USDT=72.5052
2026-06-22 06:50:15,562 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:15] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:15,692 INFO __main__ Wallet refreshed: 3 currencies, USDT=72.5052
2026-06-22 06:50:18,392 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:18] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:18,393 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:18] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:18,396 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:18] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:18,398 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:18] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:19,523 CRITICAL __main__ [KILL-SWITCH] TRIGGERED: test:live_dryrun_test
2026-06-22 06:50:19,863 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:19] "POST /api/kill-switch/test HTTP/1.1" 200 -
2026-06-22 06:50:20,358 CRITICAL __main__ [KILL-SWITCH] loop exiting (mode=LIVE): test:live_dryrun_test
2026-06-22 06:50:20,358 INFO __main__ [DRY-RUN] would cancel order 1782110955342 (BUY @ 62557.50)
2026-06-22 06:50:20,358 INFO __main__ [DRY-RUN] would cancel order 1782110955343 (BUY @ 62878.31)
2026-06-22 06:50:20,359 INFO __main__ [DRY-RUN] would cancel order 1782110955344 (BUY @ 63199.11)
2026-06-22 06:50:20,359 INFO __main__ [DRY-RUN] would cancel order 1782110955345 (BUY @ 63519.92)
2026-06-22 06:50:20,359 INFO __main__ [DRY-RUN] would cancel order 1782110955346 (BUY @ 63840.73)
2026-06-22 06:50:20,359 INFO __main__ [LIVE] cancel_all: cancelled=5, errors=0
2026-06-22 06:50:20,359 CRITICAL __main__ [KILL-SWITCH] live cancelled=0, position_closed=False, reason=test:live_dryrun_test
2026-06-22 06:50:21,403 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:21] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:21,405 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:21] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:21,409 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:21] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:21,412 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:21] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:21,412 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:21] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:23,401 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:23] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:50:23,637 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:23] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:24,397 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:24] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:24,401 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:24] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:24,401 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:24] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:24,405 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:24] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:27,393 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:27] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:27,394 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:27] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:27,396 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:27] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:27,400 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:27] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:27,401 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:27] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:30,391 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:30] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:30,394 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:30] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:30,396 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:30] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:30,397 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:30] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:30,836 INFO __main__ [KILL-SWITCH] monitor thread stopped
2026-06-22 06:50:33,395 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:33] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:33,397 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:33] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:33,398 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:33] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:33,399 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:33] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:33,399 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:33] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:33,401 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:33] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:50:37,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:37] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:37,009 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:37] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:37,011 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:37] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:37,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:37] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:37,016 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:37] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:40,005 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:40] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:40,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:40] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:40,012 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:40] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:40,014 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:40] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:42,388 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:42] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:42,391 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:42] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:42,396 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:42] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:42,397 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:42] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:42,401 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:42] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:43,389 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:43] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:50:45,400 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:45] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:45,402 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:45] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:45,404 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:45] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:45,409 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:45] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:45,411 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:45] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:46,248 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET / HTTP/1.1" 200 -
2026-06-22 06:50:46,400 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET /static/chartjs-plugin-annotation.min.js HTTP/1.1" 304 -
2026-06-22 06:50:46,402 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET /static/chart.umd.min.js HTTP/1.1" 304 -
2026-06-22 06:50:46,469 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET /api/settings HTTP/1.1" 200 -
2026-06-22 06:50:46,471 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:46,475 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:46,476 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:46,478 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:50:46,529 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:46,742 INFO __main__ Wallet refreshed: 3 currencies, USDT=72.5052
2026-06-22 06:50:46,743 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:47,666 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET / HTTP/1.1" 200 -
2026-06-22 06:50:47,811 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET /static/chart.umd.min.js HTTP/1.1" 304 -
2026-06-22 06:50:47,812 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET /static/chartjs-plugin-annotation.min.js HTTP/1.1" 304 -
2026-06-22 06:50:47,878 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:47,884 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET /api/settings HTTP/1.1" 200 -
2026-06-22 06:50:47,885 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET /api/price-history?range=1h HTTP/1.1" 200 -
2026-06-22 06:50:47,886 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:47,888 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:47,889 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:47,938 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:50,884 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:50] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:50,887 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:50] "GET /api/logs HTTP/1.1" 200 -
2026-06-22 06:50:50,888 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:50] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:50,891 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:50] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:50,892 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:50] "GET /api/kronos HTTP/1.1" 200 -
2026-06-22 06:50:51,030 WARNING __main__ [KILL-SWITCH] RESET - kill event cleared, peak=0
2026-06-22 06:50:51,031 WARNING __main__ [KILL-SWITCH] RESET via /api/kill-switch/reset (manual)
2026-06-22 06:50:51,581 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:51] "POST /api/kill-switch/reset HTTP/1.1" 200 -
2026-06-22 06:50:53,882 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:53] "GET /api/status HTTP/1.1" 200 -
2026-06-22 06:50:53,886 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:53] "GET /api/grid HTTP/1.1" 200 -
2026-06-22 06:50:53,888 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:53] "GET /api/kill-switch/status HTTP/1.1" 200 -
2026-06-22 06:50:53,889 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:53] "GET /api/logs HTTP/1.1" 200 -
+15
View File
@@ -0,0 +1,15 @@
2026-06-22 06:49:49,314 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:49:49,314 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:49:49,314 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:49:49,314 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:49:49,314 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:49:49,315 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:49:49,316 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:49:49,317 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
2026-06-22 06:49:49,318 INFO __main__ [main] trading_loop thread started: 131498090952384, alive=True
2026-06-22 06:49:49,319 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:49:49,319 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
* Serving Flask app 'grid-bot'
* Debug mode: off
Address already in use
Port 5555 is in use by another program. Either identify and stop that program, or start the server with a different port.
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
2026-06-22 06:51:39,812 INFO __main__ Grid Bot starting — mode=DEMO
2026-06-22 06:51:39,812 INFO __main__ Tradernet API: https://tradernet.by
2026-06-22 06:51:39,812 INFO __main__ Trading pair: BTC-USDT.IMEX
2026-06-22 06:51:39,813 INFO __main__ Starting web dashboard on 0.0.0.0:5555
2026-06-22 06:51:39,813 INFO __main__ DEMO mode → auto-starting trading loop
2026-06-22 06:51:39,813 INFO __main__ [bot-thread] starting trading_loop...
2026-06-22 06:51:39,814 INFO __main__ [KILL-SWITCH] monitor thread running
2026-06-22 06:51:39,815 INFO __main__ [KILL-SWITCH] started (DD>10.0%, 5err/600s, TG every 30s)
2026-06-22 06:51:39,816 INFO __main__ [main] trading_loop thread started: 126881130084032, alive=True
2026-06-22 06:51:39,824 INFO __main__ Restored 11 trades from trade_history.json, balance=375.56
2026-06-22 06:51:39,824 INFO __main__ [FEE-VALIDATOR] step=0.500% vs required=0.500% (round-trip fee 0.400% × safety 1.25) → margin +0.0%
* Serving Flask app 'grid-bot'
* Debug mode: off
Address already in use
Port 5555 is in use by another program. Either identify and stop that program, or start the server with a different port.
File diff suppressed because it is too large Load Diff
+236
View File
@@ -0,0 +1,236 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
# === Fee-coverage validator (QuantDinger validator pattern) ===
# Оценочная комиссия биржи Cifra Markets за сделку. Не подтверждена
# официально (см. MEMORY.md). 0.2% — безопасная оценка для BTC.
FEE_RATE_PERCENT = float(os.getenv("FEE_RATE_PERCENT", "0.2")) # 0.2% = 0.002
# Множитель запаса: round-trip fee * safety = минимально допустимый
# шаг сетки. QuantDinger default = 1.25.
FEE_SAFETY_MULTIPLIER = float(os.getenv("FEE_SAFETY_MULTIPLIER", "1.25"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# Режим построения сетки: 'arithmetic' (равные $шаги, по умолчанию)
# или 'geometric' (равные %шаги, не зависит от цены — точнее для
# широких диапазонов BTC).
GRID_MODE = os.getenv("GRID_MODE", "arithmetic").lower().strip()
# === State.json versioning (2026-06-30) ===
# При изменении grid-параметров в .env (step/tp/levels) - bump на новую
# версию. load_state() в main.py делает auto-merge при несовпадении версии:
# подтягивает свежие grid-параметры из .env, сохраняет runtime (position,
# balance, trade_log, demo_mode, symbol).
# История версий:
# "1.0" - оригинал, без version (читается как "0.0")
# "1.1" - fee-aware step: GRID_STEP_PERCENT по умолчанию 0.020 (2.0%)
GRID_STATE_VERSION = os.getenv("GRID_STATE_VERSION", "1.1")
# Какие ключи подтягиваются из .env при merge (runtime-state не трогаем)
GRID_CONFIG_KEYS = ("step_percent", "take_profit_percent", "grid_levels")
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === SMA-ATR strategy (2026-06-21) ===
# Replaces "grid_center = current_price" with SMA-anchored grid + lock FSM.
# Lock triggers when |price - sma| > LOCK_PCT%, unlock when < UNLOCK_PCT%.
# Bias source for asymmetry (3:2 split): Kronos primary, TREND fallback.
STRATEGY_CENTER_MODE = os.getenv("STRATEGY_CENTER_MODE", "current_price") # "sma" | "current_price"
STRATEGY_SMA_TF_MIN = int(os.getenv("STRATEGY_SMA_TF_MIN", "60")) # 60 = 1h
STRATEGY_SMA_PERIOD = int(os.getenv("STRATEGY_SMA_PERIOD", "24"))
STRATEGY_LOCK_PCT = float(os.getenv("STRATEGY_LOCK_PCT", "2.0")) # |dev| > 2% → lock
STRATEGY_UNLOCK_PCT = float(os.getenv("STRATEGY_UNLOCK_PCT", "1.5")) # |dev| < 1.5% → unlock
STRATEGY_BIAS_SOURCE = os.getenv("STRATEGY_BIAS_SOURCE", "kronos") # "kronos" | "trend" | "none"
STRATEGY_BIAS_FALLBACK = os.getenv("STRATEGY_BIAS_FALLBACK", "trend") # used when primary is unavailable
STRATEGY_SMA_CACHE_SEC = int(os.getenv("STRATEGY_SMA_CACHE_SEC", "300")) # recompute SMA every 5 min
STRATEGY_LOCK_COOLDOWN_SEC = int(os.getenv("STRATEGY_LOCK_COOLDOWN_SEC", "900")) # 15 min between lock state changes
STRATEGY_ASYMMETRY_UP = os.getenv("STRATEGY_ASYMMETRY_UP", "3:2") # bias=up → 3 BUY / 2 SELL
STRATEGY_ASYMMETRY_DOWN = os.getenv("STRATEGY_ASYMMETRY_DOWN", "2:3") # bias=down → 2 BUY / 3 SELL
STRATEGY_ASYMMETRY_RANGE = os.getenv("STRATEGY_ASYMMETRY_RANGE", "3:2") # bias=range → 3 BUY / 2 SELL
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Kill switch (2026-06-22) ===
# Защита для live-режима: автоматически останавливает бота при:
# 1. Drawdown > KILL_DD_PCT% от пикового баланса (balance + unrealized position)
# 2. > KILL_API_ERRORS ошибок API за KILL_API_WINDOW_SEC секунд
# 3. Telegram /stop команда от TELEGRAM_CHAT_ID (long-poll каждые KILL_TG_POLL_SEC)
# При срабатывании: cancel all open orders (для live - через _live_cancel_all_pending),
# bot_running=False, требует ручного /api/bot/start для resume.
# KILL_SWITCH_ENABLED=false - весь модуль no-op (для отладки).
KILL_SWITCH_ENABLED = os.getenv("KILL_SWITCH_ENABLED", "true").lower() in ("true", "1", "yes")
KILL_DD_PCT = float(os.getenv("KILL_DD_PCT", "10.0"))
KILL_API_ERRORS = int(os.getenv("KILL_API_ERRORS", "5"))
KILL_API_WINDOW_SEC = int(os.getenv("KILL_API_WINDOW_SEC", "600"))
KILL_TG_POLL_SEC = int(os.getenv("KILL_TG_POLL_SEC", "30"))
KILL_TG_LONG_POLL = int(os.getenv("KILL_TG_LONG_POLL", "25"))
# === Live-mode kill switch (2026-06-22) ===
# В live режиме kill switch дополнительно может:
# - LIVE_KILL_CLOSE_POSITION=true: закрыть открытую позицию market-sell при trigger
# (опасно в falling knife - оставь false для консервативного старта)
# - LIVE_KILL_DD_PCT: если задан (>0), переопределяет KILL_DD_PCT для live
# (например, можно осторожнее: 7% для live вместо 10% для demo)
LIVE_KILL_CLOSE_POSITION = os.getenv("LIVE_KILL_CLOSE_POSITION", "false").lower() in ("true", "1", "yes")
_live_dd_env = os.getenv("LIVE_KILL_DD_PCT", "")
LIVE_KILL_DD_PCT = float(_live_dd_env) if _live_dd_env else 0.0 # 0 = use KILL_DD_PCT
# === Reconcile (QuantDinger ledger_reconcile pattern) ===
# Периодическая сверка demo.position с trade_log: ловит
# "зависшие" позиции, открытые ДО последнего TP/SELL-fill.
# 0 = выключено. 300 = каждые 5 мин (рекомендация QuantDinger).
RECONCILE_INTERVAL_SEC = int(os.getenv("RECONCILE_INTERVAL_SEC", "300"))
# Если найдена shadow-позиция: 'log' (только предупреждение, безопасно)
# или 'clear' (auto-clear). По умолчанию log.
RECONCILE_AUTO_CLEAR = os.getenv("RECONCILE_AUTO_CLEAR", "log").lower() in ("true", "1", "yes")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+102
View File
@@ -0,0 +1,102 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Log ===
LOG_FILE = "margin-bot.log"
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
+119
View File
@@ -0,0 +1,119 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "12"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.5"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
@@ -0,0 +1,113 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Log ===
LOG_FILE = "margin-bot.log"
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
+199
View File
@@ -0,0 +1,199 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
# === Fee-coverage validator (QuantDinger validator pattern) ===
# Оценочная комиссия биржи Cifra Markets за сделку. Не подтверждена
# официально (см. MEMORY.md). 0.2% — безопасная оценка для BTC.
FEE_RATE_PERCENT = float(os.getenv("FEE_RATE_PERCENT", "0.2")) # 0.2% = 0.002
# Множитель запаса: round-trip fee * safety = минимально допустимый
# шаг сетки. QuantDinger default = 1.25.
FEE_SAFETY_MULTIPLIER = float(os.getenv("FEE_SAFETY_MULTIPLIER", "1.25"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# Режим построения сетки: 'arithmetic' (равные $шаги, по умолчанию)
# или 'geometric' (равные %шаги, не зависит от цены — точнее для
# широких диапазонов BTC).
GRID_MODE = os.getenv("GRID_MODE", "arithmetic").lower().strip()
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === SMA-ATR strategy (2026-06-21) ===
# Replaces "grid_center = current_price" with SMA-anchored grid + lock FSM.
# Lock triggers when |price - sma| > LOCK_PCT%, unlock when < UNLOCK_PCT%.
# Bias source for asymmetry (3:2 split): Kronos primary, TREND fallback.
STRATEGY_CENTER_MODE = os.getenv("STRATEGY_CENTER_MODE", "current_price") # "sma" | "current_price"
STRATEGY_SMA_TF_MIN = int(os.getenv("STRATEGY_SMA_TF_MIN", "60")) # 60 = 1h
STRATEGY_SMA_PERIOD = int(os.getenv("STRATEGY_SMA_PERIOD", "24"))
STRATEGY_LOCK_PCT = float(os.getenv("STRATEGY_LOCK_PCT", "2.0")) # |dev| > 2% → lock
STRATEGY_UNLOCK_PCT = float(os.getenv("STRATEGY_UNLOCK_PCT", "1.5")) # |dev| < 1.5% → unlock
STRATEGY_BIAS_SOURCE = os.getenv("STRATEGY_BIAS_SOURCE", "kronos") # "kronos" | "trend" | "none"
STRATEGY_BIAS_FALLBACK = os.getenv("STRATEGY_BIAS_FALLBACK", "trend") # used when primary is unavailable
STRATEGY_SMA_CACHE_SEC = int(os.getenv("STRATEGY_SMA_CACHE_SEC", "300")) # recompute SMA every 5 min
STRATEGY_LOCK_COOLDOWN_SEC = int(os.getenv("STRATEGY_LOCK_COOLDOWN_SEC", "900")) # 15 min between lock state changes
STRATEGY_ASYMMETRY_UP = os.getenv("STRATEGY_ASYMMETRY_UP", "3:2") # bias=up → 3 BUY / 2 SELL
STRATEGY_ASYMMETRY_DOWN = os.getenv("STRATEGY_ASYMMETRY_DOWN", "2:3") # bias=down → 2 BUY / 3 SELL
STRATEGY_ASYMMETRY_RANGE = os.getenv("STRATEGY_ASYMMETRY_RANGE", "3:2") # bias=range → 3 BUY / 2 SELL
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Reconcile (QuantDinger ledger_reconcile pattern) ===
# Периодическая сверка demo.position с trade_log: ловит
# "зависшие" позиции, открытые ДО последнего TP/SELL-fill.
# 0 = выключено. 300 = каждые 5 мин (рекомендация QuantDinger).
RECONCILE_INTERVAL_SEC = int(os.getenv("RECONCILE_INTERVAL_SEC", "300"))
# Если найдена shadow-позиция: 'log' (только предупреждение, безопасно)
# или 'clear' (auto-clear). По умолчанию log.
RECONCILE_AUTO_CLEAR = os.getenv("RECONCILE_AUTO_CLEAR", "log").lower() in ("true", "1", "yes")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
@@ -0,0 +1,214 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
# === Fee-coverage validator (QuantDinger validator pattern) ===
# Оценочная комиссия биржи Cifra Markets за сделку. Не подтверждена
# официально (см. MEMORY.md). 0.2% — безопасная оценка для BTC.
FEE_RATE_PERCENT = float(os.getenv("FEE_RATE_PERCENT", "0.2")) # 0.2% = 0.002
# Множитель запаса: round-trip fee * safety = минимально допустимый
# шаг сетки. QuantDinger default = 1.25.
FEE_SAFETY_MULTIPLIER = float(os.getenv("FEE_SAFETY_MULTIPLIER", "1.25"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# Режим построения сетки: 'arithmetic' (равные $шаги, по умолчанию)
# или 'geometric' (равные %шаги, не зависит от цены — точнее для
# широких диапазонов BTC).
GRID_MODE = os.getenv("GRID_MODE", "arithmetic").lower().strip()
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === SMA-ATR strategy (2026-06-21) ===
# Replaces "grid_center = current_price" with SMA-anchored grid + lock FSM.
# Lock triggers when |price - sma| > LOCK_PCT%, unlock when < UNLOCK_PCT%.
# Bias source for asymmetry (3:2 split): Kronos primary, TREND fallback.
STRATEGY_CENTER_MODE = os.getenv("STRATEGY_CENTER_MODE", "current_price") # "sma" | "current_price"
STRATEGY_SMA_TF_MIN = int(os.getenv("STRATEGY_SMA_TF_MIN", "60")) # 60 = 1h
STRATEGY_SMA_PERIOD = int(os.getenv("STRATEGY_SMA_PERIOD", "24"))
STRATEGY_LOCK_PCT = float(os.getenv("STRATEGY_LOCK_PCT", "2.0")) # |dev| > 2% → lock
STRATEGY_UNLOCK_PCT = float(os.getenv("STRATEGY_UNLOCK_PCT", "1.5")) # |dev| < 1.5% → unlock
STRATEGY_BIAS_SOURCE = os.getenv("STRATEGY_BIAS_SOURCE", "kronos") # "kronos" | "trend" | "none"
STRATEGY_BIAS_FALLBACK = os.getenv("STRATEGY_BIAS_FALLBACK", "trend") # used when primary is unavailable
STRATEGY_SMA_CACHE_SEC = int(os.getenv("STRATEGY_SMA_CACHE_SEC", "300")) # recompute SMA every 5 min
STRATEGY_LOCK_COOLDOWN_SEC = int(os.getenv("STRATEGY_LOCK_COOLDOWN_SEC", "900")) # 15 min between lock state changes
STRATEGY_ASYMMETRY_UP = os.getenv("STRATEGY_ASYMMETRY_UP", "3:2") # bias=up → 3 BUY / 2 SELL
STRATEGY_ASYMMETRY_DOWN = os.getenv("STRATEGY_ASYMMETRY_DOWN", "2:3") # bias=down → 2 BUY / 3 SELL
STRATEGY_ASYMMETRY_RANGE = os.getenv("STRATEGY_ASYMMETRY_RANGE", "3:2") # bias=range → 3 BUY / 2 SELL
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Kill switch (2026-06-22) ===
# Защита для live-режима: автоматически останавливает бота при:
# 1. Drawdown > KILL_DD_PCT% от пикового баланса (balance + unrealized position)
# 2. > KILL_API_ERRORS ошибок API за KILL_API_WINDOW_SEC секунд
# 3. Telegram /stop команда от TELEGRAM_CHAT_ID (long-poll каждые KILL_TG_POLL_SEC)
# При срабатывании: cancel all open orders (для live - через _live_cancel_all_pending),
# bot_running=False, требует ручного /api/bot/start для resume.
# KILL_SWITCH_ENABLED=false - весь модуль no-op (для отладки).
KILL_SWITCH_ENABLED = os.getenv("KILL_SWITCH_ENABLED", "true").lower() in ("true", "1", "yes")
KILL_DD_PCT = float(os.getenv("KILL_DD_PCT", "10.0"))
KILL_API_ERRORS = int(os.getenv("KILL_API_ERRORS", "5"))
KILL_API_WINDOW_SEC = int(os.getenv("KILL_API_WINDOW_SEC", "600"))
KILL_TG_POLL_SEC = int(os.getenv("KILL_TG_POLL_SEC", "30"))
KILL_TG_LONG_POLL = int(os.getenv("KILL_TG_LONG_POLL", "25"))
# === Reconcile (QuantDinger ledger_reconcile pattern) ===
# Периодическая сверка demo.position с trade_log: ловит
# "зависшие" позиции, открытые ДО последнего TP/SELL-fill.
# 0 = выключено. 300 = каждые 5 мин (рекомендация QuantDinger).
RECONCILE_INTERVAL_SEC = int(os.getenv("RECONCILE_INTERVAL_SEC", "300"))
# Если найдена shadow-позиция: 'log' (только предупреждение, безопасно)
# или 'clear' (auto-clear). По умолчанию log.
RECONCILE_AUTO_CLEAR = os.getenv("RECONCILE_AUTO_CLEAR", "log").lower() in ("true", "1", "yes")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+122
View File
@@ -0,0 +1,122 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "12"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.5"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Log ===
LOG_FILE = "margin-bot.log"
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
@@ -0,0 +1,161 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+182
View File
@@ -0,0 +1,182 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
# === Fee-coverage validator (QuantDinger validator pattern) ===
# Оценочная комиссия биржи Cifra Markets за сделку. Не подтверждена
# официально (см. MEMORY.md). 0.2% — безопасная оценка для BTC.
FEE_RATE_PERCENT = float(os.getenv("FEE_RATE_PERCENT", "0.2")) # 0.2% = 0.002
# Множитель запаса: round-trip fee * safety = минимально допустимый
# шаг сетки. QuantDinger default = 1.25.
FEE_SAFETY_MULTIPLIER = float(os.getenv("FEE_SAFETY_MULTIPLIER", "1.25"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# Режим построения сетки: 'arithmetic' (равные $шаги, по умолчанию)
# или 'geometric' (равные %шаги, не зависит от цены — точнее для
# широких диапазонов BTC).
GRID_MODE = os.getenv("GRID_MODE", "arithmetic").lower().strip()
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Reconcile (QuantDinger ledger_reconcile pattern) ===
# Периодическая сверка demo.position с trade_log: ловит
# "зависшие" позиции, открытые ДО последнего TP/SELL-fill.
# 0 = выключено. 300 = каждые 5 мин (рекомендация QuantDinger).
RECONCILE_INTERVAL_SEC = int(os.getenv("RECONCILE_INTERVAL_SEC", "300"))
# Если найдена shadow-позиция: 'log' (только предупреждение, безопасно)
# или 'clear' (auto-clear). По умолчанию log.
RECONCILE_AUTO_CLEAR = os.getenv("RECONCILE_AUTO_CLEAR", "log").lower() in ("true", "1", "yes")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+224
View File
@@ -0,0 +1,224 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
# === Fee-coverage validator (QuantDinger validator pattern) ===
# Оценочная комиссия биржи Cifra Markets за сделку. Не подтверждена
# официально (см. MEMORY.md). 0.2% — безопасная оценка для BTC.
FEE_RATE_PERCENT = float(os.getenv("FEE_RATE_PERCENT", "0.2")) # 0.2% = 0.002
# Множитель запаса: round-trip fee * safety = минимально допустимый
# шаг сетки. QuantDinger default = 1.25.
FEE_SAFETY_MULTIPLIER = float(os.getenv("FEE_SAFETY_MULTIPLIER", "1.25"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# Режим построения сетки: 'arithmetic' (равные $шаги, по умолчанию)
# или 'geometric' (равные %шаги, не зависит от цены — точнее для
# широких диапазонов BTC).
GRID_MODE = os.getenv("GRID_MODE", "arithmetic").lower().strip()
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === SMA-ATR strategy (2026-06-21) ===
# Replaces "grid_center = current_price" with SMA-anchored grid + lock FSM.
# Lock triggers when |price - sma| > LOCK_PCT%, unlock when < UNLOCK_PCT%.
# Bias source for asymmetry (3:2 split): Kronos primary, TREND fallback.
STRATEGY_CENTER_MODE = os.getenv("STRATEGY_CENTER_MODE", "current_price") # "sma" | "current_price"
STRATEGY_SMA_TF_MIN = int(os.getenv("STRATEGY_SMA_TF_MIN", "60")) # 60 = 1h
STRATEGY_SMA_PERIOD = int(os.getenv("STRATEGY_SMA_PERIOD", "24"))
STRATEGY_LOCK_PCT = float(os.getenv("STRATEGY_LOCK_PCT", "2.0")) # |dev| > 2% → lock
STRATEGY_UNLOCK_PCT = float(os.getenv("STRATEGY_UNLOCK_PCT", "1.5")) # |dev| < 1.5% → unlock
STRATEGY_BIAS_SOURCE = os.getenv("STRATEGY_BIAS_SOURCE", "kronos") # "kronos" | "trend" | "none"
STRATEGY_BIAS_FALLBACK = os.getenv("STRATEGY_BIAS_FALLBACK", "trend") # used when primary is unavailable
STRATEGY_SMA_CACHE_SEC = int(os.getenv("STRATEGY_SMA_CACHE_SEC", "300")) # recompute SMA every 5 min
STRATEGY_LOCK_COOLDOWN_SEC = int(os.getenv("STRATEGY_LOCK_COOLDOWN_SEC", "900")) # 15 min between lock state changes
STRATEGY_ASYMMETRY_UP = os.getenv("STRATEGY_ASYMMETRY_UP", "3:2") # bias=up → 3 BUY / 2 SELL
STRATEGY_ASYMMETRY_DOWN = os.getenv("STRATEGY_ASYMMETRY_DOWN", "2:3") # bias=down → 2 BUY / 3 SELL
STRATEGY_ASYMMETRY_RANGE = os.getenv("STRATEGY_ASYMMETRY_RANGE", "3:2") # bias=range → 3 BUY / 2 SELL
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Kill switch (2026-06-22) ===
# Защита для live-режима: автоматически останавливает бота при:
# 1. Drawdown > KILL_DD_PCT% от пикового баланса (balance + unrealized position)
# 2. > KILL_API_ERRORS ошибок API за KILL_API_WINDOW_SEC секунд
# 3. Telegram /stop команда от TELEGRAM_CHAT_ID (long-poll каждые KILL_TG_POLL_SEC)
# При срабатывании: cancel all open orders (для live - через _live_cancel_all_pending),
# bot_running=False, требует ручного /api/bot/start для resume.
# KILL_SWITCH_ENABLED=false - весь модуль no-op (для отладки).
KILL_SWITCH_ENABLED = os.getenv("KILL_SWITCH_ENABLED", "true").lower() in ("true", "1", "yes")
KILL_DD_PCT = float(os.getenv("KILL_DD_PCT", "10.0"))
KILL_API_ERRORS = int(os.getenv("KILL_API_ERRORS", "5"))
KILL_API_WINDOW_SEC = int(os.getenv("KILL_API_WINDOW_SEC", "600"))
KILL_TG_POLL_SEC = int(os.getenv("KILL_TG_POLL_SEC", "30"))
KILL_TG_LONG_POLL = int(os.getenv("KILL_TG_LONG_POLL", "25"))
# === Live-mode kill switch (2026-06-22) ===
# В live режиме kill switch дополнительно может:
# - LIVE_KILL_CLOSE_POSITION=true: закрыть открытую позицию market-sell при trigger
# (опасно в falling knife - оставь false для консервативного старта)
# - LIVE_KILL_DD_PCT: если задан (>0), переопределяет KILL_DD_PCT для live
# (например, можно осторожнее: 7% для live вместо 10% для demo)
LIVE_KILL_CLOSE_POSITION = os.getenv("LIVE_KILL_CLOSE_POSITION", "false").lower() in ("true", "1", "yes")
_live_dd_env = os.getenv("LIVE_KILL_DD_PCT", "")
LIVE_KILL_DD_PCT = float(_live_dd_env) if _live_dd_env else 0.0 # 0 = use KILL_DD_PCT
# === Reconcile (QuantDinger ledger_reconcile pattern) ===
# Периодическая сверка demo.position с trade_log: ловит
# "зависшие" позиции, открытые ДО последнего TP/SELL-fill.
# 0 = выключено. 300 = каждые 5 мин (рекомендация QuantDinger).
RECONCILE_INTERVAL_SEC = int(os.getenv("RECONCILE_INTERVAL_SEC", "300"))
# Если найдена shadow-позиция: 'log' (только предупреждение, безопасно)
# или 'clear' (auto-clear). По умолчанию log.
RECONCILE_AUTO_CLEAR = os.getenv("RECONCILE_AUTO_CLEAR", "log").lower() in ("true", "1", "yes")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+133
View File
@@ -0,0 +1,133 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+161
View File
@@ -0,0 +1,161 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+175
View File
@@ -0,0 +1,175 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+133
View File
@@ -0,0 +1,133 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+147
View File
@@ -0,0 +1,147 @@
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
+159
View File
@@ -0,0 +1,159 @@
"""
consult_endpoint.py — Flask Blueprint с POST /api/consult для лендинга it.kolp.pro.
Изолированный модуль: НЕ импортирует ничего из gridbot (main.py, grid.py, config.py).
Регистрация в api.py — одна строка:
from consult_endpoint import consult_bp
app.register_blueprint(consult_bp)
Содержит:
- Валидацию JSON
- Rate-limit (5 заявок/час с одного IP, in-memory)
- Защиту от header injection (sanitization в consult_mail)
- Try/except глухой — никакое исключение не должно валить Flask-приложение gridbot-а
- 200 / 400 / 429 / 500 — без утечки внутренних деталей наружу
"""
import time
import logging
from collections import defaultdict, deque
from threading import Lock
from typing import Tuple
from flask import Blueprint, request, jsonify
from consult_mail import send_consult_email, ConfigurationError
# === Blueprint ===
consult_bp = Blueprint("consult", __name__)
# === Rate-limit (in-memory, не сохраняется через рестарт) ===
# {ip: deque[timestamp_float]}
_rate_buckets: dict = defaultdict(deque)
_rate_lock = Lock()
_RATE_WINDOW_SEC = 3600 # 1 час
_RATE_MAX = 5 # 5 заявок/час (env override ниже)
try:
_RATE_MAX = int(__import__("os").getenv("CONSULT_RATE_LIMIT_PER_HOUR", "5"))
except (TypeError, ValueError):
_RATE_MAX = 5
def _check_rate_limit(ip: str) -> Tuple[bool, int]:
"""
Проверяет, не превысил ли IP rate-limit.
:return: (allowed, retry_after_seconds)
"""
now = time.time()
with _rate_lock:
bucket = _rate_buckets[ip]
# Удаляем старые timestamps
while bucket and (now - bucket[0]) > _RATE_WINDOW_SEC:
bucket.popleft()
if len(bucket) >= _RATE_MAX:
# Через сколько секунд освободится слот
retry_after = int(_RATE_WINDOW_SEC - (now - bucket[0])) + 1
return False, retry_after
bucket.append(now)
return True, 0
# === Логгер (тот же файл, что и в consult_mail) ===
import os as _os
_LOG_FILE = _os.getenv("CONSULT_LOG_FILE", "/root/grid-bot/consult.log")
def _get_logger() -> logging.Logger:
log = logging.getLogger("consult_endpoint")
if log.handlers:
return log
log.setLevel(logging.INFO)
try:
handler = logging.FileHandler(_LOG_FILE)
except (PermissionError, OSError):
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
log.addHandler(handler)
log.propagate = False
return log
# === Helpers ===
def _client_ip() -> str:
"""Получаем IP клиента с учётом reverse proxy (nginx)."""
fwd = request.headers.get("X-Forwarded-For", "").split(",")[0].strip()
return fwd or request.remote_addr or "unknown"
# === Route ===
@consult_bp.route("/api/consult", methods=["POST"])
def api_consult():
log = _get_logger()
ip = _client_ip()
ua = request.headers.get("User-Agent", "unknown")[:300]
ftype = "consultation" # lead-magnet пока не подключаем (TODO)
# 1) Rate limit
allowed, retry_after = _check_rate_limit(ip)
if not allowed:
log.warning("RATE-LIMIT IP=%s ua=%s retry_after=%ss", ip, ua, retry_after)
resp = jsonify({
"ok": False,
"error": "rate_limited",
"message": f"Слишком много заявок. Попробуйте через {retry_after // 60} мин.",
"retry_after": retry_after,
})
resp.status_code = 429
resp.headers["Retry-After"] = str(retry_after)
return resp
# 2) Парсим JSON
try:
data = request.get_json(force=True, silent=False)
except Exception:
log.warning("BAD JSON IP=%s ua=%s", ip, ua)
return jsonify({"ok": False, "error": "bad_request", "message": "Невалидный JSON"}), 400
if not isinstance(data, dict):
return jsonify({"ok": False, "error": "bad_request", "message": "Ожидается объект"}), 400
# 3) Передаём в mail-модуль
data["_ip"] = ip
data["_ua"] = ua
data["_type"] = ftype
try:
ok = send_consult_email(data)
except ValueError as e:
log.info("VALIDATION FAIL IP=%s: %s", ip, str(e)[:200])
return jsonify({"ok": False, "error": "validation", "message": str(e)}), 400
except ConfigurationError as e:
# Критично — endpoint не сконфигурирован, но gridbot должен жить
log.error("CONFIGURATION ERROR: %s — endpoint не настроен", str(e))
return jsonify({"ok": False, "error": "server_error",
"message": "Сервер временно не принимает заявки. Напишите на kolpakov.andrej@gmail.com"}), 500
except Exception as e:
# Глухой catch-all — ни одно исключение не должно валить Flask
log.exception("UNEXPECTED ERROR in send_consult_email: %s", str(e)[:200])
return jsonify({"ok": False, "error": "server_error",
"message": "Сервер временно не принимает заявки. Напишите на kolpakov.andrej@gmail.com"}), 500
if not ok:
return jsonify({"ok": False, "error": "send_failed",
"message": "Не удалось отправить. Попробуйте позже или напишите на kolpakov.andrej@gmail.com"}), 500
log.info("OK IP=%s name=%r", ip, data.get("name", "")[:50])
return jsonify({"ok": True, "message": "Заявка принята. Отвечу в течение 24 часов."}), 200
# === Health-check (опционально, для отладки) ===
@consult_bp.route("/api/consult/health", methods=["GET"])
def api_consult_health():
"""Простой ping, не зависит от SMTP."""
return jsonify({"ok": True, "rate_limit_per_hour": _RATE_MAX}), 200
+250
View File
@@ -0,0 +1,250 @@
"""
consult_mail.py — SMTP-логика для формы /api/consult.
Изолированный модуль: НЕ импортирует ничего из gridbot (main.py, grid.py, config.py).
Загружает переменные окружения CONSULT_SMTP_*, CONSULT_TO, CONSULT_FROM_NAME,
CONSULT_SUBJECT_PREFIX. Если переменные не заданы — выбрасывает ConfigurationError
при первом вызове (не при импорте, чтобы gridbot не падал).
Использует smtplib.SMTP_SSL с timeout 10 сек. При любой ошибке — пишет в
/root/grid-bot/consult.log и возвращает False, не выбрасывая исключение
наружу (endpoint сам решает, что отвечать клиенту).
"""
import os
import smtplib
import ssl
import logging
from email.message import EmailMessage
from typing import Optional
# === Конфигурация ===
class ConfigurationError(Exception):
"""Не заданы обязательные переменные окружения."""
pass
def _get_env(name: str, *, required: bool = True) -> Optional[str]:
val = os.getenv(name, "").strip()
if required and not val:
raise ConfigurationError(f"Required env var {name!r} is not set")
return val or None
def _get_bool(name: str, default: bool) -> bool:
raw = os.getenv(name, "").strip().lower()
if not raw:
return default
return raw in ("1", "true", "yes", "on")
# === Логгер (в файл, не в gridbot-логи) ===
_LOG_FILE = os.getenv("CONSULT_LOG_FILE", "/root/grid-bot/consult.log")
def _get_logger() -> logging.Logger:
log = logging.getLogger("consult_mail")
if log.handlers:
return log
log.setLevel(logging.INFO)
try:
handler = logging.FileHandler(_LOG_FILE)
except (PermissionError, OSError) as e:
# Fallback: stderr (для локальной разработки)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
"%(asctime)s consult_mail [%(levelname)s] %(message)s"
))
log.warning("Cannot open log file %s: %s — falling back to stderr", _LOG_FILE, e)
handler.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s"
))
log.addHandler(handler)
log.propagate = False
return log
# === Защита от header injection / мусора ===
_CONTROL_CHARS = set(chr(i) for i in range(32) if chr(i) not in "\t")
_CONTROL_CHARS.add("\r")
_CONTROL_CHARS.add("\n")
_CONTROL_CHARS.add("\x7f")
def _sanitize(value: str, max_len: int = 500) -> str:
"""Strip whitespace, remove control chars, truncate. No header injection possible."""
if not isinstance(value, str):
return ""
cleaned = "".join(c for c in value if c not in _CONTROL_CHARS)
cleaned = cleaned.strip()
if len(cleaned) > max_len:
cleaned = cleaned[:max_len]
return cleaned
_EMAIL_RE = None
def _is_valid_email(s: str) -> bool:
"""Lightweight email check — not RFC 5322, but enough to reject obvious garbage."""
global _EMAIL_RE
import re
if _EMAIL_RE is None:
_EMAIL_RE = re.compile(r"^[^@\s\x00-\x1f]+@[^@\s\x00-\x1f]+\.[^@\s\x00-\x1f]{2,}$")
return bool(_EMAIL_RE.match(s))
# === Публичный API ===
def send_consult_email(form_data: dict) -> bool:
"""
Отправляет письмо-заявку с сайта it.kolp.pro.
:param form_data: dict с полями name, email, company, phone, message
:return: True если письмо ушло, False при любой ошибке
:raises ConfigurationError: если не заданы env-переменные
:raises ValueError: если невалидные данные (вызывающий должен перехватить → 400)
"""
log = _get_logger()
# 1) Sanitize входные данные
name = _sanitize(form_data.get("name", ""), max_len=100)
email = _sanitize(form_data.get("email", ""), max_len=200)
company = _sanitize(form_data.get("company", ""), max_len=200)
phone = _sanitize(form_data.get("phone", ""), max_len=50)
message = _sanitize(form_data.get("message", ""), max_len=2000)
# 2) Валидация
if not name:
raise ValueError("name is required")
if not email or not _is_valid_email(email):
raise ValueError("valid email is required")
# 3) Читаем конфиг
host = _get_env("CONSULT_SMTP_HOST")
port = int(_get_env("CONSULT_SMTP_PORT", required=False) or "465")
use_ssl = _get_bool("CONSULT_SMTP_USE_SSL", default=True)
user = _get_env("CONSULT_SMTP_USER")
password = _get_env("CONSULT_SMTP_PASSWORD")
sender = _get_env("CONSULT_FROM", required=False) or user
sender_name = _get_env("CONSULT_FROM_NAME", required=False) or "Сайт it.kolp.pro"
recipient = _get_env("CONSULT_TO")
subject_prefix = _get_env("CONSULT_SUBJECT_PREFIX", required=False) or "[it.kolp.pro]"
# 4) Формируем письмо
msg = EmailMessage()
msg["From"] = f"{sender_name} <{sender}>"
msg["To"] = recipient
msg["Reply-To"] = f"{name} <{email}>"
subj_type = {
"consultation": "Заявка",
"checklist": "Чек-лист",
}.get(form_data.get("_type", "consultation"), "Заявка")
msg["Subject"] = f"{subject_prefix} {subj_type} от {name}"
body_lines = [
f"Имя: {name}",
f"Email: {email}",
]
if company:
body_lines.append(f"Компания: {company}")
if phone:
body_lines.append(f"Телефон / Telegram: {phone}")
if message:
body_lines.append("")
body_lines.append("Что болит:")
body_lines.append(message)
# === Чек-лист (если передан И заполнен хотя бы частично) ===
# По требованию: если чек-лист НЕ заполнен, не прикреплять.
# Считаем чек-лист "заполненным" если completed=True или answered >= 1.
checklist = form_data.get("checklist")
checklist_has_data = (
isinstance(checklist, dict)
and (
checklist.get("completed") is True
or int(checklist.get("answered", 0) or 0) > 0
or any((checklist.get("goals") or {}).values())
)
)
if isinstance(checklist, dict) and checklist_has_data:
body_lines.append("")
body_lines.append("=" * 50)
body_lines.append("РЕЗУЛЬТАТ ЧЕК-ЛИСТА")
body_lines.append("=" * 50)
body_lines.append(f"Уровень: {checklist.get('level', '—')}")
body_lines.append(f"Балл: {checklist.get('total_score', 0)} / {checklist.get('max_score', 30)}")
body_lines.append(f"Отвечено: {checklist.get('answered', 0)} из {checklist.get('total_questions', 15)}")
body_lines.append(f"Заполнен полностью: {'да' if checklist.get('completed') else 'нет'}")
answers = checklist.get("answers", [])
questions = checklist.get("questions", [])
if isinstance(answers, list) and answers:
body_lines.append("")
body_lines.append("Ответы по вопросам:")
for i, ans in enumerate(answers, 1):
q_text = questions[i-1] if i-1 < len(questions) else f"Вопрос {i}"
# Сократим текст вопроса до 200 символов чтобы не раздувать
q_text_short = (q_text[:200] + "…") if len(q_text) > 200 else q_text
body_lines.append(f" {i}. {q_text_short}")
if isinstance(ans, dict):
v = ans.get("v")
if v is None:
body_lines.append(" → (не отвечено)")
else:
body_lines.append(f" → {ans.get('txt', v)} ({v}/2)")
else:
body_lines.append(f" → {ans}")
prep = checklist.get("prep_facts", {})
if isinstance(prep, dict) and prep:
body_lines.append("")
body_lines.append("Цифры для встречи:")
for k, v in prep.items():
if v:
body_lines.append(f" • {v}")
goals = checklist.get("goals", {})
if isinstance(goals, dict) and any(goals.values()):
body_lines.append("")
body_lines.append("Чего ждёте от ИТ:")
if goals.get("what_to_improve"):
body_lines.append(f" Стать лучше: {goals['what_to_improve']}")
if goals.get("what_to_discuss"):
body_lines.append(f" Обсудить: {goals['what_to_discuss']}")
body_lines.append("")
body_lines.append("---")
body_lines.append(f"IP отправителя: {form_data.get('_ip', 'unknown')}")
body_lines.append(f"User-Agent: {form_data.get('_ua', 'unknown')}")
body_lines.append(f"Тип формы: {form_data.get('_type', 'consultation')}")
msg.set_content("\n".join(body_lines), charset="utf-8")
# 5) Отправляем
try:
ctx = ssl.create_default_context()
if use_ssl:
with smtplib.SMTP_SSL(host, port, context=ctx, timeout=10) as smtp:
smtp.ehlo()
smtp.login(user, password)
smtp.send_message(msg)
else:
with smtplib.SMTP(host, port, timeout=10) as smtp:
smtp.ehlo()
smtp.starttls(context=ctx)
smtp.ehlo()
smtp.login(user, password)
smtp.send_message(msg)
log.info("OK: lead from %s <%s> sent to %s", name, email, recipient)
return True
except smtplib.SMTPAuthenticationError as e:
log.error("SMTP AUTH FAILED (check credentials): %s", str(e)[:200])
except smtplib.SMTPException as e:
log.error("SMTP ERROR: %s: %s", type(e).__name__, str(e)[:200])
except (OSError, ssl.SSLError) as e:
log.error("NETWORK/SSL ERROR: %s: %s", type(e).__name__, str(e)[:200])
except Exception as e:
log.exception("UNEXPECTED ERROR: %s: %s", type(e).__name__, str(e)[:200])
return False
+250
View File
@@ -0,0 +1,250 @@
"""
consult_mail.py — SMTP-логика для формы /api/consult.
Изолированный модуль: НЕ импортирует ничего из gridbot (main.py, grid.py, config.py).
Загружает переменные окружения CONSULT_SMTP_*, CONSULT_TO, CONSULT_FROM_NAME,
CONSULT_SUBJECT_PREFIX. Если переменные не заданы — выбрасывает ConfigurationError
при первом вызове (не при импорте, чтобы gridbot не падал).
Использует smtplib.SMTP_SSL с timeout 10 сек. При любой ошибке — пишет в
/root/grid-bot/consult.log и возвращает False, не выбрасывая исключение
наружу (endpoint сам решает, что отвечать клиенту).
"""
import os
import smtplib
import ssl
import logging
from email.message import EmailMessage
from typing import Optional
# === Конфигурация ===
class ConfigurationError(Exception):
"""Не заданы обязательные переменные окружения."""
pass
def _get_env(name: str, *, required: bool = True) -> Optional[str]:
val = os.getenv(name, "").strip()
if required and not val:
raise ConfigurationError(f"Required env var {name!r} is not set")
return val or None
def _get_bool(name: str, default: bool) -> bool:
raw = os.getenv(name, "").strip().lower()
if not raw:
return default
return raw in ("1", "true", "yes", "on")
# === Логгер (в файл, не в gridbot-логи) ===
_LOG_FILE = os.getenv("CONSULT_LOG_FILE", "/root/grid-bot/consult.log")
def _get_logger() -> logging.Logger:
log = logging.getLogger("consult_mail")
if log.handlers:
return log
log.setLevel(logging.INFO)
try:
handler = logging.FileHandler(_LOG_FILE)
except (PermissionError, OSError) as e:
# Fallback: stderr (для локальной разработки)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
"%(asctime)s consult_mail [%(levelname)s] %(message)s"
))
log.warning("Cannot open log file %s: %s — falling back to stderr", _LOG_FILE, e)
handler.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s"
))
log.addHandler(handler)
log.propagate = False
return log
# === Защита от header injection / мусора ===
_CONTROL_CHARS = set(chr(i) for i in range(32) if chr(i) not in "\t")
_CONTROL_CHARS.add("\r")
_CONTROL_CHARS.add("\n")
_CONTROL_CHARS.add("\x7f")
def _sanitize(value: str, max_len: int = 500) -> str:
"""Strip whitespace, remove control chars, truncate. No header injection possible."""
if not isinstance(value, str):
return ""
cleaned = "".join(c for c in value if c not in _CONTROL_CHARS)
cleaned = cleaned.strip()
if len(cleaned) > max_len:
cleaned = cleaned[:max_len]
return cleaned
_EMAIL_RE = None
def _is_valid_email(s: str) -> bool:
"""Lightweight email check — not RFC 5322, but enough to reject obvious garbage."""
global _EMAIL_RE
import re
if _EMAIL_RE is None:
_EMAIL_RE = re.compile(r"^[^@\s\x00-\x1f]+@[^@\s\x00-\x1f]+\.[^@\s\x00-\x1f]{2,}$")
return bool(_EMAIL_RE.match(s))
# === Публичный API ===
def send_consult_email(form_data: dict) -> bool:
"""
Отправляет письмо-заявку с сайта it.kolp.pro.
:param form_data: dict с полями name, email, company, phone, message
:return: True если письмо ушло, False при любой ошибке
:raises ConfigurationError: если не заданы env-переменные
:raises ValueError: если невалидные данные (вызывающий должен перехватить → 400)
"""
log = _get_logger()
# 1) Sanitize входные данные
name = _sanitize(form_data.get("name", ""), max_len=100)
email = _sanitize(form_data.get("email", ""), max_len=200)
company = _sanitize(form_data.get("company", ""), max_len=200)
phone = _sanitize(form_data.get("phone", ""), max_len=50)
message = _sanitize(form_data.get("message", ""), max_len=2000)
# 2) Валидация
if not name:
raise ValueError("name is required")
if not email or not _is_valid_email(email):
raise ValueError("valid email is required")
# 3) Читаем конфиг
host = _get_env("CONSULT_SMTP_HOST")
port = int(_get_env("CONSULT_SMTP_PORT", required=False) or "465")
use_ssl = _get_bool("CONSULT_SMTP_USE_SSL", default=True)
user = _get_env("CONSULT_SMTP_USER")
password = _get_env("CONSULT_SMTP_PASSWORD")
sender = _get_env("CONSULT_FROM", required=False) or user
sender_name = _get_env("CONSULT_FROM_NAME", required=False) or "Сайт it.kolp.pro"
recipient = _get_env("CONSULT_TO")
subject_prefix = _get_env("CONSULT_SUBJECT_PREFIX", required=False) or "[it.kolp.pro]"
# 4) Формируем письмо
msg = EmailMessage()
msg["From"] = f"{sender_name} <{sender}>"
msg["To"] = recipient
msg["Reply-To"] = f"{name} <{email}>"
subj_type = {
"consultation": "Заявка",
"checklist": "Чек-лист",
}.get(form_data.get("_type", "consultation"), "Заявка")
msg["Subject"] = f"{subject_prefix} {subj_type} от {name}"
body_lines = [
f"Имя: {name}",
f"Email: {email}",
]
if company:
body_lines.append(f"Компания: {company}")
if phone:
body_lines.append(f"Телефон / Telegram: {phone}")
if message:
body_lines.append("")
body_lines.append("Что болит:")
body_lines.append(message)
# === Чек-лист (если передан И заполнен хотя бы частично) ===
# По требованию: если чек-лист НЕ заполнен, не прикреплять.
# Считаем чек-лист "заполненным" если completed=True или answered >= 1.
checklist = form_data.get("checklist")
checklist_has_data = (
isinstance(checklist, dict)
and (
checklist.get("completed") is True
or int(checklist.get("answered", 0) or 0) > 0
or any((checklist.get("goals") or {}).values())
)
)
if isinstance(checklist, dict) and checklist_has_data:
body_lines.append("")
body_lines.append("=" * 50)
body_lines.append("РЕЗУЛЬТАТ ЧЕК-ЛИСТА")
body_lines.append("=" * 50)
body_lines.append(f"Уровень: {checklist.get('level', '—')}")
body_lines.append(f"Балл: {checklist.get('total_score', 0)} / {checklist.get('max_score', 30)}")
body_lines.append(f"Отвечено: {checklist.get('answered', 0)} из {checklist.get('total_questions', 15)}")
body_lines.append(f"Заполнен полностью: {'да' if checklist.get('completed') else 'нет'}")
answers = checklist.get("answers", [])
questions = checklist.get("questions", [])
if isinstance(answers, list) and answers:
body_lines.append("")
body_lines.append("Ответы по вопросам:")
for i, ans in enumerate(answers, 1):
q_text = questions[i-1] if i-1 < len(questions) else f"Вопрос {i}"
# Сократим текст вопроса до 200 символов чтобы не раздувать
q_text_short = (q_text[:200] + "…") if len(q_text) > 200 else q_text
body_lines.append(f" {i}. {q_text_short}")
if isinstance(ans, dict):
v = ans.get("v")
if v is None:
body_lines.append(" → (не отвечено)")
else:
body_lines.append(f" → {ans.get('txt', v)} ({v}/2)")
else:
body_lines.append(f" → {ans}")
prep = checklist.get("prep_facts", {})
if isinstance(prep, dict) and prep:
body_lines.append("")
body_lines.append("Цифры для встречи:")
for k, v in prep.items():
if v:
body_lines.append(f" • {v}")
goals = checklist.get("goals", {})
if isinstance(goals, dict) and any(goals.values()):
body_lines.append("")
body_lines.append("Чего ждёте от ИТ:")
if goals.get("what_to_improve"):
body_lines.append(f" Стать лучше: {goals['what_to_improve']}")
if goals.get("what_to_discuss"):
body_lines.append(f" Обсудить: {goals['what_to_discuss']}")
body_lines.append("")
body_lines.append("---")
body_lines.append(f"IP отправителя: {form_data.get('_ip', 'unknown')}")
body_lines.append(f"User-Agent: {form_data.get('_ua', 'unknown')}")
body_lines.append(f"Тип формы: {form_data.get('_type', 'consultation')}")
msg.set_content("\n".join(body_lines), charset="utf-8")
# 5) Отправляем
try:
ctx = ssl.create_default_context()
if use_ssl:
with smtplib.SMTP_SSL(host, port, context=ctx, timeout=10) as smtp:
smtp.ehlo()
smtp.login(user, password)
smtp.send_message(msg)
else:
with smtplib.SMTP(host, port, timeout=10) as smtp:
smtp.ehlo()
smtp.starttls(context=ctx)
smtp.ehlo()
smtp.login(user, password)
smtp.send_message(msg)
log.info("OK: lead from %s <%s> sent to %s", name, email, recipient)
return True
except smtplib.SMTPAuthenticationError as e:
log.error("SMTP AUTH FAILED (check credentials): %s", str(e)[:200])
except smtplib.SMTPException as e:
log.error("SMTP ERROR: %s: %s", type(e).__name__, str(e)[:200])
except (OSError, ssl.SSLError) as e:
log.error("NETWORK/SSL ERROR: %s: %s", type(e).__name__, str(e)[:200])
except Exception as e:
log.exception("UNEXPECTED ERROR: %s: %s", type(e).__name__, str(e)[:200])
return False
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=Grid Trading Bot (Tradernet/Cifra.by)
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/grid-bot
EnvironmentFile=/root/grid-bot/.env
ExecStart=/root/grid-bot/venv/bin/python /root/grid-bot/main.py
ExecStop=/bin/kill -SIGINT $MAINPID
Restart=on failure
RestartSec=10
StandardOutput=append:/root/grid-bot/bot.log
StandardError=append:/root/grid-bot/bot.log
[Install]
WantedBy=multi-user.target
+484
View File
@@ -0,0 +1,484 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
grid_mode: str = "arithmetic",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
# 'arithmetic' — равные $шаги; 'geometric' — равные %шаги.
# Для BTC на широком диапазоне geometric точнее (см. QuantDinger levels.py).
self.grid_mode = str(grid_mode or "arithmetic").lower().strip()
if self.grid_mode not in ("arithmetic", "geometric"):
self.grid_mode = "arithmetic"
def calc_step(self, center_price: float) -> float:
"""Absolute step size at the center price.
arithmetic: constant $ step = center * step_percent
geometric: multiplicative — step at level n is roughly center * step_percent * ratio^n.
The constant returned is the *level-1* step in $.
"""
if self.grid_mode == "geometric" and center_price > 0 and self.step_percent > 0:
# Return nominal $ step at center for compat with old code paths
# (rebuild threshold, etc.). Real price spacing is computed in
# get_grid_levels() via multiplicative ratio.
return center_price * self.step_percent
return center_price * self.step_percent
def get_grid_levels(
self,
center_price: float,
base_qty: float = 0.0001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid levels.
buy_center - anchor for BUY levels (default: center_price)
sell_center - anchor for SELL levels (default: center_price)
If they equal center_price, behaviour matches the legacy symmetric grid.
If they differ, BUY is built around buy_center and SELL around sell_center.
Used by Kronos logic: at bias=up the BUY anchor stays near current_price (aggressive),
the SELL anchor is shifted toward the forecasted price; at bias=down - mirrored.
Modes:
- arithmetic (default): levels at center ± n*step$
- geometric: levels at center / ratio^n and center * ratio^n
ratio = 1 + step_percent (constant % step).
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
levels = []
if self.grid_mode == "geometric" and self.step_percent > 0:
ratio = 1.0 + self.step_percent
for n in range(1, self.levels_count + 1):
factor = ratio ** n
buy_price = buy_center / factor
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center * factor
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
else:
step = self.calc_step(center_price)
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = buy_center - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def get_asymmetric_grid_levels(
self,
center_price: float,
buy_count: int,
sell_count: int,
base_qty: float = 0.0001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid with buy_count levels below and sell_count above center.
Replaces symmetric get_grid_levels when asymmetry is desired (e.g.,
SMA-ATR strategy: 3 BUY + 2 SELL for bias=up).
Supports arithmetic (default) and geometric modes — same as
get_grid_levels.
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
buy_count = max(0, int(buy_count))
sell_count = max(0, int(sell_count))
if buy_count == 0 and sell_count == 0:
return []
levels: list[GridLevel] = []
max_n = max(buy_count, sell_count)
if self.grid_mode == "geometric" and self.step_percent > 0:
ratio = 1.0 + self.step_percent
for n in range(1, max_n + 1):
factor = ratio ** n
if n <= buy_count:
buy_price = buy_center / factor
if buy_price > 0:
levels.append(GridLevel(
level_id=-n, price=round(buy_price, 8),
side="BUY", qty=base_qty, step_num=n,
))
if n <= sell_count:
sell_price = sell_center * factor
levels.append(GridLevel(
level_id=+n, price=round(sell_price, 8),
side="SELL", qty=base_qty, step_num=n,
))
else:
step = self.calc_step(center_price)
for n in range(1, max_n + 1):
offset = n * step
if n <= buy_count:
buy_price = buy_center - offset
if buy_price > 0:
levels.append(GridLevel(
level_id=-n, price=round(buy_price, 8),
side="BUY", qty=base_qty, step_num=n,
))
if n <= sell_count:
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n, price=round(sell_price, 8),
side="SELL", qty=base_qty, step_num=n,
))
levels.sort(key=lambda x: x.level_id)
return levels
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order.
Auto-shrinks qty if balance is insufficient for full size.
Min qty = 0.0001 BTC (Tradernet minimum lot for BTC-USDT).
"""
min_qty = 0.0001
cost = price * qty
if self.balance >= cost:
# Full size fits
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
# Try to fit a smaller qty into available balance
affordable_qty = round(self.balance / price, 8) if price > 0 else 0
if affordable_qty >= min_qty:
cost = price * affordable_qty
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": affordable_qty, "level_id": level_id, "filled": False})
return True
# Insufficient balance for even min lot — warn loudly (was silent before 15.06 patch)
import logging
logging.getLogger(__name__).warning(
f"[DEMO] skip BUY @ ${price:,.2f} (qty={qty} BTC): "
f"balance=${self.balance:.2f} < min_cost=${min_qty*price:.2f} "
f"(short by ${min_qty*price - self.balance:.2f}); level_id={level_id}"
)
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
opened_at=time.time(),
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def _cancel_open_sells(self, reason: str) -> int:
"""Отменить все висящие (не-filled) SELL-ордера. Возвращает кол-во отменённых.
Вызывается при ЗАКРЫТИИ позиции — иначе SELL-ордер висит и при fill
без открытой позиции даёт фантомный pnl=0 и теряет proceeds.
"""
before = len(self.orders)
self.orders = [o for o in self.orders if o["filled"] or o["side"] != "SELL"]
cancelled = before - len(self.orders)
if cancelled > 0:
# Логгируем на уровне INFO через глобальный logger, если есть
import logging
logging.getLogger("grid").info(
f"[DEMO] cancelled {cancelled} open SELL order(s): {reason}"
)
return cancelled
def cancel_open_buys(self, reason: str = "") -> int:
"""Cancel all unfilled BUY orders AND refund reserved balance.
For BUY orders the cost (price * qty) was debited at
place_buy_order — cancelling must credit it back, otherwise
every grid rebuild silently drains the wallet. Returns the
count of cancelled orders. SELL orders are not touched
(no balance was reserved for them).
"""
new_orders = []
refunded = 0.0
cancelled = 0
for o in self.orders:
if o["side"] == "BUY" and not o.get("filled"):
refunded += o["price"] * o["qty"]
cancelled += 1
else:
new_orders.append(o)
self.orders = new_orders
if cancelled > 0:
self.balance += refunded
import logging
logging.getLogger("grid").info(
f"[DEMO] cancelled {cancelled} open BUY order(s) and refunded "
f"${refunded:.2f}; reason={reason}"
)
return cancelled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: SELL закрыл позицию → остальные SELL ордера-зомби
# больше не нужны, отменяем.
self._cancel_open_sells("position closed via SELL fill")
else:
# SELL-zombie: ордер заполнился, но позиции уже нет.
# Не начисляем proceeds (нет BTC), логгируем warning.
import logging
logging.getLogger("grid").warning(
f"[DEMO] SELL-zombie fill @ ${order['price']:,.2f} qty={order['qty']} BTC "
f"— no position to close, no proceeds credited"
)
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: TP закрыл позицию → SELL-ордера-зомби отменяем
self._cancel_open_sells("position closed via TAKE_PROFIT")
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+272
View File
@@ -0,0 +1,272 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(self, center_price: float, base_qty: float = 0.001) -> list[GridLevel]:
"""Build grid levels around center_price."""
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = center_price - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = center_price + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order."""
cost = price * qty
if self.balance >= cost:
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
})
self.position = None
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
})
self.position = None
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len([o for o in self.orders if not o["filled"]]),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+290
View File
@@ -0,0 +1,290 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(
self,
center_price: float,
base_qty: float = 0.001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid levels.
buy_center - anchor for BUY levels (default: center_price)
sell_center - anchor for SELL levels (default: center_price)
If they equal center_price, behaviour matches the legacy symmetric grid.
If they differ, BUY is built around buy_center and SELL around sell_center.
Used by Kronos logic: at bias=up the BUY anchor stays near current_price (aggressive),
the SELL anchor is shifted toward the forecasted price; at bias=down - mirrored.
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = buy_center - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order."""
cost = price * qty
if self.balance >= cost:
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
})
self.position = None
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
})
self.position = None
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+340
View File
@@ -0,0 +1,340 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(
self,
center_price: float,
base_qty: float = 0.001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid levels.
buy_center - anchor for BUY levels (default: center_price)
sell_center - anchor for SELL levels (default: center_price)
If they equal center_price, behaviour matches the legacy symmetric grid.
If they differ, BUY is built around buy_center and SELL around sell_center.
Used by Kronos logic: at bias=up the BUY anchor stays near current_price (aggressive),
the SELL anchor is shifted toward the forecasted price; at bias=down - mirrored.
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = buy_center - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order.
Auto-shrinks qty if balance is insufficient for full size.
Min qty = 0.0001 BTC (Tradernet minimum lot for BTC-USDT).
"""
min_qty = 0.0001
cost = price * qty
if self.balance >= cost:
# Full size fits
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
# Try to fit a smaller qty into available balance
affordable_qty = round(self.balance / price, 8) if price > 0 else 0
if affordable_qty >= min_qty:
cost = price * affordable_qty
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": affordable_qty, "level_id": level_id, "filled": False})
return True
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
opened_at=time.time(),
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def _cancel_open_sells(self, reason: str) -> int:
"""Отменить все висящие (не-filled) SELL-ордера. Возвращает кол-во отменённых.
Вызывается при ЗАКРЫТИИ позиции — иначе SELL-ордер висит и при fill
без открытой позиции даёт фантомный pnl=0 и теряет proceeds.
"""
before = len(self.orders)
self.orders = [o for o in self.orders if o["filled"] or o["side"] != "SELL"]
cancelled = before - len(self.orders)
if cancelled > 0:
# Логгируем на уровне INFO через глобальный logger, если есть
import logging
logging.getLogger("grid").info(
f"[DEMO] cancelled {cancelled} open SELL order(s): {reason}"
)
return cancelled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: SELL закрыл позицию → остальные SELL ордера-зомби
# больше не нужны, отменяем.
self._cancel_open_sells("position closed via SELL fill")
else:
# SELL-zombie: ордер заполнился, но позиции уже нет.
# Не начисляем proceeds (нет BTC), логгируем warning.
import logging
logging.getLogger("grid").warning(
f"[DEMO] SELL-zombie fill @ ${order['price']:,.2f} qty={order['qty']} BTC "
f"— no position to close, no proceeds credited"
)
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: TP закрыл позицию → SELL-ордера-зомби отменяем
self._cancel_open_sells("position closed via TAKE_PROFIT")
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
@@ -0,0 +1,302 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(
self,
center_price: float,
base_qty: float = 0.001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid levels.
buy_center - anchor for BUY levels (default: center_price)
sell_center - anchor for SELL levels (default: center_price)
If they equal center_price, behaviour matches the legacy symmetric grid.
If they differ, BUY is built around buy_center and SELL around sell_center.
Used by Kronos logic: at bias=up the BUY anchor stays near current_price (aggressive),
the SELL anchor is shifted toward the forecasted price; at bias=down - mirrored.
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = buy_center - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order.
Auto-shrinks qty if balance is insufficient for full size.
Min qty = 0.0001 BTC (Tradernet minimum lot for BTC-USDT).
"""
min_qty = 0.0001
cost = price * qty
if self.balance >= cost:
# Full size fits
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
# Try to fit a smaller qty into available balance
affordable_qty = round(self.balance / price, 8) if price > 0 else 0
if affordable_qty >= min_qty:
cost = price * affordable_qty
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": affordable_qty, "level_id": level_id, "filled": False})
return True
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
})
self.position = None
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
})
self.position = None
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+347
View File
@@ -0,0 +1,347 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(
self,
center_price: float,
base_qty: float = 0.0001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid levels.
buy_center - anchor for BUY levels (default: center_price)
sell_center - anchor for SELL levels (default: center_price)
If they equal center_price, behaviour matches the legacy symmetric grid.
If they differ, BUY is built around buy_center and SELL around sell_center.
Used by Kronos logic: at bias=up the BUY anchor stays near current_price (aggressive),
the SELL anchor is shifted toward the forecasted price; at bias=down - mirrored.
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = buy_center - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order.
Auto-shrinks qty if balance is insufficient for full size.
Min qty = 0.0001 BTC (Tradernet minimum lot for BTC-USDT).
"""
min_qty = 0.0001
cost = price * qty
if self.balance >= cost:
# Full size fits
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
# Try to fit a smaller qty into available balance
affordable_qty = round(self.balance / price, 8) if price > 0 else 0
if affordable_qty >= min_qty:
cost = price * affordable_qty
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": affordable_qty, "level_id": level_id, "filled": False})
return True
# Insufficient balance for even min lot — warn loudly (was silent before 15.06 patch)
import logging
logging.getLogger(__name__).warning(
f"[DEMO] skip BUY @ ${price:,.2f} (qty={qty} BTC): "
f"balance=${self.balance:.2f} < min_cost=${min_qty*price:.2f} "
f"(short by ${min_qty*price - self.balance:.2f}); level_id={level_id}"
)
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
opened_at=time.time(),
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def _cancel_open_sells(self, reason: str) -> int:
"""Отменить все висящие (не-filled) SELL-ордера. Возвращает кол-во отменённых.
Вызывается при ЗАКРЫТИИ позиции — иначе SELL-ордер висит и при fill
без открытой позиции даёт фантомный pnl=0 и теряет proceeds.
"""
before = len(self.orders)
self.orders = [o for o in self.orders if o["filled"] or o["side"] != "SELL"]
cancelled = before - len(self.orders)
if cancelled > 0:
# Логгируем на уровне INFO через глобальный logger, если есть
import logging
logging.getLogger("grid").info(
f"[DEMO] cancelled {cancelled} open SELL order(s): {reason}"
)
return cancelled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: SELL закрыл позицию → остальные SELL ордера-зомби
# больше не нужны, отменяем.
self._cancel_open_sells("position closed via SELL fill")
else:
# SELL-zombie: ордер заполнился, но позиции уже нет.
# Не начисляем proceeds (нет BTC), логгируем warning.
import logging
logging.getLogger("grid").warning(
f"[DEMO] SELL-zombie fill @ ${order['price']:,.2f} qty={order['qty']} BTC "
f"— no position to close, no proceeds credited"
)
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: TP закрыл позицию → SELL-ордера-зомби отменяем
self._cancel_open_sells("position closed via TAKE_PROFIT")
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
@@ -0,0 +1,375 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(
self,
center_price: float,
base_qty: float = 0.0001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid levels.
buy_center - anchor for BUY levels (default: center_price)
sell_center - anchor for SELL levels (default: center_price)
If they equal center_price, behaviour matches the legacy symmetric grid.
If they differ, BUY is built around buy_center and SELL around sell_center.
Used by Kronos logic: at bias=up the BUY anchor stays near current_price (aggressive),
the SELL anchor is shifted toward the forecasted price; at bias=down - mirrored.
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = buy_center - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order.
Auto-shrinks qty if balance is insufficient for full size.
Min qty = 0.0001 BTC (Tradernet minimum lot for BTC-USDT).
"""
min_qty = 0.0001
cost = price * qty
if self.balance >= cost:
# Full size fits
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
# Try to fit a smaller qty into available balance
affordable_qty = round(self.balance / price, 8) if price > 0 else 0
if affordable_qty >= min_qty:
cost = price * affordable_qty
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": affordable_qty, "level_id": level_id, "filled": False})
return True
# Insufficient balance for even min lot — warn loudly (was silent before 15.06 patch)
import logging
logging.getLogger(__name__).warning(
f"[DEMO] skip BUY @ ${price:,.2f} (qty={qty} BTC): "
f"balance=${self.balance:.2f} < min_cost=${min_qty*price:.2f} "
f"(short by ${min_qty*price - self.balance:.2f}); level_id={level_id}"
)
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
opened_at=time.time(),
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def _cancel_open_sells(self, reason: str) -> int:
"""Отменить все висящие (не-filled) SELL-ордера. Возвращает кол-во отменённых.
Вызывается при ЗАКРЫТИИ позиции — иначе SELL-ордер висит и при fill
без открытой позиции даёт фантомный pnl=0 и теряет proceeds.
"""
before = len(self.orders)
self.orders = [o for o in self.orders if o["filled"] or o["side"] != "SELL"]
cancelled = before - len(self.orders)
if cancelled > 0:
# Логгируем на уровне INFO через глобальный logger, если есть
import logging
logging.getLogger("grid").info(
f"[DEMO] cancelled {cancelled} open SELL order(s): {reason}"
)
return cancelled
def cancel_open_buys(self, reason: str = "") -> int:
"""Cancel all unfilled BUY orders AND refund reserved balance.
For BUY orders the cost (price * qty) was debited at
place_buy_order — cancelling must credit it back, otherwise
every grid rebuild silently drains the wallet. Returns the
count of cancelled orders. SELL orders are not touched
(no balance was reserved for them).
"""
new_orders = []
refunded = 0.0
cancelled = 0
for o in self.orders:
if o["side"] == "BUY" and not o.get("filled"):
refunded += o["price"] * o["qty"]
cancelled += 1
else:
new_orders.append(o)
self.orders = new_orders
if cancelled > 0:
self.balance += refunded
import logging
logging.getLogger("grid").info(
f"[DEMO] cancelled {cancelled} open BUY order(s) and refunded "
f"${refunded:.2f}; reason={reason}"
)
return cancelled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: SELL закрыл позицию → остальные SELL ордера-зомби
# больше не нужны, отменяем.
self._cancel_open_sells("position closed via SELL fill")
else:
# SELL-zombie: ордер заполнился, но позиции уже нет.
# Не начисляем proceeds (нет BTC), логгируем warning.
import logging
logging.getLogger("grid").warning(
f"[DEMO] SELL-zombie fill @ ${order['price']:,.2f} qty={order['qty']} BTC "
f"— no position to close, no proceeds credited"
)
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: TP закрыл позицию → SELL-ордера-зомби отменяем
self._cancel_open_sells("position closed via TAKE_PROFIT")
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+418
View File
@@ -0,0 +1,418 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
grid_mode: str = "arithmetic",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
# 'arithmetic' — равные $шаги; 'geometric' — равные %шаги.
# Для BTC на широком диапазоне geometric точнее (см. QuantDinger levels.py).
self.grid_mode = str(grid_mode or "arithmetic").lower().strip()
if self.grid_mode not in ("arithmetic", "geometric"):
self.grid_mode = "arithmetic"
def calc_step(self, center_price: float) -> float:
"""Absolute step size at the center price.
arithmetic: constant $ step = center * step_percent
geometric: multiplicative — step at level n is roughly center * step_percent * ratio^n.
The constant returned is the *level-1* step in $.
"""
if self.grid_mode == "geometric" and center_price > 0 and self.step_percent > 0:
# Return nominal $ step at center for compat with old code paths
# (rebuild threshold, etc.). Real price spacing is computed in
# get_grid_levels() via multiplicative ratio.
return center_price * self.step_percent
return center_price * self.step_percent
def get_grid_levels(
self,
center_price: float,
base_qty: float = 0.0001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid levels.
buy_center - anchor for BUY levels (default: center_price)
sell_center - anchor for SELL levels (default: center_price)
If they equal center_price, behaviour matches the legacy symmetric grid.
If they differ, BUY is built around buy_center and SELL around sell_center.
Used by Kronos logic: at bias=up the BUY anchor stays near current_price (aggressive),
the SELL anchor is shifted toward the forecasted price; at bias=down - mirrored.
Modes:
- arithmetic (default): levels at center ± n*step$
- geometric: levels at center / ratio^n and center * ratio^n
ratio = 1 + step_percent (constant % step).
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
levels = []
if self.grid_mode == "geometric" and self.step_percent > 0:
ratio = 1.0 + self.step_percent
for n in range(1, self.levels_count + 1):
factor = ratio ** n
buy_price = buy_center / factor
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center * factor
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
else:
step = self.calc_step(center_price)
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = buy_center - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order.
Auto-shrinks qty if balance is insufficient for full size.
Min qty = 0.0001 BTC (Tradernet minimum lot for BTC-USDT).
"""
min_qty = 0.0001
cost = price * qty
if self.balance >= cost:
# Full size fits
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
# Try to fit a smaller qty into available balance
affordable_qty = round(self.balance / price, 8) if price > 0 else 0
if affordable_qty >= min_qty:
cost = price * affordable_qty
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": affordable_qty, "level_id": level_id, "filled": False})
return True
# Insufficient balance for even min lot — warn loudly (was silent before 15.06 patch)
import logging
logging.getLogger(__name__).warning(
f"[DEMO] skip BUY @ ${price:,.2f} (qty={qty} BTC): "
f"balance=${self.balance:.2f} < min_cost=${min_qty*price:.2f} "
f"(short by ${min_qty*price - self.balance:.2f}); level_id={level_id}"
)
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
opened_at=time.time(),
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def _cancel_open_sells(self, reason: str) -> int:
"""Отменить все висящие (не-filled) SELL-ордера. Возвращает кол-во отменённых.
Вызывается при ЗАКРЫТИИ позиции — иначе SELL-ордер висит и при fill
без открытой позиции даёт фантомный pnl=0 и теряет proceeds.
"""
before = len(self.orders)
self.orders = [o for o in self.orders if o["filled"] or o["side"] != "SELL"]
cancelled = before - len(self.orders)
if cancelled > 0:
# Логгируем на уровне INFO через глобальный logger, если есть
import logging
logging.getLogger("grid").info(
f"[DEMO] cancelled {cancelled} open SELL order(s): {reason}"
)
return cancelled
def cancel_open_buys(self, reason: str = "") -> int:
"""Cancel all unfilled BUY orders AND refund reserved balance.
For BUY orders the cost (price * qty) was debited at
place_buy_order — cancelling must credit it back, otherwise
every grid rebuild silently drains the wallet. Returns the
count of cancelled orders. SELL orders are not touched
(no balance was reserved for them).
"""
new_orders = []
refunded = 0.0
cancelled = 0
for o in self.orders:
if o["side"] == "BUY" and not o.get("filled"):
refunded += o["price"] * o["qty"]
cancelled += 1
else:
new_orders.append(o)
self.orders = new_orders
if cancelled > 0:
self.balance += refunded
import logging
logging.getLogger("grid").info(
f"[DEMO] cancelled {cancelled} open BUY order(s) and refunded "
f"${refunded:.2f}; reason={reason}"
)
return cancelled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: SELL закрыл позицию → остальные SELL ордера-зомби
# больше не нужны, отменяем.
self._cancel_open_sells("position closed via SELL fill")
else:
# SELL-zombie: ордер заполнился, но позиции уже нет.
# Не начисляем proceeds (нет BTC), логгируем warning.
import logging
logging.getLogger("grid").warning(
f"[DEMO] SELL-zombie fill @ ${order['price']:,.2f} qty={order['qty']} BTC "
f"— no position to close, no proceeds credited"
)
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
exit_ts = time.time()
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
"entry_time": datetime.fromtimestamp(self.position.opened_at, tz=timezone.utc).isoformat() if self.position.opened_at else None,
"exit_time": datetime.fromtimestamp(exit_ts, tz=timezone.utc).isoformat(),
})
self.position = None
# BUGFIX: TP закрыл позицию → SELL-ордера-зомби отменяем
self._cancel_open_sells("position closed via TAKE_PROFIT")
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+272
View File
@@ -0,0 +1,272 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(self, center_price: float, base_qty: float = 0.001) -> list[GridLevel]:
"""Build grid levels around center_price."""
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = center_price - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = center_price + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order."""
cost = price * qty
if self.balance >= cost:
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
})
self.position = None
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
})
self.position = None
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+272
View File
@@ -0,0 +1,272 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(self, center_price: float, base_qty: float = 0.001) -> list[GridLevel]:
"""Build grid levels around center_price."""
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = center_price - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = center_price + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order."""
cost = price * qty
if self.balance >= cost:
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
})
self.position = None
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
})
self.position = None
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+272
View File
@@ -0,0 +1,272 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(self, center_price: float, base_qty: float = 0.001) -> list[GridLevel]:
"""Build grid levels around center_price."""
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = center_price - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = center_price + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order."""
cost = price * qty
if self.balance >= cost:
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
})
self.position = None
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
})
self.position = None
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+302
View File
@@ -0,0 +1,302 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(
self,
center_price: float,
base_qty: float = 0.001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid levels.
buy_center - anchor for BUY levels (default: center_price)
sell_center - anchor for SELL levels (default: center_price)
If they equal center_price, behaviour matches the legacy symmetric grid.
If they differ, BUY is built around buy_center and SELL around sell_center.
Used by Kronos logic: at bias=up the BUY anchor stays near current_price (aggressive),
the SELL anchor is shifted toward the forecasted price; at bias=down - mirrored.
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = buy_center - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order.
Auto-shrinks qty if balance is insufficient for full size.
Min qty = 0.0001 BTC (Tradernet minimum lot for BTC-USDT).
"""
min_qty = 0.0001
cost = price * qty
if self.balance >= cost:
# Full size fits
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
# Try to fit a smaller qty into available balance
affordable_qty = round(self.balance / price, 8) if price > 0 else 0
if affordable_qty >= min_qty:
cost = price * affordable_qty
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": affordable_qty, "level_id": level_id, "filled": False})
return True
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
})
self.position = None
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
})
self.position = None
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+331
View File
@@ -0,0 +1,331 @@
"""
Margin Grid Bot — Grid engine.
Builds a grid of BUY/SELL orders around a center price.
Supports both real trading and demo simulation.
"""
import math
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GridLevel:
level_id: int # negative=below center (BUY), positive=above center (SELL)
price: float
side: str # "BUY" or "SELL"
qty: float
step_num: int # 1..N from center
filled: bool = False
def to_dict(self):
return {
"level_id": self.level_id,
"price": round(self.price, 8),
"side": self.side,
"qty": self.qty,
"step_num": self.step_num,
"filled": self.filled,
}
@dataclass
class GridState:
center_price: float = 0.0
step_percent: float = 0.005
step_price: float = 0.0
levels: list = field(default_factory=list)
total_levels: int = 0
take_profit_percent: float = 0.5
symbol: str = "USDC_BTC"
mode: str = "demo" # "demo" or "live"
last_update: float = 0.0
class GridEngine:
"""
Grid trading engine.
Buy orders below center: center - n×step (level_id: -1, -2, ... -N)
Sell orders above center: center + n×step (level_id: +1, +2, ... +N)
Each round-trip = profit = step × qty.
When a level is filled → check if total PnL >= take_profit → close position.
"""
def __init__(
self,
levels: int = 10,
step_percent: float = 0.005,
take_profit_percent: float = 0.5,
symbol: str = "USDC_BTC",
mode: str = "demo",
):
self.levels_count = levels
self.step_percent = step_percent
self.take_profit_percent = take_profit_percent
self.symbol = symbol
self.mode = mode
def calc_step(self, center_price: float) -> float:
return center_price * self.step_percent
def get_grid_levels(
self,
center_price: float,
base_qty: float = 0.001,
buy_center: float | None = None,
sell_center: float | None = None,
) -> list[GridLevel]:
"""Build grid levels.
buy_center - anchor for BUY levels (default: center_price)
sell_center - anchor for SELL levels (default: center_price)
If they equal center_price, behaviour matches the legacy symmetric grid.
If they differ, BUY is built around buy_center and SELL around sell_center.
Used by Kronos logic: at bias=up the BUY anchor stays near current_price (aggressive),
the SELL anchor is shifted toward the forecasted price; at bias=down - mirrored.
"""
if buy_center is None:
buy_center = center_price
if sell_center is None:
sell_center = center_price
step = self.calc_step(center_price)
levels = []
for n in range(1, self.levels_count + 1):
offset = n * step
buy_price = buy_center - offset
if buy_price <= 0:
continue
levels.append(GridLevel(
level_id=-n,
price=round(buy_price, 8),
side="BUY",
qty=base_qty,
step_num=n,
))
sell_price = sell_center + offset
levels.append(GridLevel(
level_id=+n,
price=round(sell_price, 8),
side="SELL",
qty=base_qty,
step_num=n,
))
# Sort: BUY first (negatives), then SELL (positives)
levels.sort(key=lambda x: x.level_id)
return levels
def build_state(self, current_price: float, base_qty: float = 0.001) -> GridState:
levels = self.get_grid_levels(current_price, base_qty)
step = self.calc_step(current_price)
return GridState(
center_price=current_price,
step_percent=self.step_percent,
step_price=step,
levels=[l.to_dict() for l in levels],
total_levels=len(levels),
take_profit_percent=self.take_profit_percent,
symbol=self.symbol,
mode=self.mode,
last_update=0.0,
)
def should_rebuild(self, current_price: float, center_price: float, threshold: float = 3) -> bool:
"""Rebuild grid if price moved by threshold levels."""
step = self.calc_step(center_price)
if step == 0:
return False
levels_moved = abs(current_price - center_price) / step
return levels_moved >= threshold
@dataclass
class DemoPosition:
entry_price: float
qty: float
side: str # "BUY" or "SELL"
opened_at: float = 0.0
def current_value(self, current_price: float) -> float:
if self.side == "BUY":
return self.qty * current_price
else:
return self.qty * (2 * self.entry_price - current_price)
def pnl_percent(self, current_price: float) -> float:
if self.side == "BUY":
return (current_price - self.entry_price) / self.entry_price * 100
else:
return (self.entry_price - current_price) / self.entry_price * 100
class DemoSimulator:
"""
Simulates trading on a virtual wallet of DEMO_START_BALANCE USDC.
Uses real market prices from Tradernet.
"""
def __init__(self, start_balance: float = 100.0):
self.start_balance = start_balance
self.balance = start_balance # USDC
self.position: Optional[DemoPosition] = None
self.orders: list = [] # simulated open orders
self.trade_log: list = []
def reset(self):
self.balance = self.start_balance
self.position = None
self.orders = []
self.trade_log = []
def place_buy_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a BUY limit order.
Auto-shrinks qty if balance is insufficient for full size.
Min qty = 0.0001 BTC (Tradernet minimum lot for BTC-USDT).
"""
min_qty = 0.0001
cost = price * qty
if self.balance >= cost:
# Full size fits
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
# Try to fit a smaller qty into available balance
affordable_qty = round(self.balance / price, 8) if price > 0 else 0
if affordable_qty >= min_qty:
cost = price * affordable_qty
self.balance -= cost
self.orders.append({"side": "BUY", "price": price, "qty": affordable_qty, "level_id": level_id, "filled": False})
return True
return False
def place_sell_order(self, price: float, qty: float, level_id: int):
"""Simulate placing a SELL limit order."""
if self.position and self.position.side == "BUY" and self.position.qty >= qty:
self.orders.append({"side": "SELL", "price": price, "qty": qty, "level_id": level_id, "filled": False})
return True
return False
def check_fill_buy(self, current_price: float) -> list[dict]:
"""Check if any BUY orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "BUY" and current_price <= order["price"]:
order["filled"] = True
self.position = DemoPosition(
entry_price=order["price"],
qty=order["qty"],
side="BUY",
)
self.balance += order["price"] * order["qty"] # rest of balance
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def _cancel_open_sells(self, reason: str) -> int:
"""Отменить все висящие (не-filled) SELL-ордера. Возвращает кол-во отменённых.
Вызывается при ЗАКРЫТИИ позиции — иначе SELL-ордер висит и при fill
без открытой позиции даёт фантомный pnl=0 и теряет proceeds.
"""
before = len(self.orders)
self.orders = [o for o in self.orders if o["filled"] or o["side"] != "SELL"]
cancelled = before - len(self.orders)
if cancelled > 0:
# Логгируем на уровне INFO через глобальный logger, если есть
import logging
logging.getLogger("grid").info(
f"[DEMO] cancelled {cancelled} open SELL order(s): {reason}"
)
return cancelled
def check_fill_sell(self, current_price: float) -> list[dict]:
"""Check if any SELL orders should be filled at current_price."""
filled = []
for order in self.orders:
if not order["filled"] and order["side"] == "SELL" and current_price >= order["price"]:
order["filled"] = True
# Close BUY position
if self.position and self.position.side == "BUY":
proceeds = order["qty"] * order["price"]
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "BUY→SELL",
"entry": self.position.entry_price,
"exit": order["price"],
"qty": order["qty"],
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl / (self.position.qty * self.position.entry_price) * 100, 2),
})
self.position = None
# BUGFIX: SELL закрыл позицию → остальные SELL ордера-зомби
# больше не нужны, отменяем.
self._cancel_open_sells("position closed via SELL fill")
else:
# SELL-zombie: ордер заполнился, но позиции уже нет.
# Не начисляем proceeds (нет BTC), логгируем warning.
import logging
logging.getLogger("grid").warning(
f"[DEMO] SELL-zombie fill @ ${order['price']:,.2f} qty={order['qty']} BTC "
f"— no position to close, no proceeds credited"
)
filled.append(order)
self.orders = [o for o in self.orders if not o["filled"]]
return filled
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""If position PnL >= take_profit_pct, close it."""
if not self.position:
return False
pnl_pct = self.position.pnl_percent(current_price)
if pnl_pct >= take_profit_pct:
# Force close at current price
if self.position.side == "BUY":
proceeds = self.position.qty * current_price
self.balance += proceeds
pnl = proceeds - (self.position.qty * self.position.entry_price)
self.trade_log.append({
"side": "TAKE_PROFIT",
"entry": self.position.entry_price,
"exit": current_price,
"qty": self.position.qty,
"pnl_usdc": round(pnl, 8),
"pnl_pct": round(pnl_pct, 2),
})
self.position = None
# BUGFIX: TP закрыл позицию → SELL-ордера-зомби отменяем
self._cancel_open_sells("position closed via TAKE_PROFIT")
return True
return False
def get_status(self, current_price: float) -> dict:
pnl_usdc = 0.0
pnl_pct = 0.0
unrealized = 0.0
if self.position:
if self.position.side == "BUY":
unrealized = (current_price - self.position.entry_price) * self.position.qty
pnl_pct = self.position.pnl_percent(current_price)
pnl_usdc = unrealized
return {
"balance": round(self.balance, 8),
"position": {
"side": self.position.side if self.position else None,
"entry_price": self.position.entry_price if self.position else None,
"qty": self.position.qty if self.position else None,
"current_value": round(self.position.current_value(current_price) if self.position else 0.0, 8),
"unrealized_pnl": round(unrealized, 8),
"pnl_pct": round(pnl_pct, 2),
} if self.position else None,
"open_orders": len(set(o["level_id"] for o in self.orders if not o["filled"])),
"total_trades": len(self.trade_log),
"total_pnl": round(sum(t["pnl_usdc"] for t in self.trade_log), 8),
"last_trade": self.trade_log[-1] if self.trade_log else None,
}
+124
View File
@@ -0,0 +1,124 @@
"""
Technical indicators for grid-bot strategy.
Pure-math module (no API calls, no I/O). Used by main.py to compute
SMA(20) and ATR(14) over Tradernet's `getHloc` candles, replacing
the reactive "center = current_price" logic with a stable, MA-anchored
grid.
Functions:
compute_sma(prices, period=20)
compute_atr(candles, period=14)
compute_sma_atr(candles, sma_period=20, atr_period=14)
"""
from typing import Sequence
def compute_sma(prices: Sequence[float], period: int = 20) -> float | None:
"""Simple Moving Average over the last `period` values of `prices`.
Returns None if fewer than `period` prices available.
"""
if len(prices) < period:
return None
return sum(prices[-period:]) / period
def compute_atr(candles: Sequence[dict], period: int = 14) -> float | None:
"""Average True Range over the last `period` candles.
Each candle: dict with keys 'o','h','l','c','v' (or 'open','high','low','close','volume').
True Range (TR) = max(high - low, |high - prev_close|, |low - prev_close|)
ATR = mean of last `period` TRs.
Returns None if fewer than `period + 1` candles available
(we need `period` TRs, each TR compares to previous close).
"""
if len(candles) < period + 1:
return None
def _h(c):
return c.get("h", c.get("high"))
def _l(c):
return c.get("l", c.get("low"))
def _c(c):
return c.get("c", c.get("close"))
trs = []
for i in range(1, len(candles)):
h, l, pc = _h(candles[i]), _l(candles[i]), _c(candles[i - 1])
if None in (h, l, pc):
continue
high_low = h - l
high_pc = abs(h - pc)
low_pc = abs(l - pc)
tr = max(high_low, high_pc, low_pc)
trs.append(tr)
if len(trs) < period:
return None
return sum(trs[-period:]) / period
def compute_sma_atr(
candles: Sequence[dict],
sma_period: int = 20,
atr_period: int = 14,
width_atr_mult: float = 2.0,
) -> dict:
"""Compute SMA + ATR + derived grid range in one call.
Returns:
{
"sma": float | None,
"atr": float | None,
"sma_period": int,
"atr_period": int,
"width_atr_mult": float,
"n_candles": int,
"last_close": float | None,
"range_h": float | None, # sma + width_atr_mult * atr (upper grid bound)
"range_l": float | None, # sma - width_atr_mult * atr (lower grid bound)
}
All values None if insufficient data.
"""
closes = []
for c in candles:
cl = c.get("c", c.get("close"))
if cl is not None:
closes.append(cl)
sma = compute_sma(closes, sma_period)
atr = compute_atr(candles, atr_period)
out = {
"sma": sma,
"atr": atr,
"sma_period": sma_period,
"atr_period": atr_period,
"width_atr_mult": width_atr_mult,
"n_candles": len(candles),
"last_close": closes[-1] if closes else None,
"range_h": None,
"range_l": None,
}
if sma is not None and atr is not None:
out["range_h"] = sma + width_atr_mult * atr
out["range_l"] = sma - width_atr_mult * atr
return out
if __name__ == "__main__":
# Smoke test on synthetic data
synth_prices = [100 + i * 0.1 + (i % 5) * 0.05 for i in range(30)]
synth_candles = [
{"o": p, "h": p + 0.5, "l": p - 0.5, "c": p, "v": 100}
for p in synth_prices
]
print("Synthetic smoke test:")
print(f" Last close: {synth_prices[-1]:.4f}")
print(f" SMA(20): {compute_sma(synth_prices, 20):.4f}")
print(f" ATR(14): {compute_atr(synth_candles, 14):.4f}")
full = compute_sma_atr(synth_candles)
print(f" Range: ${full['range_l']:.4f} — ${full['range_h']:.4f}")
+78
View File
@@ -0,0 +1,78 @@
# Установка и запуск Kronos advisor как systemd-timer
## Установка (на сервере)
```bash
# 1) Копируем unit-файлы
sudo cp kronos-advisor.service /etc/systemd/system/
sudo cp kronos-advisor.timer /etc/systemd/system/
# 2) Активируем
sudo systemctl daemon-reload
sudo systemctl enable kronos-advisor.timer
sudo systemctl start kronos-advisor.timer
# 3) Проверяем
systemctl list-timers kronos-advisor.timer
systemctl status kronos-advisor.timer
```
## Логи
```bash
journalctl -u kronos-advisor.service -f
tail -f kronos-advisor.log
```
## Тестовый запуск вручную
```bash
cd /root/grid-bot
source kronos-venv/bin/activate
python kronos_advisor_main.py --live
cat kronos_advice.json
```
## Изменение расписания
`kronos-advisor.timer`, поле `OnCalendar`:
- `*:0/15` — каждые 15 мин (по умолчанию)
- `*:0/5` — каждые 5 мин
- `*:0/30` — каждые 30 мин
- `hourly` — раз в час
- `daily` — раз в сутки
После правки:
```bash
sudo systemctl daemon-reload
sudo systemctl restart kronos-advisor.timer
```
## Безопасность
- `--live` подтягивает TRADENET_PRIVATE_KEY из .env → **не запускай advisor в live-режиме
с пустыми/скомпрометированными ключами**.
- Советник НИКОГДА не выставляет ордера — только пишет kronos_advice.json.
- Решение о применении совета — за `main.py`, под флагом `KRONOS_LIVE_ENABLED`.
- Если `KRONOS_ENABLED=false` в .env, советник ничего не делает (читается в `read_kronos_advice`).
## Что откуда берётся
```
grid-bot/
├── kronos-venv/ # python venv (создаётся setup-скриптом)
├── kronos_advisor_main.py # CLI, запускается из systemd
├── kronos_advice.json # выход, читается main.py
├── kronos-advisor.service # этот файл
├── kronos-advisor.timer # и этот
└── .env # TRADENET_* + KRONOS_ENABLED
```
## Удаление
```bash
sudo systemctl stop kronos-advisor.timer
sudo systemctl disable kronos-advisor.timer
sudo rm /etc/systemd/system/kronos-advisor.{service,timer}
sudo systemctl daemon-reload
```
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Kronos advisor — generate kronos_advice.json
After=network.target
[Service]
Type=oneshot
User=root
WorkingDirectory=/root/grid-bot
EnvironmentFile=/root/grid-bot/.env
# NB: --live включает TRADENET-источник. Если хочешь сначала Binance для теста,
# замени --source binance и убери --live.
ExecStart=/root/grid-bot/kronos-venv/bin/python /root/grid-bot/kronos_advisor_main.py --live
StandardOutput=append:/root/grid-bot/kronos-advisor.log
StandardError=append:/root/grid-bot/kronos-advisor.log
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Run Kronos advisor every 15 minutes
Requires=kronos-advisor.service
[Timer]
# Каждые 15 минут
OnCalendar=*:0/5
# При включении — запустить сразу
Persistent=true
# Случайный jitter ±60с, чтобы не нагружать сеть пиками
RandomizedDelaySec=60
[Install]
WantedBy=timers.target
+247
View File
@@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
+70
View File
@@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /root/grid-bot/kronos-venv)
else
# use the path as-is
export VIRTUAL_ENV=/root/grid-bot/kronos-venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(kronos-venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(kronos-venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
+27
View File
@@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /root/grid-bot/kronos-venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(kronos-venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(kronos-venv) '
endif
alias pydoc python -m pydoc
rehash
+69
View File
@@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /root/grid-bot/kronos-venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(kronos-venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(kronos-venv) '
end
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from dotenv.__main__ import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from numpy.f2py.f2py2e import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from fontTools.__main__ import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from huggingface_hub.commands.huggingface_cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from isympy import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from numpy._configtool import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from fontTools.merge import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from fontTools.subset import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+1
View File
@@ -0,0 +1 @@
python3
+1
View File
@@ -0,0 +1 @@
/usr/bin/python3
+1
View File
@@ -0,0 +1 @@
python3
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from huggingface_hub.inference._mcp.cli import app
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(app())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from torch.distributed.flight_recorder.fr_trace import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from torch.distributed.run import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from tqdm.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/root/grid-bot/kronos-venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from fontTools.ttx import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

Some files were not shown because too many files have changed in this diff Show More