Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets
This commit is contained in:
@@ -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
@@ -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_*/
|
||||||
@@ -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))
|
||||||
|
```
|
||||||
@@ -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` падает — пришли мне вывод ошибки, разберёмся.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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-отчётам
|
||||||
@@ -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: вариант А — выключить навсегда)
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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`
|
||||||
@@ -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 режиме перед реальной торговлей.**
|
||||||
|
⚠️ **Инвестируйте только те средства, которые готовы потерять.**
|
||||||
@@ -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 []
|
||||||
@@ -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 []
|
||||||
@@ -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
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[33mGET /ecosystem.config.js HTTP/1.1[0m" 404 -
|
||||||
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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)
|
||||||
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[33mGET /.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:16,941 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:16] "[33mGET /.env.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,001 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /.env.sample.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,061 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /.env.local.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,122 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /.env.production.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,182 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /config.dev.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,242 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /config.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,304 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /twilio/.env.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,364 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /config/.env.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,424 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /sendgrid/.env.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,484 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /.env.php.bak HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,544 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /.env.php-bak HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,604 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /.env.php-backup HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,663 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /.env.php.backup HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,723 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /test/.env.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,783 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /.env.example.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,843 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /app_dev.php/_profiler/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,904 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[33mGET /var/.env.local.php HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:17,964 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:17] "[31m[1mGET /api/.env HTTP/1.1[0m" 401 -
|
||||||
|
2026-06-22 06:01:18,024 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /app/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,084 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /admin/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,144 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /.env.production HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,203 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /.env.save HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,264 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /backend/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,324 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /core/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,384 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /laravel/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,444 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /.env.example HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,505 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /.env.prod HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,565 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /.env.local HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,625 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /.env.bak HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,684 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /public/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,745 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /.env.dev HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,805 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /.env.backup HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,866 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /local/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,926 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /config/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:18,986 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:18] "[33mGET /.env.old HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,045 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /old/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,105 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /apps/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,169 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /new/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,229 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /vendor/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,289 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /web/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,349 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /dev/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,408 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /wp-content/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,469 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /crm/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,529 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /.env.development HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,589 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /www/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,649 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /library/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,709 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /.env.stage HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,769 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /src/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,828 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /application/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,888 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /prod/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:19,948 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:19] "[33mGET /staging/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:20,008 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "[33mGET /.env.test HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:20,068 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "[33mGET /.env.dist HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:20,128 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "[33mGET /.env.production.local HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:20,189 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "[33mGET /.env_sample HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:20,248 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "[33mGET /wp-admin/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:20,308 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "[33mGET /conf/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:01:20,370 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:01:20] "[33mGET /.env.development.local HTTP/1.1[0m" 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] "[36mGET /static/chartjs-plugin-annotation.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2026-06-22 06:02:07,381 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:02:07] "[36mGET /static/chart.umd.min.js HTTP/1.1[0m" 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] "[31m[1mCNXN\x00\x00\x00\x01\x00\x00\x04\x00\x1b\x00\x00\x00M[0m" 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] "[31m[1mGET / HTTP/1.1[0m" 401 -
|
||||||
|
2026-06-22 06:22:49,797 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:49] "[33mGET /.env.staging HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:49,905 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:49] "[33mGET /.env.backup HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:50,020 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "[33mGET /.env.save HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:50,140 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "[33mGET /env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:50,248 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "[33mGET /admin/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:50,358 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "[33mGET /server/.env.production HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:50,470 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "[33mGET /.env.old HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:50,577 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "[33mGET /.dotenv HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:50,696 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "[33mGET /.env.prod HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:50,806 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "[33mGET /.env.development HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:50,911 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:50] "[33mGET /.env.stage HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:51,036 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "[33mGET /config/.env.local HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:51,152 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "[33mGET /web/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:51,262 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "[33mGET /repository/.env HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:51,378 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "[33mGET /.env.dev HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:51,487 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "[33mGET /.env.testing HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:51,595 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "[33mGET /.envrc HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:22:51,706 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:22:51] "[33mGET /.envrc.local HTTP/1.1[0m" 404 -
|
||||||
|
2026-06-22 06:23:08,261 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:23:08] "[33mGET /api/kill-switch/status HTTP/1.1[0m" 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 -
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[35m[1mGET /api/grid HTTP/1.1[0m" 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] "[35m[1mGET /api/grid HTTP/1.1[0m" 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] "[35m[1mGET /api/grid HTTP/1.1[0m" 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] "[31m[1mGET / HTTP/1.1[0m" 401 -
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[31m[1mCNXN\x00\x00\x00\x01\x00\x00\x04\x00\x1b\x00\x00\x00M[0m" 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 -
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[33mGET /favicon.ico HTTP/1.1[0m" 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 -
|
||||||
@@ -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.
|
||||||
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[36mGET /static/chart.umd.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2026-06-22 06:47:33,308 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:47:33] "[36mGET /static/chartjs-plugin-annotation.min.js HTTP/1.1[0m" 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 -
|
||||||
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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%
|
||||||
@@ -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.
|
||||||
@@ -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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[36mGET /static/chartjs-plugin-annotation.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2026-06-22 06:50:46,402 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:46] "[36mGET /static/chart.umd.min.js HTTP/1.1[0m" 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] "[36mGET /static/chart.umd.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2026-06-22 06:50:47,812 INFO werkzeug 127.0.0.1 - - [22/Jun/2026 06:50:47] "[36mGET /static/chartjs-plugin-annotation.min.js HTTP/1.1[0m" 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 -
|
||||||
@@ -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
@@ -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
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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"))
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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
@@ -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}")
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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
Reference in New Issue
Block a user