d16456cb92
# 사용자별 격리 - JWT 토큰에 uid 추가 (auth.get_uid 헬퍼) - PostgreSQL — exchange_credentials/automation_config/trades/signal_events 에 user_id BIGINT - SQLite user_settings 테이블 신설 (글로벌 settings 는 옛 호환) - 모든 DB 함수 시그니처에 user_id 인자 추가 — 다른 사용자 데이터 절대 접근 불가 - alert_state — 모든 dict key 가 (user_id, ...) tuple 로 계층화 - core_logic alert_loop — 활성 사용자 순회 + 각자 settings/symbol/텔레그램 적용 - ensure_user_defaults() / ensure_user_automation() — 첫 사용 시 자동 시드 # 사용자 관리 (admin only) - users_db: delete_user / admin_reset_password / set_role - /api/users POST DELETE PUT password PUT role (본인 강등 / 마지막 admin 보호) - /admin/users 페이지 — 등록/삭제/role 토글/비번 reset 모달 - 사이드바 adminOnly 필터 — admin role 만 메뉴 노출 # 대시보드 개선 - 모바일 / 범례 토글 (모바일 60 캔들, 데스크톱 200) - 트레이드 이력: open 트레이드 실시간 PnL% (Binance ticker 호출 + 방향별 계산) - 메트릭 카드 분리 (실거래 vs 실시간 open) # 안정성 - api.ts: error.detail array/object 안전 처리 ([object Object] 방지) - Chart.tsx: Plotly yaxis title 객체 형태 + 모바일 height 동적 조정 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65 lines
1.9 KiB
Python
65 lines
1.9 KiB
Python
import requests
|
|
from fastapi import APIRouter, Depends, Query
|
|
import trades_db
|
|
from ..auth import require_user, get_uid
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _serialize(rows):
|
|
out = []
|
|
for r in rows:
|
|
d = dict(r)
|
|
for k in ["entry_time", "exit_time", "candle_time", "fired_at"]:
|
|
if d.get(k) is not None:
|
|
d[k] = str(d[k])
|
|
out.append(d)
|
|
return out
|
|
|
|
|
|
def _get_all_tickers() -> dict:
|
|
try:
|
|
r = requests.get("https://fapi.binance.com/fapi/v1/ticker/price", timeout=5, verify=False)
|
|
return {t["symbol"]: float(t["price"]) for t in r.json()}
|
|
except Exception as e:
|
|
print(f"[trades_route] tickers fetch fail: {e}")
|
|
return {}
|
|
|
|
|
|
def _enrich_open_with_live(rows: list) -> list:
|
|
has_open = any(r.get("status") == "open" for r in rows)
|
|
if not has_open:
|
|
return rows
|
|
tickers = _get_all_tickers()
|
|
if not tickers:
|
|
return rows
|
|
for r in rows:
|
|
if r.get("status") != "open":
|
|
continue
|
|
sym = r.get("symbol")
|
|
cur = tickers.get(sym)
|
|
if cur is None or not r.get("entry_price"):
|
|
continue
|
|
entry = float(r["entry_price"])
|
|
if r.get("direction") == "long":
|
|
pnl = (cur - entry) / entry * 100.0
|
|
else:
|
|
pnl = (entry - cur) / entry * 100.0
|
|
r["current_price"] = cur
|
|
r["live_pnl_pct"] = round(pnl, 4)
|
|
return rows
|
|
|
|
|
|
@router.get("")
|
|
def list_trades(limit: int = Query(500, ge=1, le=2000), payload: dict = Depends(require_user)):
|
|
uid = get_uid(payload)
|
|
rows = _serialize(trades_db.fetch_trades(uid, limit=limit))
|
|
rows = _enrich_open_with_live(rows)
|
|
return rows
|
|
|
|
|
|
@router.get("/signals")
|
|
def list_signal_events(limit: int = Query(1000, ge=1, le=5000), payload: dict = Depends(require_user)):
|
|
uid = get_uid(payload)
|
|
return _serialize(trades_db.fetch_signal_events(uid, limit=limit))
|