- POST /api/reset endpoint: clears all tables and restarts log parser - UI: Reset DB button in footer with confirmation dialog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""
|
|
dashboard_api.py — 로그 파서가 채운 SQLite DB를 읽어서 대시보드 API 제공
|
|
"""
|
|
|
|
import sqlite3
|
|
import os
|
|
import signal
|
|
from fastapi import FastAPI, Query
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pathlib import Path
|
|
from contextlib import contextmanager
|
|
|
|
DB_PATH = os.environ.get("DB_PATH", "/app/data/dashboard.db")
|
|
|
|
app = FastAPI(title="Trading Dashboard API")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
@contextmanager
|
|
def get_db():
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
try:
|
|
yield conn
|
|
finally:
|
|
conn.close()
|
|
|
|
@app.get("/api/position")
|
|
def get_position():
|
|
with get_db() as db:
|
|
row = db.execute(
|
|
"SELECT * FROM trades WHERE status='OPEN' ORDER BY id DESC LIMIT 1"
|
|
).fetchone()
|
|
status_rows = db.execute("SELECT key, value FROM bot_status").fetchall()
|
|
bot = {r["key"]: r["value"] for r in status_rows}
|
|
return {"position": dict(row) if row else None, "bot": bot}
|
|
|
|
@app.get("/api/trades")
|
|
def get_trades(limit: int = Query(50, ge=1, le=500), offset: int = 0):
|
|
with get_db() as db:
|
|
rows = db.execute(
|
|
"SELECT * FROM trades WHERE status='CLOSED' ORDER BY id DESC LIMIT ? OFFSET ?",
|
|
(limit, offset),
|
|
).fetchall()
|
|
total = db.execute("SELECT COUNT(*) as cnt FROM trades WHERE status='CLOSED'").fetchone()["cnt"]
|
|
return {"trades": [dict(r) for r in rows], "total": total}
|
|
|
|
@app.get("/api/daily")
|
|
def get_daily(days: int = Query(30, ge=1, le=365)):
|
|
with get_db() as db:
|
|
rows = db.execute("""
|
|
SELECT
|
|
date(exit_time) as date,
|
|
COUNT(*) as total_trades,
|
|
SUM(CASE WHEN net_pnl > 0 THEN 1 ELSE 0 END) as wins,
|
|
SUM(CASE WHEN net_pnl <= 0 THEN 1 ELSE 0 END) as losses,
|
|
ROUND(SUM(net_pnl), 4) as net_pnl,
|
|
ROUND(SUM(commission), 4) as total_fees
|
|
FROM trades
|
|
WHERE status='CLOSED' AND exit_time IS NOT NULL
|
|
GROUP BY date(exit_time)
|
|
ORDER BY date DESC
|
|
LIMIT ?
|
|
""", (days,)).fetchall()
|
|
return {"daily": [dict(r) for r in rows]}
|
|
|
|
@app.get("/api/stats")
|
|
def get_stats():
|
|
with get_db() as db:
|
|
row = db.execute("""
|
|
SELECT
|
|
COUNT(*) as total_trades,
|
|
COALESCE(SUM(CASE WHEN net_pnl > 0 THEN 1 ELSE 0 END), 0) as wins,
|
|
COALESCE(SUM(CASE WHEN net_pnl <= 0 THEN 1 ELSE 0 END), 0) as losses,
|
|
COALESCE(SUM(net_pnl), 0) as total_pnl,
|
|
COALESCE(SUM(commission), 0) as total_fees,
|
|
COALESCE(AVG(net_pnl), 0) as avg_pnl,
|
|
COALESCE(MAX(net_pnl), 0) as best_trade,
|
|
COALESCE(MIN(net_pnl), 0) as worst_trade
|
|
FROM trades WHERE status='CLOSED'
|
|
""").fetchone()
|
|
status_rows = db.execute("SELECT key, value FROM bot_status").fetchall()
|
|
bot = {r["key"]: r["value"] for r in status_rows}
|
|
result = dict(row)
|
|
result["current_price"] = bot.get("current_price")
|
|
result["balance"] = bot.get("balance")
|
|
return result
|
|
|
|
@app.get("/api/candles")
|
|
def get_candles(limit: int = Query(96, ge=1, le=1000)):
|
|
with get_db() as db:
|
|
rows = db.execute("SELECT * FROM candles ORDER BY ts DESC LIMIT ?", (limit,)).fetchall()
|
|
return {"candles": [dict(r) for r in reversed(rows)]}
|
|
|
|
@app.get("/api/health")
|
|
def health():
|
|
try:
|
|
with get_db() as db:
|
|
cnt = db.execute("SELECT COUNT(*) as c FROM candles").fetchone()["c"]
|
|
return {"status": "ok", "candles_count": cnt}
|
|
except Exception as e:
|
|
return {"status": "error", "detail": str(e)}
|
|
|
|
|
|
@app.post("/api/reset")
|
|
def reset_db():
|
|
"""DB 전체 초기화 후 파서 재시작 (로그를 처음부터 다시 파싱)"""
|
|
with get_db() as db:
|
|
for table in ["trades", "daily_pnl", "parse_state", "bot_status", "candles"]:
|
|
db.execute(f"DELETE FROM {table}")
|
|
db.commit()
|
|
|
|
# 파서 프로세스 재시작 (entrypoint.sh의 백그라운드 프로세스)
|
|
import subprocess
|
|
# 기존 파서 종료
|
|
subprocess.run(["pkill", "-f", "log_parser.py"], capture_output=True)
|
|
# 새 파서 시작
|
|
subprocess.Popen(["python", "log_parser.py"])
|
|
|
|
return {"status": "ok", "message": "DB 초기화 완료, 파서 재시작됨"}
|
|
|
|
|