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
|
LOG_LEVEL=INFO
|
||||||
TZ=Europe/Minsk
|
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_ENABLED=true
|
||||||
EXCHANGE_NAME=dzengi
|
EXCHANGE_NAME=dzengi
|
||||||
EXCHANGE_BASE_URL=https://demo-api-adapter.dzengi.com
|
EXCHANGE_BASE_URL=https://demo-api-adapter.dzengi.com
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
aiogram==3.13.1
|
aiogram==3.13.1
|
||||||
python-dotenv==1.0.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.bootstrap.logging import setup_logging
|
||||||
from src.core.config import load_settings
|
from src.core.config import load_settings
|
||||||
|
from src.storage.schema import init_schema
|
||||||
from src.telegram.routers import setup_routers
|
from src.telegram.routers import setup_routers
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> tuple[Bot, Dispatcher]:
|
def create_app() -> tuple[Bot, Dispatcher]:
|
||||||
settings = load_settings()
|
settings = load_settings()
|
||||||
setup_logging(settings.log_level)
|
setup_logging(settings.log_level)
|
||||||
|
init_schema()
|
||||||
|
|
||||||
bot = Bot(
|
bot = Bot(
|
||||||
token=settings.bot_token,
|
token=settings.bot_token,
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ class Settings:
|
|||||||
exchange_timeout_sec: int
|
exchange_timeout_sec: int
|
||||||
exchange_testnet: bool
|
exchange_testnet: bool
|
||||||
default_symbol: str
|
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:
|
def _parse_bool(raw_value: str, default: bool = False) -> bool:
|
||||||
value = (raw_value or "").strip().lower()
|
value = (raw_value or "").strip().lower()
|
||||||
if not value:
|
if not value:
|
||||||
@@ -49,4 +54,9 @@ def load_settings() -> Settings:
|
|||||||
exchange_timeout_sec=_parse_int(os.getenv("EXCHANGE_TIMEOUT_SEC", "10"), 10),
|
exchange_timeout_sec=_parse_int(os.getenv("EXCHANGE_TIMEOUT_SEC", "10"), 10),
|
||||||
exchange_testnet=_parse_bool(os.getenv("EXCHANGE_TESTNET", "false")),
|
exchange_testnet=_parse_bool(os.getenv("EXCHANGE_TESTNET", "false")),
|
||||||
default_symbol=os.getenv("DEFAULT_SYMBOL", "BTC/USD_LEVERAGE").strip() or "BTC/USD_LEVERAGE",
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
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.core.constants import APP_NAME, APP_VERSION
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.integrations.exchange.service import ExchangeService
|
||||||
|
from src.storage.session import check_database_health
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class ComponentStatus:
|
class ComponentStatus:
|
||||||
name: str
|
name: str
|
||||||
state: str
|
state: str
|
||||||
details: str
|
details: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class SystemSnapshot:
|
class SystemSnapshot:
|
||||||
app_name: str
|
app_name: str
|
||||||
app_version: str
|
app_version: str
|
||||||
app_env: str
|
db_label: str
|
||||||
python_version: str
|
|
||||||
os_name: str
|
|
||||||
timezone_name: str
|
timezone_name: str
|
||||||
exchange_enabled: bool
|
mode_label: str
|
||||||
exchange_name: str
|
|
||||||
default_symbol: str
|
default_symbol: str
|
||||||
symbol_validation_message: str
|
|
||||||
private_auth_message: str
|
|
||||||
components: list[ComponentStatus]
|
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:
|
def get_system_snapshot() -> SystemSnapshot:
|
||||||
settings = load_settings()
|
settings = load_settings()
|
||||||
exchange_service = ExchangeService()
|
exchange_service = ExchangeService()
|
||||||
|
|
||||||
try:
|
database_status, db_label = _build_database_status()
|
||||||
symbol_validation = exchange_service.validate_symbol(settings.default_symbol)
|
exchange_status = _build_exchange_status(exchange_service, settings.default_symbol)
|
||||||
except Exception as exc:
|
account_status = _build_account_status(exchange_service)
|
||||||
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 "🔴 ошибка"
|
|
||||||
|
|
||||||
components = [
|
components = [
|
||||||
ComponentStatus(
|
ComponentStatus(name="Приложение", state="🟢"),
|
||||||
name="Бот",
|
database_status,
|
||||||
state="🟢 работает",
|
ComponentStatus(name="Telegram", state="🟢"),
|
||||||
details="Процесс бота запущен и обрабатывает команды.",
|
exchange_status,
|
||||||
),
|
account_status,
|
||||||
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="Слой хранения пока только подготовлен структурно.",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return SystemSnapshot(
|
return SystemSnapshot(
|
||||||
app_name=APP_NAME,
|
app_name=APP_NAME,
|
||||||
app_version=APP_VERSION,
|
app_version=APP_VERSION,
|
||||||
app_env=settings.app_env,
|
db_label=db_label,
|
||||||
python_version=platform.python_version(),
|
|
||||||
os_name=f"{platform.system()} {platform.release()}",
|
|
||||||
timezone_name=settings.tz,
|
timezone_name=settings.tz,
|
||||||
exchange_enabled=settings.exchange_enabled,
|
mode_label=_resolve_mode_label(settings.exchange_testnet),
|
||||||
exchange_name=settings.exchange_name,
|
|
||||||
default_symbol=settings.default_symbol,
|
default_symbol=settings.default_symbol,
|
||||||
symbol_validation_message=symbol_validation_message,
|
|
||||||
private_auth_message=private_auth_health.message,
|
|
||||||
components=components,
|
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:
|
def build_system_text() -> str:
|
||||||
snapshot = get_system_snapshot()
|
snapshot = get_system_snapshot()
|
||||||
|
components_block = "\n".join(_render_component(component) for component in snapshot.components)
|
||||||
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)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
"<b>⚙️ Система</b>\n\n"
|
"<b>⚙️ Система</b>\n\n"
|
||||||
"<b>Статус компонентов</b>\n"
|
|
||||||
f"{components_block}\n\n"
|
f"{components_block}\n\n"
|
||||||
"<b>Окружение</b>\n"
|
"<b>🌐 Окружение</b>\n"
|
||||||
f"- приложение: {snapshot.app_name} {snapshot.app_version}\n"
|
f"• приложение: {snapshot.app_name} {snapshot.app_version}\n"
|
||||||
f"- env: {snapshot.app_env}\n"
|
f"• база данных: {snapshot.db_label}\n"
|
||||||
f"- python: {snapshot.python_version}\n"
|
f"• часовой пояс: {snapshot.timezone_name}\n"
|
||||||
f"- os: {snapshot.os_name}\n"
|
f"• режим: {snapshot.mode_label}\n"
|
||||||
f"- timezone: {snapshot.timezone_name}\n"
|
f"• инструмент: {snapshot.default_symbol}"
|
||||||
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 — открыть системную справку"
|
|
||||||
)
|
|
||||||
@@ -242,4 +242,4 @@ class ExchangeService:
|
|||||||
price=float(price_raw),
|
price=float(price_raw),
|
||||||
source="dzengi-demo-api",
|
source="dzengi-demo-api",
|
||||||
updated_at=updated_at,
|
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:
|
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:
|
bot:
|
||||||
build:
|
build:
|
||||||
context: ../..
|
context: ../..
|
||||||
@@ -7,3 +25,9 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- ../../app/.env
|
- ../../app/.env
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dzentra_postgres_data:
|
||||||
Reference in New Issue
Block a user