diff --git a/app/.env.example b/app/.env.example
index 9ad2539..f028aac 100644
--- a/app/.env.example
+++ b/app/.env.example
@@ -2,4 +2,12 @@ BOT_TOKEN=PUT_YOUR_TELEGRAM_BOT_TOKEN_HERE
BOT_PARSE_MODE=HTML
APP_ENV=dev
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
\ No newline at end of file
diff --git a/app/src/core/config.py b/app/src/core/config.py
index 0eaae85..8e683fa 100644
--- a/app/src/core/config.py
+++ b/app/src/core/config.py
@@ -1,17 +1,11 @@
from __future__ import annotations
-
import os
from dataclasses import dataclass
from pathlib import Path
-
from dotenv import load_dotenv
-
-
BASE_DIR = Path(__file__).resolve().parents[2]
ENV_FILE = BASE_DIR / ".env"
load_dotenv(ENV_FILE)
-
-
@dataclass(slots=True)
class Settings:
bot_token: str
@@ -19,17 +13,40 @@ class Settings:
app_env: str
log_level: 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:
bot_token = os.getenv("BOT_TOKEN", "").strip()
if not bot_token:
raise RuntimeError("BOT_TOKEN is not set in app/.env")
-
return Settings(
bot_token=bot_token,
bot_parse_mode=os.getenv("BOT_PARSE_MODE", "HTML").strip() or "HTML",
app_env=os.getenv("APP_ENV", "dev").strip() or "dev",
log_level=os.getenv("LOG_LEVEL", "INFO").strip().upper() or "INFO",
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",
+ )
\ No newline at end of file
diff --git a/app/src/core/system_status.py b/app/src/core/system_status.py
index 3bc652a..f4acad2 100644
--- a/app/src/core/system_status.py
+++ b/app/src/core/system_status.py
@@ -3,8 +3,9 @@ from __future__ import annotations
import platform
from dataclasses import dataclass
-from src.core.constants import APP_NAME, APP_VERSION
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)
@@ -22,11 +23,22 @@ class SystemSnapshot:
python_version: str
os_name: str
timezone_name: str
+ exchange_enabled: bool
+ exchange_name: str
components: list[ComponentStatus]
def get_system_snapshot() -> SystemSnapshot:
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 = [
ComponentStatus(
@@ -41,8 +53,8 @@ def get_system_snapshot() -> SystemSnapshot:
),
ComponentStatus(
name="Биржа",
- state="🟡 не подключена",
- details="Интеграция с биржей будет добавлена на следующем этапе.",
+ state=exchange_state,
+ details=exchange_health.message,
),
ComponentStatus(
name="База данных",
@@ -58,6 +70,8 @@ def get_system_snapshot() -> SystemSnapshot:
python_version=platform.python_version(),
os_name=f"{platform.system()} {platform.release()}",
timezone_name=settings.tz,
+ exchange_enabled=settings.exchange_enabled,
+ exchange_name=settings.exchange_name,
components=components,
)
@@ -83,7 +97,9 @@ def build_system_text() -> str:
f"- env: {snapshot.app_env}\n"
f"- python: {snapshot.python_version}\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"
"Справка\n"
"/start — стартовый экран\n"
"/menu — показать меню\n"
diff --git a/app/src/integrations/exchange/__init__.py b/app/src/integrations/exchange/__init__.py
new file mode 100644
index 0000000..1f56c7d
--- /dev/null
+++ b/app/src/integrations/exchange/__init__.py
@@ -0,0 +1 @@
+"""Exchange integration package."""
diff --git a/app/src/integrations/exchange/mock_data.py b/app/src/integrations/exchange/mock_data.py
new file mode 100644
index 0000000..36ee4c9
--- /dev/null
+++ b/app/src/integrations/exchange/mock_data.py
@@ -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"),
+ ]
diff --git a/app/src/integrations/exchange/models.py b/app/src/integrations/exchange/models.py
new file mode 100644
index 0000000..27389bf
--- /dev/null
+++ b/app/src/integrations/exchange/models.py
@@ -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
diff --git a/app/src/integrations/exchange/service.py b/app/src/integrations/exchange/service.py
new file mode 100644
index 0000000..b05758d
--- /dev/null
+++ b/app/src/integrations/exchange/service.py
@@ -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()
diff --git a/app/src/telegram/handlers/market.py b/app/src/telegram/handlers/market.py
index af95359..12b0a55 100644
--- a/app/src/telegram/handlers/market.py
+++ b/app/src/telegram/handlers/market.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from aiogram import F, Router
from aiogram.types import Message
-from src.telegram.menus import MARKET_TEXT
+from src.integrations.exchange.service import ExchangeService
router = Router(name="market")
@@ -9,4 +11,15 @@ router = Router(name="market")
@router.message(F.text == "📈 Рынок")
async def open_market(message: Message) -> None:
- await message.answer(MARKET_TEXT)
+ service = ExchangeService()
+ ticker = service.get_price()
+
+ text = (
+ "📈 Рынок\n\n"
+ f"Символ: {ticker.symbol}\n"
+ f"Цена: {ticker.price:.2f}\n"
+ f"Источник: {ticker.source}\n"
+ f"Обновлено: {ticker.updated_at}"
+ )
+
+ await message.answer(text)
diff --git a/docs/decisions/0005-exchange-integration.md b/docs/decisions/0005-exchange-integration.md
new file mode 100644
index 0000000..66b7767
--- /dev/null
+++ b/docs/decisions/0005-exchange-integration.md
@@ -0,0 +1,14 @@
+# 0005 — Exchange Integration Start
+
+## Решение
+Начать интеграцию с биржей не с реальных ордеров, а с mock / readonly уровня.
+
+## Причины
+- это безопаснее
+- это позволяет сначала стабилизировать transport/config/UX
+- это дает быстрый результат в интерфейсе
+
+## Последствия
+- первые экраны работают в mock mode
+- реальный REST client будет добавлен позже
+- рынок и система начинают зависеть от exchange service, а не от raw handlers logic
diff --git a/docs/git_flow_dzentra_bot.md b/docs/git_flow_dzentra_bot.md
new file mode 100644
index 0000000..139fac8
--- /dev/null
+++ b/docs/git_flow_dzentra_bot.md
@@ -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
diff --git a/docs/stages/stage-03-1-integration-mock.md b/docs/stages/stage-03-1-integration-mock.md
new file mode 100644
index 0000000..437a4c8
--- /dev/null
+++ b/docs/stages/stage-03-1-integration-mock.md
@@ -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"
+```