Stage 03.1 - mock exchange integration

This commit is contained in:
2026-04-13 22:54:01 +03:00
parent aa21342116
commit 9166022b3c
11 changed files with 421 additions and 17 deletions

View File

@@ -2,4 +2,12 @@ BOT_TOKEN=PUT_YOUR_TELEGRAM_BOT_TOKEN_HERE
BOT_PARSE_MODE=HTML BOT_PARSE_MODE=HTML
APP_ENV=dev APP_ENV=dev
LOG_LEVEL=INFO LOG_LEVEL=INFO
TZ=Europe/Madrid TZ=Europe/Minsk
EXCHANGE_ENABLED=false
EXCHANGE_NAME=dzengi
EXCHANGE_BASE_URL=
EXCHANGE_API_KEY=
EXCHANGE_API_SECRET=
EXCHANGE_TIMEOUT_SEC=10
EXCHANGE_TESTNET=false
DEFAULT_SYMBOL=BTCUSDT

View File

@@ -1,17 +1,11 @@
from __future__ import annotations from __future__ import annotations
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parents[2] BASE_DIR = Path(__file__).resolve().parents[2]
ENV_FILE = BASE_DIR / ".env" ENV_FILE = BASE_DIR / ".env"
load_dotenv(ENV_FILE) load_dotenv(ENV_FILE)
@dataclass(slots=True) @dataclass(slots=True)
class Settings: class Settings:
bot_token: str bot_token: str
@@ -19,17 +13,40 @@ class Settings:
app_env: str app_env: str
log_level: str log_level: str
tz: str tz: str
exchange_enabled: bool
exchange_name: str
exchange_base_url: str
exchange_api_key: str
exchange_api_secret: str
exchange_timeout_sec: int
exchange_testnet: bool
default_symbol: str
def _parse_bool(raw_value: str, default: bool = False) -> bool:
value = (raw_value or "").strip().lower()
if not value:
return default
return value in {"1", "true", "yes", "on"}
def _parse_int(raw_value: str, default: int) -> int:
value = (raw_value or "").strip()
if not value:
return default
return int(value)
def load_settings() -> Settings: def load_settings() -> Settings:
bot_token = os.getenv("BOT_TOKEN", "").strip() bot_token = os.getenv("BOT_TOKEN", "").strip()
if not bot_token: if not bot_token:
raise RuntimeError("BOT_TOKEN is not set in app/.env") raise RuntimeError("BOT_TOKEN is not set in app/.env")
return Settings( return Settings(
bot_token=bot_token, bot_token=bot_token,
bot_parse_mode=os.getenv("BOT_PARSE_MODE", "HTML").strip() or "HTML", bot_parse_mode=os.getenv("BOT_PARSE_MODE", "HTML").strip() or "HTML",
app_env=os.getenv("APP_ENV", "dev").strip() or "dev", app_env=os.getenv("APP_ENV", "dev").strip() or "dev",
log_level=os.getenv("LOG_LEVEL", "INFO").strip().upper() or "INFO", log_level=os.getenv("LOG_LEVEL", "INFO").strip().upper() or "INFO",
tz=os.getenv("TZ", "Europe/Madrid").strip() or "Europe/Madrid", tz=os.getenv("TZ", "Europe/Madrid").strip() or "Europe/Madrid",
) exchange_enabled=_parse_bool(os.getenv("EXCHANGE_ENABLED", "false")),
exchange_name=os.getenv("EXCHANGE_NAME", "dzengi").strip() or "dzengi",
exchange_base_url=os.getenv("EXCHANGE_BASE_URL", "").strip(),
exchange_api_key=os.getenv("EXCHANGE_API_KEY", "").strip(),
exchange_api_secret=os.getenv("EXCHANGE_API_SECRET", "").strip(),
exchange_timeout_sec=_parse_int(os.getenv("EXCHANGE_TIMEOUT_SEC", "10"), 10),
exchange_testnet=_parse_bool(os.getenv("EXCHANGE_TESTNET", "false")),
default_symbol=os.getenv("DEFAULT_SYMBOL", "BTCUSDT").strip() or "BTCUSDT",
)

View File

