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(),
|
||||||
)
|
)
|
||||||
@@ -6,132 +6,150 @@ 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
|
python_version: str
|
||||||
os_name: str
|
os_name: str
|
||||||
|
app_env: str
|
||||||
timezone_name: str
|
timezone_name: str
|
||||||
exchange_enabled: bool
|
exchange_enabled: bool
|
||||||
exchange_name: 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 or "PostgreSQL " not in raw:
|
||||||
|
return "Postgres"
|
||||||
|
|
||||||
|
tail = raw.split("PostgreSQL ", 1)[1]
|
||||||
|
version = tail.split(" ", 1)[0].strip()
|
||||||
|
return f"Postgres {version}" if version else "Postgres"
|
||||||
|
|
||||||
|
|
||||||
|
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 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(),
|
python_version=platform.python_version(),
|
||||||
os_name=f"{platform.system()} {platform.release()}",
|
os_name=f"{platform.system()} {platform.release()}",
|
||||||
|
app_env=settings.app_env,
|
||||||
timezone_name=settings.tz,
|
timezone_name=settings.tz,
|
||||||
exchange_enabled=settings.exchange_enabled,
|
exchange_enabled=settings.exchange_enabled,
|
||||||
exchange_name=settings.exchange_name,
|
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"• python: {snapshot.python_version}\n"
|
||||||
f"- os: {snapshot.os_name}\n"
|
f"• os: {snapshot.os_name}\n"
|
||||||
f"- timezone: {snapshot.timezone_name}\n"
|
f"• env: {snapshot.app_env}\n"
|
||||||
f"- exchange_enabled: {snapshot.exchange_enabled}\n"
|
f"• timezone: {snapshot.timezone_name}\n"
|
||||||
f"- exchange_name: {snapshot.exchange_name}\n"
|
f"• exchange_enabled: {snapshot.exchange_enabled}\n"
|
||||||
f"- default_symbol: {snapshot.default_symbol}\n\n"
|
f"• exchange_name: {snapshot.exchange_name}\n"
|
||||||
"<b>Справка</b>\n"
|
f"• default_symbol: {snapshot.default_symbol}"
|
||||||
"/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 current_database(), current_user, version()")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row is None:
|
||||||
|
return False, "PostgreSQL ping returned no rows."
|
||||||
|
db_name, db_user, version = row
|
||||||
|
except Exception as exc:
|
||||||
|
return False, f"PostgreSQL error: {exc}"
|
||||||
|
|
||||||
|
version_short = str(version).split(",")[0]
|
||||||
|
return True, f"PostgreSQL OK: db={db_name}, user={db_user}, {version_short}"
|
||||||
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
|
||||||
22
docs/stages/stage-04-1-storage-foundation-postgres.md
Normal file
22
docs/stages/stage-04-1-storage-foundation-postgres.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Stage 04.1 — Storage Foundation (PostgreSQL)
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
Подключить первый слой хранения сразу на PostgreSQL и заложить базу для журналирования и order flow.
|
||||||
|
|
||||||
|
## Что добавлено
|
||||||
|
- PostgreSQL как основная БД
|
||||||
|
- инициализация схемы при старте приложения
|
||||||
|
- таблицы:
|
||||||
|
- balance_snapshots
|
||||||
|
- journal_events
|
||||||
|
- order_drafts
|
||||||
|
- health check БД в `⚙️ Система`
|
||||||
|
|
||||||
|
## Почему PostgreSQL
|
||||||
|
- подходит как целевая БД на рост
|
||||||
|
- удобно для JSONB, событий и order flow
|
||||||
|
- не требует будущей смены движка при росте проекта
|
||||||
|
|
||||||
|
## Что дальше
|
||||||
|
- Stage 04.2 — Journal / event log
|
||||||
|
- Stage 04.3 — repositories
|
||||||
25
docs/stages/stage-04-1-ui-polish.md
Normal file
25
docs/stages/stage-04-1-ui-polish.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Stage 04.1+ — System Screen UI Polish
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
Сделать экран `⚙️ Система` короче, чище и понятнее без изменения бизнес-логики.
|
||||||
|
|
||||||
|
## Что изменено
|
||||||
|
- убраны формулировки вида `OK База данных`
|
||||||
|
- заголовки компонентов теперь выглядят единообразно
|
||||||
|
- текст PostgreSQL health сокращён до компактного вида
|
||||||
|
- блок окружения переведён на более аккуратный формат
|
||||||
|
|
||||||
|
## Что НЕ менялось
|
||||||
|
- логика health checks
|
||||||
|
- подключение PostgreSQL
|
||||||
|
- проверка public/private API
|
||||||
|
- symbol validation
|
||||||
|
|
||||||
|
## Результат
|
||||||
|
Экран `⚙️ Система` стал:
|
||||||
|
- компактнее
|
||||||
|
- понятнее
|
||||||
|
- ближе к продовому UI
|
||||||
|
|
||||||
|
## Рекомендуемый commit
|
||||||
|
`Stage 04.1+ - polish system screen UI`
|
||||||
@@ -1,4 +1,16 @@
|
|||||||
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
|
||||||
bot:
|
bot:
|
||||||
build:
|
build:
|
||||||
context: ../..
|
context: ../..
|
||||||
@@ -7,3 +19,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- ../../app/.env
|
- ../../app/.env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
volumes:
|
||||||
|
dzentra_postgres_data:
|
||||||
Reference in New Issue
Block a user