07.4.4.1.13 — AutoTrade Runtime Journal, Execution Refactor & Trade Analytics
This commit is contained in:
@@ -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
|
||||
@@ -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]),
|
||||
}
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# app/src/storage/session.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
Reference in New Issue
Block a user