@@ -3,8 +3,9 @@ from __future__ import annotations
import platform import platform
from dataclasses import dataclass from dataclasses import dataclass
from src.core.constants import APP_NAME, APP_VERSION
from src.core.config import load_settings from src.core.config import load_settings
from src.core.constants import APP_NAME, APP_VERSION
from src.integrations.exchange.service import ExchangeService
@dataclass(slots=True) @dataclass(slots=True)
@@ -22,11 +23,22 @@ class SystemSnapshot:
python_version: str python_version: str
os_name: str os_name: str
timezone_name: str timezone_name: str
exchange_enabled: bool
exchange_name: str
components: list[ComponentStatus] components: list[ComponentStatus]
def get_system_snapshot() -> SystemSnapshot: def get_system_snapshot() -> SystemSnapshot:
settings = load_settings() settings = load_settings()
exchange_service = ExchangeService()
exchange_health = exchange_service.get_health()
if exchange_health.ok and exchange_health.mode == "mock":
exchange_state = "🟡 mock mode"
elif exchange_health.ok:
exchange_state = "🟢 OK"
else:
exchange_state = "🔴 attention"
components = [ components = [
ComponentStatus( ComponentStatus(
@@ -41,8 +53,8 @@ def get_system_snapshot() -> SystemSnapshot:
), ),
ComponentStatus( ComponentStatus(
name="Биржа", name="Биржа",
state="🟡 не подключена", state=exchange_state,
details="Интеграция с биржей будет добавлена на следующем этапе.", details=exchange_health.message,
), ),
ComponentStatus( ComponentStatus(
name="База данных", name="База данных",
@@ -58,6 +70,8 @@ def get_system_snapshot() -> SystemSnapshot:
python_version=platform.python_version(), python_version=platform.python_version(),
os_name=f"{platform.system()} {platform.release()}", os_name=f"{platform.system()} {platform.release()}",
timezone_name=settings.tz, timezone_name=settings.tz,
exchange_enabled=settings.exchange_enabled,
exchange_name=settings.exchange_name,
components=components, components=components,
) )
@@ -83,7 +97,9 @@ def build_system_text() -> str:
f"- env: {snapshot.app_env}\n" f"- env: {snapshot.app_env}\n"
f"- python: {snapshot.python_version}\n" f"- python: {snapshot.python_version}\n"
f"- os: {snapshot.os_name}\n" f"- os: {snapshot.os_name}\n"
f"- timezone: {snapshot.timezone_name}\n\n" f"- timezone: {snapshot.timezone_name}\n"
f"- exchange_enabled: {snapshot.exchange_enabled}\n"
f"- exchange_name: {snapshot.exchange_name}\n\n"
"<b>Справка</b>\n" "<b>Справка</b>\n"
"/start — стартовый экран\n" "/start — стартовый экран\n"
"/menu — показать меню\n" "/menu — показать меню\n"

View File

@@ -0,0 +1 @@
"""Exchange integration package."""

View File

@@ -0,0 +1,37 @@
from __future__ import annotations
from datetime import datetime, timezone
from src.integrations.exchange.models import BalanceSummary, ExchangeHealth, TickerPrice
def mock_exchange_health() -> ExchangeHealth:
return ExchangeHealth(
ok=True,
mode="mock",
message="Биржа работает в mock mode. Реальный API пока не подключен.",
)
def mock_ticker_price(symbol: str) -> TickerPrice:
symbol = symbol.upper().strip()
fake_prices = {
"BTCUSDT": 68425.10,
"ETHUSDT": 3521.44,
"BNBUSDT": 612.33,
}
price = fake_prices.get(symbol, 100.00)
updated_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
return TickerPrice(
symbol=symbol,
price=price,
source="mock",
updated_at=updated_at,
)
def mock_balance_summary() -> list[BalanceSummary]:
return [
BalanceSummary(currency="USDT", available=1500.00, locked=0.0, source="mock"),
BalanceSummary(currency="BTC", available=0.025, locked=0.0, source="mock"),
]

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(slots=True)
class ExchangeHealth:
ok: bool
mode: str
message: str
@dataclass(slots=True)
class TickerPrice:
symbol: str
price: float
source: str
updated_at: str
@dataclass(slots=True)
class BalanceSummary:
currency: str
available: float
locked: float
source: str

View File

@@ -0,0 +1,38 @@
from __future__ import annotations
from src.core.config import load_settings
from src.integrations.exchange.mock_data import (
mock_balance_summary,
mock_exchange_health,
mock_ticker_price,
)
from src.integrations.exchange.models import BalanceSummary, ExchangeHealth, TickerPrice
class ExchangeService:
def __init__(self) -> None:
self.settings = load_settings()
def get_health(self) -> ExchangeHealth:
if not self.settings.exchange_enabled:
return mock_exchange_health()
if not self.settings.exchange_api_key or not self.settings.exchange_api_secret:
return ExchangeHealth(
ok=False,
mode="configured_without_keys",
message="Интеграция включена, но API key / secret не заданы.",
)
return ExchangeHealth(
ok=False,
mode="real_pending",
message="Реальный REST client еще не подключен. Пока доступен только mock mode.",
)
def get_price(self, symbol: str | None = None) -> TickerPrice:
symbol_to_use = symbol or self.settings.default_symbol
return mock_ticker_price(symbol_to_use)
def get_balance_summary(self) -> list[BalanceSummary]:
return mock_balance_summary()

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
from aiogram import F, Router from aiogram import F, Router
from aiogram.types import Message from aiogram.types import Message
from src.telegram.menus import MARKET_TEXT from src.integrations.exchange.service import ExchangeService
router = Router(name="market") router = Router(name="market")
@@ -9,4 +11,15 @@ router = Router(name="market")
@router.message(F.text == "📈 Рынок") @router.message(F.text == "📈 Рынок")
async def open_market(message: Message) -> None: async def open_market(message: Message) -> None:
await message.answer(MARKET_TEXT) service = ExchangeService()
ticker = service.get_price()
text = (
"<b>📈 Рынок</b>\n\n"
f"Символ: <b>{ticker.symbol}</b>\n"
f"Цена: <b>{ticker.price:.2f}</b>\n"
f"Источник: {ticker.source}\n"
f"Обновлено: {ticker.updated_at}"
)
await message.answer(text)

View File

@@ -0,0 +1,14 @@
# 0005 — Exchange Integration Start
## Решение
Начать интеграцию с биржей не с реальных ордеров, а с mock / readonly уровня.
## Причины
- это безопаснее
- это позволяет сначала стабилизировать transport/config/UX
- это дает быстрый результат в интерфейсе
## Последствия
- первые экраны работают в mock mode
- реальный REST client будет добавлен позже
- рынок и система начинают зависеть от exchange service, а не от raw handlers logic

View File

@@ -0,0 +1,189 @@
# Git Setup Flow: dzentra_bot
## Полный процесс: от `git init` до первого `push` в Gitea
---
## 📍 0. Перейти в корень проекта
```bash
cd ~/vsprojects/dzentra_bot
pwd
```
Ожидаемо:
```
.../vsprojects/dzentra_bot
```
---
## 🧱 1. Инициализация Git
```bash
git init
```
Проверка:
```bash
git status
```
---
## 📦 2. Проверка `.gitignore`
```bash
cat .gitignore
```
Должны быть строки:
```
app/.env
app/.venv/
__pycache__/
.DS_Store
```
---
## 3. Добавить все файлы
```bash
git add .
```
Проверка:
```bash
git status
```
---
## 💾 4. Первый commit
```bash
git commit -m "bootstrap v2 stable start"
```
---
## 🔗 5. Подключить удалённый репозиторий (Gitea)
```bash
git remote add origin https://gitadmin@git.segeba.by/gitadmin/dzentra_bot.git
```
Проверка:
```bash
git remote -v
```
---
## 🌿 6. Установить ветку main
```bash
git branch -M main
```
---
## 🔐 7. Настроить сохранение токена (macOS)
```bash
git config --global credential.helper osxkeychain
```
---
## 🚀 8. Первый push
```bash
git push -u origin main
```
При запросе:
### Username:
```
gitadmin
```
### Password:
👉 вставить **Personal Access Token из Gitea**
---
## ✅ 9. Проверка
```bash
git status
git branch -vv
```
Проверить в Gitea — файлы должны появиться.
---
# 🔁 Дальнейшая работа
## Каждый цикл разработки
```bash
git status
git add .
git commit -m "описание изменения"
git push
```
---
# 🖥 Работа с проектом
## Запуск бота
```bash
cd app
source .venv/bin/activate
python -m src.main
```
## Git всегда из корня
```bash
cd ~/vsprojects/dzentra_bot
git status
```
---
# 🚀 Deploy на Synology
```bash
git pull
sudo docker compose -f infra/compose/docker-compose.yml up --build -d
```
---
# ⚠️ Важно
Не коммитить:
```
app/.env
app/.venv
ключи
пароли
логи
```
---
# 🎯 Итог
Git используется как:
- система контроля версий
- история проекта
- мост между Mac и Synology

View File

@@ -0,0 +1,45 @@
# Stage 03.1 — Mock Integration
## Цель
Оживить экран `📈 Рынок` и добавить первый статус интеграции с биржей без риска реальной торговли.
## Что добавляется
- exchange settings в `config.py`
- модели интеграции
- mock data
- exchange service
- экран `📈 Рынок` с mock price
- экран `⚙️ Система` со статусом интеграции
## Какой результат нужен
### Рынок
Пользователь нажимает `📈 Рынок` и видит:
- символ
- цену
- источник `mock`
- время обновления
### Система
Пользователь нажимает `⚙️ Система` и видит:
- Биржа: mock mode
- exchange_enabled
- exchange_name
## Почему это правильный шаг
- не нужен реальный API в первый день
- можно спокойно отладить архитектуру
- рынок перестает быть заглушкой
- system screen становится полезнее
## Как проверить
1. Запустить бота локально
2. Нажать `📈 Рынок`
3. Нажать `⚙️ Система`
4. Проверить `/help`
## Рекомендуемые commit messages
```bash
git commit -m "add exchange config and mock models"
git commit -m "connect market screen to mock exchange service"
git commit -m "show exchange mock health in system screen"
```