Stage 04.1 - storage foundation (PostgreSQL) and system dashboard UI
This commit is contained in:
@@ -4,6 +4,12 @@ APP_ENV=dev
|
||||
LOG_LEVEL=INFO
|
||||
TZ=Europe/Minsk
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=dzentra_bot
|
||||
DB_USER=dzentra_bot
|
||||
DB_PASSWORD=change_me
|
||||
|
||||
EXCHANGE_ENABLED=true
|
||||
EXCHANGE_NAME=dzengi
|
||||
EXCHANGE_BASE_URL=https://demo-api-adapter.dzengi.com
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
aiogram==3.13.1
|
||||
python-dotenv==1.0.1
|
||||
psycopg[binary]==3.2.9
|
||||
@@ -5,12 +5,14 @@ from aiogram.client.default import DefaultBotProperties
|
||||
|
||||
from src.bootstrap.logging import setup_logging
|
||||
from src.core.config import load_settings
|
||||
from src.storage.schema import init_schema
|
||||
from src.telegram.routers import setup_routers
|
||||
|
||||
|
||||
def create_app() -> tuple[Bot, Dispatcher]:
|
||||
settings = load_settings()
|
||||
setup_logging(settings.log_level)
|
||||
init_schema()
|
||||
|
||||
bot = Bot(
|
||||
token=settings.bot_token,
|
||||
|
||||
@@ -21,6 +21,11 @@ class Settings:
|
||||
exchange_timeout_sec: int
|
||||
exchange_testnet: bool
|
||||
default_symbol: str
|
||||
db_host: str
|
||||
db_port: int
|
||||
db_name: str
|
||||
db_user: str
|
||||
db_password: str
|
||||
def _parse_bool(raw_value: str, default: bool = False) -> bool:
|
||||
value = (raw_value or "").strip().lower()
|
||||
if not value:
|
||||
@@ -49,4 +54,9 @@ def load_settings() -> Settings:
|
||||
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", "BTC/USD_LEVERAGE").strip() or "BTC/USD_LEVERAGE",
|
||||
db_host=os.getenv("DB_HOST", "localhost").strip() or "localhost",
|
||||
db_port=_parse_int(os.getenv("DB_PORT", "5432"), 5432),
|
||||
db_name=os.getenv("DB_NAME", "dzentra_bot").strip() or "dzentra_bot",
|
||||
db_user=os.getenv("DB_USER", "dzentra_bot").strip() or "dzentra_bot",
|
||||
db_password=os.getenv("DB_PASSWORD", "").strip(),
|
||||
)
|
||||
@@ -1,137 +1,150 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from src.core.config import load_settings
|
||||
from src.core.constants import APP_NAME, APP_VERSION
|
||||
from src.integrations.exchange.service import ExchangeService
|
||||
from src.storage.session import check_database_health
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ComponentStatus:
|
||||
name: str
|
||||
state: str
|
||||
details: str
|
||||
details: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SystemSnapshot:
|
||||
app_name: str
|
||||
app_version: str
|
||||
app_env: str
|
||||
python_version: str
|
||||
os_name: str
|
||||
db_label: str
|
||||
timezone_name: str
|
||||
exchange_enabled: bool
|
||||
exchange_name: str
|
||||
mode_label: str
|
||||
default_symbol: str
|
||||
symbol_validation_message: str
|
||||
private_auth_message: str
|
||||
components: list[ComponentStatus]
|
||||
|
||||
|
||||
def _extract_postgres_version(raw: str) -> str:
|
||||
if not raw:
|
||||
return "PostgreSQL"
|
||||
|
||||
match = re.search(r"PostgreSQL\s+(\d+(?:\.\d+)?)", raw)
|
||||
if match:
|
||||
return f"PostgreSQL {match.group(1)}"
|
||||
|
||||
return "PostgreSQL"
|
||||
|
||||
|
||||
def _build_exchange_status(exchange_service: ExchangeService, default_symbol: str) -> ComponentStatus:
|
||||
try:
|
||||
symbol_validation = exchange_service.validate_symbol(default_symbol)
|
||||
except Exception as exc:
|
||||
return ComponentStatus(
|
||||
name="Биржа",
|
||||
state="🔴",
|
||||
details=f"Не удалось проверить инструмент: {exc}",
|
||||
)
|
||||
|
||||
exchange_health = exchange_service.get_health()
|
||||
if exchange_health.ok and symbol_validation.is_valid:
|
||||
return ComponentStatus(name="Биржа", state="🟢")
|
||||
|
||||
if not exchange_health.ok:
|
||||
return ComponentStatus(
|
||||
name="Биржа",
|
||||
state="🔴",
|
||||
details=exchange_health.message or "Ошибка подключения к API биржи.",
|
||||
)
|
||||
|
||||
return ComponentStatus(
|
||||
name="Биржа",
|
||||
state="🔴",
|
||||
details=symbol_validation.message or "Инструмент не прошел проверку.",
|
||||
)
|
||||
|
||||
|
||||
def _build_account_status(exchange_service: ExchangeService) -> ComponentStatus:
|
||||
private_auth_health = exchange_service.get_private_auth_health()
|
||||
if private_auth_health.ok:
|
||||
return ComponentStatus(name="Аккаунт", state="🟢")
|
||||
|
||||
return ComponentStatus(
|
||||
name="Аккаунт",
|
||||
state="🔴",
|
||||
details=private_auth_health.message or "Ошибка private API.",
|
||||
)
|
||||
|
||||
|
||||
def _build_database_status() -> tuple[ComponentStatus, str]:
|
||||
db_ok, db_message = check_database_health()
|
||||
db_label = _extract_postgres_version(db_message)
|
||||
|
||||
if db_ok:
|
||||
return ComponentStatus(name="База данных", state="🟢"), db_label
|
||||
|
||||
return (
|
||||
ComponentStatus(
|
||||
name="База данных",
|
||||
state="🔴",
|
||||
details=db_message or "Ошибка подключения к БД.",
|
||||
),
|
||||
db_label,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_mode_label(exchange_testnet: bool) -> str:
|
||||
return "ДЕМО аккаунт" if exchange_testnet else "РЕАЛЬНЫЙ аккаунт"
|
||||
|
||||
|
||||
def get_system_snapshot() -> SystemSnapshot:
|
||||
settings = load_settings()
|
||||
exchange_service = ExchangeService()
|
||||
|
||||
try:
|
||||
symbol_validation = exchange_service.validate_symbol(settings.default_symbol)
|
||||
except Exception as exc:
|
||||
symbol_validation = None
|
||||
symbol_validation_message = f"Не удалось проверить символ: {exc}"
|
||||
else:
|
||||
symbol_validation_message = symbol_validation.message
|
||||
|
||||
exchange_health = exchange_service.get_health()
|
||||
private_auth_health = exchange_service.get_private_auth_health()
|
||||
|
||||
if exchange_health.ok and exchange_health.mode == "mock":
|
||||
exchange_state = "🟡 mock mode"
|
||||
elif exchange_health.ok:
|
||||
exchange_state = "🟢 API OK"
|
||||
else:
|
||||
exchange_state = "🔴 ошибка"
|
||||
|
||||
symbol_state = "🟢 OK" if symbol_validation and symbol_validation.is_valid else "🔴 ошибка"
|
||||
private_auth_state = "🟢 OK" if private_auth_health.ok else "🔴 ошибка"
|
||||
database_status, db_label = _build_database_status()
|
||||
exchange_status = _build_exchange_status(exchange_service, settings.default_symbol)
|
||||
account_status = _build_account_status(exchange_service)
|
||||
|
||||
components = [
|
||||
ComponentStatus(
|
||||
name="Бот",
|
||||
state="🟢 работает",
|
||||
details="Процесс бота запущен и обрабатывает команды.",
|
||||
),
|
||||
ComponentStatus(
|
||||
name="Telegram",
|
||||
state="🟢 OK",
|
||||
details="Polling активен, базовая маршрутизация подключена.",
|
||||
),
|
||||
ComponentStatus(
|
||||
name="Биржа",
|
||||
state=exchange_state,
|
||||
details=exchange_health.message,
|
||||
),
|
||||
ComponentStatus(
|
||||
name="Символ",
|
||||
state=symbol_state,
|
||||
details=symbol_validation_message,
|
||||
),
|
||||
ComponentStatus(
|
||||
name="Авторизация",
|
||||
state=private_auth_state,
|
||||
details=private_auth_health.message,
|
||||
),
|
||||
ComponentStatus(
|
||||
name="База данных",
|
||||
state="🟡 не подключена",
|
||||
details="Слой хранения пока только подготовлен структурно.",
|
||||
),
|
||||
ComponentStatus(name="Приложение", state="🟢"),
|
||||
database_status,
|
||||
ComponentStatus(name="Telegram", state="🟢"),
|
||||
exchange_status,
|
||||
account_status,
|
||||
]
|
||||
|
||||
return SystemSnapshot(
|
||||
app_name=APP_NAME,
|
||||
app_version=APP_VERSION,
|
||||
app_env=settings.app_env,
|
||||
python_version=platform.python_version(),
|
||||
os_name=f"{platform.system()} {platform.release()}",
|
||||
db_label=db_label,
|
||||
timezone_name=settings.tz,
|
||||
exchange_enabled=settings.exchange_enabled,
|
||||
exchange_name=settings.exchange_name,
|
||||
mode_label=_resolve_mode_label(settings.exchange_testnet),
|
||||
default_symbol=settings.default_symbol,
|
||||
symbol_validation_message=symbol_validation_message,
|
||||
private_auth_message=private_auth_health.message,
|
||||
components=components,
|
||||
)
|
||||
|
||||
|
||||
def _render_component(component: ComponentStatus) -> str:
|
||||
if component.state == "🟢":
|
||||
return f"{component.state} <b>{component.name}</b>"
|
||||
|
||||
return f"{component.state} <b>{component.name}</b>\n— {component.details}"
|
||||
|
||||
|
||||
def build_system_text() -> str:
|
||||
snapshot = get_system_snapshot()
|
||||
|
||||
component_lines = []
|
||||
for component in snapshot.components:
|
||||
component_lines.append(
|
||||
f"{component.state} <b>{component.name}</b>\n"
|
||||
f"— {component.details}"
|
||||
)
|
||||
|
||||
components_block = "\n\n".join(component_lines)
|
||||
components_block = "\n".join(_render_component(component) for component in snapshot.components)
|
||||
|
||||
return (
|
||||
"<b>⚙️ Система</b>\n\n"
|
||||
"<b>Статус компонентов</b>\n"
|
||||
f"{components_block}\n\n"
|
||||
"<b>Окружение</b>\n"
|
||||
f"- приложение: {snapshot.app_name} {snapshot.app_version}\n"
|
||||
f"- env: {snapshot.app_env}\n"
|
||||
f"- python: {snapshot.python_version}\n"
|
||||
f"- os: {snapshot.os_name}\n"
|
||||
f"- timezone: {snapshot.timezone_name}\n"
|
||||
f"- exchange_enabled: {snapshot.exchange_enabled}\n"
|
||||
f"- exchange_name: {snapshot.exchange_name}\n"
|
||||
f"- default_symbol: {snapshot.default_symbol}\n\n"
|
||||
"<b>Справка</b>\n"
|
||||
"/start — стартовый экран\n"
|
||||
"/menu — показать меню\n"
|
||||
"/help — открыть системную справку"
|
||||
)
|
||||
"<b>🌐 Окружение</b>\n"
|
||||
f"• приложение: {snapshot.app_name} {snapshot.app_version}\n"
|
||||
f"• база данных: {snapshot.db_label}\n"
|
||||
f"• часовой пояс: {snapshot.timezone_name}\n"
|
||||
f"• режим: {snapshot.mode_label}\n"
|
||||
f"• инструмент: {snapshot.default_symbol}"
|
||||
)
|
||||
@@ -242,4 +242,4 @@ class ExchangeService:
|
||||
price=float(price_raw),
|
||||
source="dzengi-demo-api",
|
||||
updated_at=updated_at,
|
||||
)
|
||||
)
|
||||
33
app/src/storage/models.py
Normal file
33
app/src/storage/models.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BalanceSnapshotRecord:
|
||||
id: int | None
|
||||
created_at: str
|
||||
source: str
|
||||
payload_json: str
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class JournalEventRecord:
|
||||
id: int | None
|
||||
created_at: str
|
||||
level: str
|
||||
event_type: str
|
||||
message: str
|
||||
payload_json: str | None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class OrderDraftRecord:
|
||||
id: int | None
|
||||
created_at: str
|
||||
symbol: str
|
||||
side: str
|
||||
order_type: str
|
||||
quantity: str
|
||||
status: str
|
||||
payload_json: str | None
|
||||
44
app/src/storage/schema.py
Normal file
44
app/src/storage/schema.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from src.storage.session import get_connection
|
||||
|
||||
|
||||
DDL = [
|
||||
'''
|
||||
CREATE TABLE IF NOT EXISTS balance_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
source TEXT NOT NULL,
|
||||
payload_json JSONB NOT NULL
|
||||
)
|
||||
''',
|
||||
'''
|
||||
CREATE TABLE IF NOT EXISTS journal_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
level TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
payload_json JSONB
|
||||
)
|
||||
''',
|
||||
'''
|
||||
CREATE TABLE IF NOT EXISTS order_drafts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
symbol TEXT NOT NULL,
|
||||
side TEXT NOT NULL,
|
||||
order_type TEXT NOT NULL,
|
||||
quantity NUMERIC(36, 18) NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
payload_json JSONB
|
||||
)
|
||||
'''
|
||||
]
|
||||
|
||||
|
||||
def init_schema() -> None:
|
||||
with get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
for statement in DDL:
|
||||
cursor.execute(statement)
|
||||
42
app/src/storage/session.py
Normal file
42
app/src/storage/session.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import psycopg
|
||||
|
||||
from src.core.config import load_settings
|
||||
|
||||
|
||||
def build_dsn() -> str:
|
||||
settings = load_settings()
|
||||
password_part = settings.db_password.replace("@", "%40")
|
||||
return (
|
||||
f"postgresql://{settings.db_user}:{password_part}"
|
||||
f"@{settings.db_host}:{settings.db_port}/{settings.db_name}"
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_connection():
|
||||
connection = psycopg.connect(build_dsn(), autocommit=False)
|
||||
try:
|
||||
yield connection
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def check_database_health() -> tuple[bool, str]:
|
||||
try:
|
||||
with get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT version()")
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
return False, "PostgreSQL ping returned no rows."
|
||||
|
||||
version = str(row[0]).strip()
|
||||
except Exception as exc:
|
||||
return False, f"PostgreSQL error: {exc}"
|
||||
|
||||
return True, version
|
||||
15
docs/decisions/0009-postgres-storage-foundation.md
Normal file
15
docs/decisions/0009-postgres-storage-foundation.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 0009 — PostgreSQL for Storage Foundation
|
||||
|
||||
## Решение
|
||||
Сразу строить storage foundation на PostgreSQL, а не на SQLite.
|
||||
|
||||
## Причины
|
||||
- проект ориентирован на рост
|
||||
- нужны нормальные concurrent writes и server DB
|
||||
- удобно хранить JSONB payloads
|
||||
- это снижает риск будущей миграции БД
|
||||
|
||||
## Последствия
|
||||
- dev setup становится чуть сложнее
|
||||
- зато storage строится сразу на целевой БД
|
||||
- journal, snapshots и order flow будут развиваться без смены движка
|
||||
88
docs/roadmap/master-roadmap.md
Normal file
88
docs/roadmap/master-roadmap.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Dzentra Bot — Master Roadmap
|
||||
|
||||
## Stage 01 — Bootstrap
|
||||
✔ структура проекта
|
||||
✔ virtualenv
|
||||
✔ запуск приложения
|
||||
✔ базовый bootstrap
|
||||
|
||||
---
|
||||
|
||||
## Stage 02 — System layer
|
||||
✔ экран ⚙️ Система
|
||||
✔ базовая диагностика
|
||||
✔ структура компонентов
|
||||
|
||||
---
|
||||
|
||||
## Stage 03 — Exchange Integration
|
||||
|
||||
### 03.1
|
||||
✔ mock exchange
|
||||
|
||||
### 03.2
|
||||
✔ время / timestamp
|
||||
|
||||
### 03.3
|
||||
✔ exchangeInfo
|
||||
✔ валидация символа
|
||||
|
||||
### 03.4
|
||||
✔ private auth (HMAC, headers, timestamp)
|
||||
|
||||
### 03.5
|
||||
✔ первый private endpoint (account)
|
||||
✔ баланс
|
||||
✔ экран 💼 Портфель
|
||||
✔ UX улучшения
|
||||
|
||||
---
|
||||
|
||||
## Stage 04 — Storage / Journal (NEXT)
|
||||
|
||||
### 04.1
|
||||
⏳ storage foundation
|
||||
|
||||
### 04.2
|
||||
⏳ journal / event log
|
||||
|
||||
### 04.3
|
||||
⏳ repositories
|
||||
|
||||
### 04.4
|
||||
⏳ UI integration
|
||||
|
||||
---
|
||||
|
||||
## Stage 03.6 — Orders (после storage)
|
||||
⏳ orders skeleton
|
||||
⏳ dry-run режим
|
||||
⏳ валидация ордеров
|
||||
|
||||
---
|
||||
|
||||
## Stage 05 — Trading logic
|
||||
⏳ стратегии
|
||||
⏳ риск-менеджмент
|
||||
⏳ сигналы
|
||||
|
||||
---
|
||||
|
||||
## Stage 06 — Automation
|
||||
⏳ авто-режим
|
||||
⏳ планировщик
|
||||
⏳ фоновые задачи
|
||||
|
||||
---
|
||||
|
||||
## Stage 07 — Observability
|
||||
⏳ логирование
|
||||
⏳ алерты
|
||||
⏳ метрики
|
||||
|
||||
---
|
||||
|
||||
## Текущий статус
|
||||
|
||||
👉 Stage 03.5 — завершён
|
||||
👉 Следующий шаг: Stage 04.1
|
||||
75
docs/roadmap/stage-03-roadmap.md
Normal file
75
docs/roadmap/stage-03-roadmap.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Stage 03 — Exchange Integration Roadmap
|
||||
|
||||
## Цель
|
||||
Интеграция с биржей:
|
||||
- public API
|
||||
- private API
|
||||
- базовый UI
|
||||
|
||||
---
|
||||
|
||||
## 03.1 — Mock exchange
|
||||
✔ имитация данных
|
||||
✔ структура клиента
|
||||
|
||||
---
|
||||
|
||||
## 03.2 — Time handling
|
||||
✔ обработка timestamp
|
||||
✔ локализация времени
|
||||
|
||||
---
|
||||
|
||||
## 03.3 — exchangeInfo
|
||||
✔ загрузка символов
|
||||
✔ валидация символа
|
||||
✔ защита от ошибок API
|
||||
|
||||
---
|
||||
|
||||
## 03.4 — Private auth
|
||||
✔ API key
|
||||
✔ secret
|
||||
✔ HMAC подпись
|
||||
✔ headers
|
||||
✔ timestamp
|
||||
|
||||
---
|
||||
|
||||
## 03.5 — Account + Portfolio
|
||||
✔ private endpoint `/account`
|
||||
✔ парсер баланса
|
||||
✔ сервисный слой
|
||||
✔ экран 💼 Портфель
|
||||
|
||||
### UX улучшения
|
||||
✔ иконки валют
|
||||
✔ сортировка
|
||||
✔ скрытие нулевых балансов
|
||||
✔ группировка активов
|
||||
|
||||
---
|
||||
|
||||
## Результат Stage 03
|
||||
|
||||
✔ бот умеет:
|
||||
- получать цену
|
||||
- валидировать символ
|
||||
- работать с private API
|
||||
- получать баланс
|
||||
- отображать портфель
|
||||
|
||||
---
|
||||
|
||||
## Ограничения
|
||||
|
||||
- нет ордеров
|
||||
- нет хранения
|
||||
- нет журнала
|
||||
- нет стратегии
|
||||
|
||||
---
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
➡ Stage 04 — Storage / Journal
|
||||
72
docs/roadmap/stage-04-roadmap.md
Normal file
72
docs/roadmap/stage-04-roadmap.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Stage 04 — Storage / Journal Roadmap
|
||||
|
||||
## Цель
|
||||
Добавить слой хранения и журналирования.
|
||||
|
||||
---
|
||||
|
||||
## 04.1 — Storage foundation
|
||||
|
||||
Цель:
|
||||
- подключить БД
|
||||
- создать базовые модели
|
||||
|
||||
Что добавить:
|
||||
- storage/session.py
|
||||
- storage/schema.py
|
||||
- SQLite
|
||||
|
||||
Сущности:
|
||||
- BalanceSnapshot
|
||||
- JournalEvent
|
||||
- OrderDraft
|
||||
|
||||
---
|
||||
|
||||
## 04.2 — Journal / event log
|
||||
|
||||
Цель:
|
||||
- логировать действия бота
|
||||
|
||||
Что логировать:
|
||||
- ошибки API
|
||||
- запросы
|
||||
- события системы
|
||||
|
||||
---
|
||||
|
||||
## 04.3 — Repositories
|
||||
|
||||
Цель:
|
||||
- хранить данные
|
||||
|
||||
Добавить:
|
||||
- repository balance
|
||||
- repository orders
|
||||
- repository journal
|
||||
|
||||
---
|
||||
|
||||
## 04.4 — UI integration
|
||||
|
||||
Цель:
|
||||
- показать storage в интерфейсе
|
||||
|
||||
Добавить:
|
||||
- статус БД в ⚙️ Система
|
||||
- последние события
|
||||
- snapshot баланса
|
||||
|
||||
---
|
||||
|
||||
## Результат Stage 04
|
||||
|
||||
✔ данные сохраняются
|
||||
✔ есть история
|
||||
✔ есть журнал
|
||||
|
||||
---
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
➡ Stage 03.6 — Orders skeleton
|
||||
142
docs/stages/stage-04-1-storage.md
Normal file
142
docs/stages/stage-04-1-storage.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Stage 04.1 — Storage Foundation (PostgreSQL) and System Dashboard
|
||||
|
||||
## Цель
|
||||
Подключить слой хранения на PostgreSQL и одновременно привести системный экран к продовому виду (операционный dashboard).
|
||||
|
||||
---
|
||||
|
||||
## Что добавлено
|
||||
|
||||
### Storage
|
||||
- PostgreSQL как основная база данных
|
||||
- подключение через `psycopg`
|
||||
- инициализация схемы при старте приложения
|
||||
- базовые таблицы:
|
||||
- balance_snapshots
|
||||
- journal_events
|
||||
- order_drafts
|
||||
- health check БД
|
||||
|
||||
---
|
||||
|
||||
### System Dashboard (⚙️ Система)
|
||||
Экран системы переработан из технического вывода в операционный статус.
|
||||
|
||||
#### Компоненты системы
|
||||
Добавлен компактный статус всех ключевых частей:
|
||||
|
||||
- Приложение
|
||||
- База данных
|
||||
- Telegram
|
||||
- Биржа (public API + символ)
|
||||
- Аккаунт (private API)
|
||||
|
||||
Поведение:
|
||||
- при 🟢 → только краткий статус
|
||||
- при 🔴 → добавляется описание ошибки
|
||||
|
||||
---
|
||||
|
||||
### Биржа (объединённый статус)
|
||||
Объединены проверки:
|
||||
- доступность public API
|
||||
- валидность символа (exchangeInfo)
|
||||
|
||||
Теперь это один сигнал:
|
||||
- 🟢 Биржа → всё работает
|
||||
- 🔴 Биржа → проблема (API или символ)
|
||||
|
||||
---
|
||||
|
||||
### Аккаунт
|
||||
Добавлен статус private API:
|
||||
|
||||
- проверка API ключей
|
||||
- проверка доступа к аккаунту
|
||||
|
||||
---
|
||||
|
||||
## Блок "🌐 Окружение"
|
||||
|
||||
Экран упрощён до минимально полезного набора параметров:
|
||||
|
||||
🌐 Окружение
|
||||
• приложение: Dzentra Bot 2.0.0
|
||||
• база данных: PostgreSQL 17.9
|
||||
• часовой пояс: Europe/Minsk
|
||||
• режим: ДЕМО аккаунт / РЕАЛЬНЫЙ аккаунт
|
||||
• инструмент: BTC/USD_LEVERAGE
|
||||
|
||||
### Принципы отбора
|
||||
Оставлены только параметры, которые:
|
||||
- влияют на поведение системы
|
||||
- важны для диагностики
|
||||
- критичны для безопасности (режим торговли)
|
||||
|
||||
Удалены:
|
||||
- env
|
||||
- exchange_enabled
|
||||
- exchange_name
|
||||
- os
|
||||
- python
|
||||
|
||||
---
|
||||
|
||||
## Режим работы (важное изменение)
|
||||
|
||||
Вместо технического параметра:
|
||||
|
||||
env: dev
|
||||
|
||||
используется бизнес-понятие:
|
||||
|
||||
режим: ДЕМО аккаунт / РЕАЛЬНЫЙ аккаунт
|
||||
|
||||
Источник:
|
||||
- exchange_testnet
|
||||
|
||||
Зачем:
|
||||
- снижает риск ошибок
|
||||
- сразу понятно, используются ли реальные деньги
|
||||
|
||||
---
|
||||
|
||||
## Отображение PostgreSQL
|
||||
|
||||
Реализовано корректное отображение версии БД:
|
||||
|
||||
PostgreSQL 17.9
|
||||
|
||||
Особенности:
|
||||
- версия берётся напрямую из БД (SELECT version())
|
||||
- из строки извлекается только нужная часть
|
||||
- исключены технические строки типа OK: db=...
|
||||
|
||||
---
|
||||
|
||||
## Архитектурный результат
|
||||
|
||||
После Stage 04.1 система получила:
|
||||
|
||||
- полноценный storage layer (PostgreSQL)
|
||||
- health-check инфраструктуры
|
||||
- единый системный dashboard
|
||||
- разделение:
|
||||
- технических параметров
|
||||
- пользовательского UI
|
||||
|
||||
---
|
||||
|
||||
## Почему это важно
|
||||
|
||||
- система готова к журналированию
|
||||
- можно диагностировать проблемы без логов
|
||||
- UI стал безопасным (режим торговли)
|
||||
- база данных интегрирована как часть системы, а не внешний компонент
|
||||
|
||||
---
|
||||
|
||||
## Что дальше
|
||||
|
||||
- Stage 04.2 — Journal / event log
|
||||
- Stage 04.3 — Repositories
|
||||
@@ -1,4 +1,22 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
container_name: dzentra_postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: dzentra_bot
|
||||
POSTGRES_USER: dzentra_bot
|
||||
POSTGRES_PASSWORD: change_me
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- dzentra_postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U dzentra_bot -d dzentra_bot"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
bot:
|
||||
build:
|
||||
context: ../..
|
||||
@@ -7,3 +25,9 @@ services:
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ../../app/.env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
dzentra_postgres_data:
|
||||
Reference in New Issue
Block a user