Stage 03.1 - mock exchange integration
This commit is contained in:
@@ -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
|
||||||
@@ -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",
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
|||||||
1
app/src/integrations/exchange/__init__.py
Normal file
1
app/src/integrations/exchange/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Exchange integration package."""
|
||||||
37
app/src/integrations/exchange/mock_data.py
Normal file
37
app/src/integrations/exchange/mock_data.py
Normal 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"),
|
||||||
|
]
|
||||||
26
app/src/integrations/exchange/models.py
Normal file
26
app/src/integrations/exchange/models.py
Normal 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
|
||||||
38
app/src/integrations/exchange/service.py
Normal file
38
app/src/integrations/exchange/service.py
Normal 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()
|
||||||
@@ -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)
|
||||||
|
|||||||
14
docs/decisions/0005-exchange-integration.md
Normal file
14
docs/decisions/0005-exchange-integration.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 0005 — Exchange Integration Start
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
Начать интеграцию с биржей не с реальных ордеров, а с mock / readonly уровня.
|
||||||
|
|
||||||
|
## Причины
|
||||||
|
- это безопаснее
|
||||||
|
- это позволяет сначала стабилизировать transport/config/UX
|
||||||
|
- это дает быстрый результат в интерфейсе
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
- первые экраны работают в mock mode
|
||||||
|
- реальный REST client будет добавлен позже
|
||||||
|
- рынок и система начинают зависеть от exchange service, а не от raw handlers logic
|
||||||
189
docs/git_flow_dzentra_bot.md
Normal file
189
docs/git_flow_dzentra_bot.md
Normal 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
|
||||||
45
docs/stages/stage-03-1-integration-mock.md
Normal file
45
docs/stages/stage-03-1-integration-mock.md
Normal 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"
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user