You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

339 lines
12 KiB
Python

"""TGTC Paper Trading API endpoints."""
from __future__ import annotations
import datetime as dt
import os
from typing import Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/tgtc", tags=["tgtc-trading"])
def _db_path() -> str:
from pathlib import Path
project_root = Path(__file__).parent.parent.parent.parent
env_path = os.environ.get("TGTC_TRADER_DB", "data/paper/tgtc.db")
path = Path(env_path) if Path(env_path).is_absolute() else project_root / env_path
path.parent.mkdir(parents=True, exist_ok=True)
return str(path)
def _state() -> Any:
from apps.tgtc_trader.state import TGTCStateManager
return TGTCStateManager(_db_path())
# ── Request models ────────────────────────────────────────────────────────────
class CreateSessionRequest(BaseModel):
name: str
config: str
capital: float = 10000.0
class AutoStartRequest(BaseModel):
sessions: list[str] = []
dry_run: bool = True
class StartNowRequest(BaseModel):
sessions: list[str] = []
dry_run: bool = True
collect_duration_secs: int = 300
quick_end_after_mins: int = 60
class BacktestRequest(BaseModel):
date: str # YYYY-MM-DD start date
config: str # config path
universe: str | None = None
end_date: str | None = None # YYYY-MM-DD end date (multi-day if set)
# ── Sessions ──────────────────────────────────────────────────────────────────
@router.get("/sessions")
def list_sessions() -> dict[str, Any]:
state = _state()
sessions = state.list_sessions()
today = dt.date.today().isoformat()
results = []
for s in sessions:
daily = state.get_daily_state(s.session_id, today)
ran_today = daily is not None and daily.phase not in ("idle", "", None)
equity = state.get_equity(s.session_id) or s.initial_equity
total_pnl = equity - s.initial_equity
results.append({
"session_id": s.session_id,
"session_name": s.session_name,
"config_path": s.config_path,
"initial_equity": s.initial_equity,
"current_equity": round(equity, 2),
"total_return_pct": round((total_pnl / s.initial_equity) * 100, 2) if s.initial_equity > 0 else 0.0,
"created_at": s.created_at,
"status": s.status,
"ran_today": ran_today,
"phase": daily.phase if daily else "idle",
})
return {"sessions": results}
@router.post("/sessions")
def create_session(req: CreateSessionRequest) -> dict[str, Any]:
from pathlib import Path
import yaml
project_root = Path(__file__).parent.parent.parent.parent
config_path = req.config if Path(req.config).is_absolute() else str(project_root / req.config)
if not Path(config_path).exists():
raise HTTPException(status_code=404, detail=f"Config not found: {config_path}")
try:
raw = yaml.safe_load(Path(config_path).read_text()) or {}
if raw.get("strategy_mode") != "tgtc":
raise HTTPException(
status_code=400,
detail=f"Config strategy_mode must be 'tgtc', got '{raw.get('strategy_mode')}'"
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid config: {e}")
state = _state()
if state.get_session(req.name) is not None:
raise HTTPException(status_code=409, detail=f"Session '{req.name}' already exists")
session_id = state.create_session(req.name, config_path, req.capital)
return {"session_id": session_id, "session_name": req.name,
"config_path": config_path, "initial_equity": req.capital}
@router.get("/sessions/{session_id}")
def get_session(session_id: str) -> dict[str, Any]:
state = _state()
session = state.get_session(session_id)
if session is None:
raise HTTPException(status_code=404, detail="Session not found")
equity = state.get_equity(session.session_id) or session.initial_equity
return {
"session_id": session.session_id,
"session_name": session.session_name,
"config_path": session.config_path,
"initial_equity": session.initial_equity,
"current_equity": round(equity, 2),
"created_at": session.created_at,
"status": session.status,
}
@router.delete("/sessions/{session_id}")
def delete_session(session_id: str) -> dict[str, Any]:
state = _state()
session = state.get_session(session_id)
if session is None:
raise HTTPException(status_code=404, detail="Session not found")
state.delete_session(session_id)
return {"deleted": session_id}
# ── Strategies list ───────────────────────────────────────────────────────────
@router.get("/strategies")
def list_strategies() -> dict[str, Any]:
"""List available TGTC strategy configs."""
import yaml
from pathlib import Path
project_root = Path(__file__).parent.parent.parent.parent
strat_dir = project_root / "configs" / "intraday" / "strategies"
strategies = []
for p in sorted(strat_dir.glob("tgtc_*.yaml")):
try:
raw = yaml.safe_load(p.read_text()) or {}
if raw.get("strategy_mode") != "tgtc":
continue
meta = raw.get("_meta", {})
strategies.append({
"config_path": str(p.relative_to(project_root)),
"name": meta.get("name", p.stem),
"id": meta.get("id", p.stem),
"status": meta.get("status", ""),
"live_readiness": meta.get("live_readiness", ""),
"description": meta.get("description", ""),
})
except Exception:
pass
return {"strategies": strategies}
# ── Live data ─────────────────────────────────────────────────────────────────
@router.get("/sessions/{session_id}/snapshots")
def get_snapshots(session_id: str, date: str | None = None) -> dict[str, Any]:
state = _state()
if state.get_session(session_id) is None:
raise HTTPException(status_code=404, detail="Session not found")
d = date or dt.date.today().isoformat()
rows = state.get_latest_snapshots(session_id, d)
return {"date": d, "snapshots": rows, "count": len(rows)}
@router.get("/sessions/{session_id}/candidates")
def get_candidates(session_id: str, date: str | None = None) -> dict[str, Any]:
state = _state()
if state.get_session(session_id) is None:
raise HTTPException(status_code=404, detail="Session not found")
d = date or dt.date.today().isoformat()
return {"date": d, "candidates": state.get_candidates(session_id, d)}
@router.get("/sessions/{session_id}/positions")
def get_positions(session_id: str) -> dict[str, Any]:
state = _state()
if state.get_session(session_id) is None:
raise HTTPException(status_code=404, detail="Session not found")
positions = state.get_all_positions(session_id)
return {"positions": positions}
@router.get("/sessions/{session_id}/trades")
def get_trades(session_id: str) -> dict[str, Any]:
state = _state()
if state.get_session(session_id) is None:
raise HTTPException(status_code=404, detail="Session not found")
return {"trades": state.get_trades(session_id)}
@router.get("/sessions/{session_id}/equity")
def get_equity(session_id: str) -> dict[str, Any]:
state = _state()
session = state.get_session(session_id)
if session is None:
raise HTTPException(status_code=404, detail="Session not found")
snapshots = state.get_daily_snapshots(session_id)
return {
"initial_equity": session.initial_equity,
"current_equity": state.get_equity(session_id) or session.initial_equity,
"daily_snapshots": snapshots,
}
# ── Auto scheduler ────────────────────────────────────────────────────────────
@router.get("/auto")
def get_auto_status() -> dict[str, Any]:
from apps.web.tgtc_service import tgtc_auto_scheduler
status = tgtc_auto_scheduler.get_status()
log_text = tgtc_auto_scheduler.get_log(lines=200)
return {**status, "log": log_text, "log_lines": log_text.splitlines()[-50:]}
@router.post("/auto/start")
async def start_auto(req: AutoStartRequest) -> dict[str, Any]:
from apps.web.tgtc_service import tgtc_auto_scheduler
if tgtc_auto_scheduler.running:
raise HTTPException(status_code=409, detail="TGTC scheduler already running")
try:
tgtc_auto_scheduler.start(
sessions=req.sessions,
db_path=_db_path(),
dry_run=req.dry_run,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return {"started": True, "dry_run": req.dry_run}
@router.post("/auto/start-now")
async def start_auto_now(req: StartNowRequest) -> dict[str, Any]:
from apps.web.tgtc_service import tgtc_auto_scheduler
if tgtc_auto_scheduler.running:
raise HTTPException(status_code=409, detail="TGTC scheduler already running")
try:
tgtc_auto_scheduler.start(
sessions=req.sessions,
db_path=_db_path(),
dry_run=req.dry_run,
start_now=True,
collect_duration_secs=req.collect_duration_secs,
quick_end_after_mins=req.quick_end_after_mins,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return {
"started_now": True,
"dry_run": req.dry_run,
"collect_duration_secs": req.collect_duration_secs,
"quick_end_after_mins": req.quick_end_after_mins,
}
@router.post("/auto/stop")
def stop_auto() -> dict[str, Any]:
from apps.web.tgtc_service import tgtc_auto_scheduler
tgtc_auto_scheduler.stop()
return {"stopped": True}
@router.post("/auto/clear-log")
def clear_auto_log() -> dict[str, Any]:
from apps.web.tgtc_service import tgtc_auto_scheduler
tgtc_auto_scheduler.clear_log()
return {"cleared": True}
# ── Backtest ──────────────────────────────────────────────────────────────────
@router.post("/backtest/submit")
async def submit_backtest(req: BacktestRequest) -> dict[str, Any]:
from pathlib import Path
project_root = Path(__file__).parent.parent.parent.parent
config_path = req.config if Path(req.config).is_absolute() else str(project_root / req.config)
if not Path(config_path).exists():
raise HTTPException(status_code=404, detail=f"Config not found: {config_path}")
try:
dt.date.fromisoformat(req.date)
if req.end_date:
end = dt.date.fromisoformat(req.end_date)
if end < dt.date.fromisoformat(req.date):
raise HTTPException(status_code=400, detail="end_date must be >= date")
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=f"Invalid date format: {e}")
from apps.web.tgtc_service import submit_backtest as _submit
task_id = await _submit(req.date, config_path, req.universe, req.end_date)
return {"task_id": task_id, "status": "pending"}
@router.get("/backtest/tasks")
def list_backtest_tasks() -> dict[str, Any]:
from apps.web.tgtc_service import get_bt_tasks
return {"tasks": get_bt_tasks()}
@router.get("/backtest/tasks/{task_id}")
def get_backtest_task(task_id: str) -> dict[str, Any]:
from apps.web.tgtc_service import get_bt_task
task = get_bt_task(task_id)
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
return task
@router.get("/backtest/tasks/{task_id}/result")
def get_backtest_result(task_id: str) -> dict[str, Any]:
from apps.web.tgtc_service import get_bt_task
task = get_bt_task(task_id)
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
if task["status"] != "completed":
raise HTTPException(status_code=409, detail=f"Task status: {task['status']}")
return task.get("result") or {}