07.4.4.1.13 — AutoTrade Runtime Journal, Execution Refactor & Trade Analytics

This commit is contained in:
2026-05-28 10:30:54 +03:00
parent f9a25e7671
commit d9e6392e28
75 changed files with 9934 additions and 10508 deletions

View File

@@ -1,3 +1,5 @@
# app/src/storage/models.py
from __future__ import annotations
from dataclasses import dataclass
@@ -18,16 +20,4 @@ class JournalEventRecord:
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
payload_json: str | None

View File

@@ -17,7 +17,11 @@ class JournalRepository:
message: str,
payload: dict[str, Any] | None = None,
) -> None:
payload_json = json.dumps(payload, ensure_ascii=False) if payload is not None else None
payload_json = (
json.dumps(payload, ensure_ascii=False)
if payload is not None
else None
)
with get_connection() as connection:
with connection.cursor() as cursor:
@@ -66,7 +70,11 @@ class JournalRepository:
return [self._row_to_dict(row) for row in rows]
def list_recent_with_offset(self, limit: int, offset: int) -> list[dict[str, Any]]:
def list_recent_with_offset(
self,
limit: int,
offset: int,
) -> list[dict[str, Any]]:
with get_connection() as connection:
with connection.cursor() as cursor:
cursor.execute(
@@ -82,13 +90,86 @@ class JournalRepository:
return [self._row_to_dict(row) for row in rows]
def list_export_rows(self, limit: int = 5000) -> list[dict[str, Any]]:
def list_export_rows(
self,
limit: int = 5000,
export_filter: str = "all",
) -> list[dict[str, Any]]:
where_sql = ""
if export_filter == "auto":
where_sql = """
WHERE
COALESCE(payload_json ->> 'screen', '') = 'auto'
OR event_type LIKE 'position_%%'
OR event_type LIKE 'trade_%%'
OR event_type LIKE 'runtime_%%'
OR event_type IN (
'signal_summary',
'signal_ready',
'signal_changed',
'signal_blocked',
'execution_blocked',
'execution_quality_changed'
)
"""
elif export_filter == "trades":
where_sql = """
WHERE
event_type IN (
'position_opened',
'position_closed',
'position_flipped',
'position_flip_blocked',
'trade_opened',
'trade_closed',
'trade_flipped'
)
OR event_type LIKE 'trade_%%'
"""
elif export_filter == "errors":
where_sql = """
WHERE
level IN ('ERROR', 'CRITICAL')
OR (
level = 'WARNING'
AND (
event_type LIKE '%%error%%'
OR event_type LIKE '%%blocked%%'
OR event_type LIKE '%%failed%%'
OR COALESCE(payload_json ->> 'error_type', '') <> ''
OR COALESCE(payload_json ->> 'raw_error', '') <> ''
)
)
"""
elif export_filter == "not_auto":
where_sql = """
WHERE NOT (
COALESCE(payload_json ->> 'screen', '') = 'auto'
OR event_type LIKE 'position_%%'
OR event_type LIKE 'trade_%%'
OR event_type LIKE 'runtime_%%'
OR event_type IN (
'signal_summary',
'signal_ready',
'signal_changed',
'signal_blocked',
'execution_blocked',
'execution_quality_changed'
)
)
"""
with get_connection() as connection:
with connection.cursor() as cursor:
cursor.execute(
"""
f"""
SELECT id, created_at, level, event_type, message, payload_json
FROM journal_events
{where_sql}
ORDER BY created_at DESC, id DESC
LIMIT %s
""",
@@ -114,17 +195,6 @@ class JournalRepository:
return int(deleted_count or 0)
def _row_to_dict(self, row: tuple[Any, ...]) -> dict[str, Any]:
return {
"id": str(row[0]),
"created_at": str(row[1]),
"level": str(row[2]),
"event_type": str(row[3]),
"message": str(row[4]),
"payload": self._parse_payload(row[5]),
}
def delete_older_than_days(self, days: int) -> int:
with get_connection() as connection:
with connection.cursor() as cursor:
@@ -137,4 +207,14 @@ class JournalRepository:
)
deleted_count = cursor.rowcount
return deleted_count
return int(deleted_count or 0)
def _row_to_dict(self, row: tuple[Any, ...]) -> dict[str, Any]:
return {
"id": str(row[0]),
"created_at": str(row[1]),
"level": str(row[2]),
"event_type": str(row[3]),
"message": str(row[4]),
"payload": self._parse_payload(row[5]),
}

View File

@@ -1,111 +0,0 @@
# app/src/storage/repositories/order_drafts.py
from __future__ import annotations
import json
from typing import Any
from src.storage.session import get_connection
class OrderDraftRepository:
def add_draft(
self,
*,
symbol: str,
side: str,
order_type: str,
quantity: str,
status: str = "draft",
payload: dict[str, Any] | None = None,
) -> None:
payload_json = json.dumps(payload, ensure_ascii=False) if payload is not None else None
with get_connection() as connection:
with connection.cursor() as cursor:
cursor.execute(
'''
INSERT INTO order_drafts (symbol, side, order_type, quantity, status, payload_json)
VALUES (%s, %s, %s, %s, %s, %s::jsonb)
''',
(symbol, side, order_type, quantity, status, payload_json),
)
def list_recent_drafts(self, limit: int = 10) -> list[dict[str, str]]:
with get_connection() as connection:
with connection.cursor() as cursor:
cursor.execute(
'''
SELECT id, created_at, symbol, side, order_type, quantity::text, status
FROM order_drafts
ORDER BY created_at DESC, id DESC
LIMIT %s
''',
(limit,),
)
rows = cursor.fetchall()
items: list[dict[str, str]] = []
for row in rows:
items.append(
{
"id": str(row[0]),
"created_at": str(row[1]),
"symbol": str(row[2]),
"side": str(row[3]),
"order_type": str(row[4]),
"quantity": str(row[5]),
"status": str(row[6]),
}
)
return items
def get_draft_by_id(self, draft_id: str) -> dict[str, str] | None:
with get_connection() as connection:
with connection.cursor() as cursor:
cursor.execute(
'''
SELECT id, created_at, symbol, side, order_type, quantity::text, status, payload_json
FROM order_drafts
WHERE id = %s
LIMIT 1
''',
(draft_id,),
)
row = cursor.fetchone()
if not row:
return None
payload_raw = row[7]
payload: dict[str, Any] = {}
if isinstance(payload_raw, dict):
payload = payload_raw
elif payload_raw:
try:
payload = json.loads(str(payload_raw))
except Exception:
payload = {}
price = payload.get("price")
price_text = str(price) if price not in (None, "") else ""
return {
"id": str(row[0]),
"created_at": str(row[1]),
"symbol": str(row[2]),
"side": str(row[3]),
"order_type": str(row[4]),
"quantity": str(row[5]),
"status": str(row[6]),
"price": price_text,
}
def count_drafts(self) -> int:
with get_connection() as connection:
with connection.cursor() as cursor:
cursor.execute("SELECT COUNT(*) FROM order_drafts")
row = cursor.fetchone()
return int(row[0]) if row else 0

View File

@@ -1,15 +1,23 @@
# app/src/storage/schema.py
from __future__ import annotations
from psycopg import sql
from src.storage.session import get_connection
DDL = [
'''
# SQL-команды для первичной инициализации базы данных.
DDL: list[sql.SQL] = [
sql.SQL("""
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
)
''',
'''
"""),
sql.SQL("""
CREATE TABLE IF NOT EXISTS journal_events (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@@ -18,28 +26,19 @@ DDL = [
message TEXT NOT NULL,
payload_json JSONB
)
''',
'''
"""),
sql.SQL("""
CREATE INDEX IF NOT EXISTS idx_journal_events_created_at
ON journal_events (created_at DESC)
''',
'''
"""),
sql.SQL("""
CREATE INDEX IF NOT EXISTS idx_journal_events_event_type
ON journal_events (event_type)
''',
'''
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:

View File

@@ -1,3 +1,5 @@
# app/src/storage/session.py
from __future__ import annotations
from contextlib import contextmanager