feat: Add Phase 4 advanced metrics and components
- Add advanced metrics dashboard with trade analytics - Add new trading components (EntryTypeAnalysis, MultiDayPositionTracker, NewsEventTracker, etc.) - Add strategy mode selector and trend confirmation - Add risk automation panel and slippage correlation analysis - Add daily trading plan enhancements with modal components - Add custom hooks (useApi, useLocalStorage, useAdvancedTradeMetrics) - Add broker service integration and trading API - Add test setup and vitest configuration - Include parquet data files for live market data - Add comprehensive documentation in docs/ folder
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""CSV/Parquet replay feed.
|
||||
|
||||
Loads OHLCV data from disk and replays it into live_store at a configurable
|
||||
speed. Useful for offline demos or backtesting visualizations.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Set, Tuple
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from app.streaming.live_store import live_store
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CSVKey:
|
||||
symbol: str
|
||||
timeframe: str
|
||||
|
||||
|
||||
class CSVFeedProvider:
|
||||
def __init__(self, data_dir: str | Path | None = None) -> None:
|
||||
self._subs: Dict[CSVKey, Set[asyncio.Queue]] = {}
|
||||
self._tasks: Dict[CSVKey, asyncio.Task] = {}
|
||||
self._pinned: Set[CSVKey] = set()
|
||||
self._lock = asyncio.Lock()
|
||||
self._data_dir = Path(data_dir or Path.cwd() / "data" / "parquet" / "live")
|
||||
self._speed = 1.0 # 1x realtime replay
|
||||
|
||||
def get_status(self) -> list[dict]:
|
||||
out: list[dict] = []
|
||||
for key, subs in self._subs.items():
|
||||
out.append(
|
||||
{
|
||||
"symbol": key.symbol,
|
||||
"timeframe": key.timeframe,
|
||||
"subscribers": len(subs),
|
||||
"source": "csv_replay",
|
||||
"data_dir": str(self._data_dir),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
async def subscribe(self, symbol: str, timeframe: str = "1m") -> Tuple[asyncio.Queue, Any]:
|
||||
key = CSVKey(symbol.upper().replace("/", ""), timeframe)
|
||||
queue: asyncio.Queue = asyncio.Queue(maxsize=100)
|
||||
async with self._lock:
|
||||
subs = self._subs.setdefault(key, set())
|
||||
subs.add(queue)
|
||||
if key not in self._tasks:
|
||||
self._tasks[key] = asyncio.create_task(self._run_replay(key))
|
||||
|
||||
async def _unsubscribe() -> None:
|
||||
async with self._lock:
|
||||
s = self._subs.get(key)
|
||||
if s and queue in s:
|
||||
s.remove(queue)
|
||||
try:
|
||||
queue.put_nowait(None)
|
||||
except Exception:
|
||||
pass
|
||||
if s and len(s) == 0 and key not in self._pinned:
|
||||
task = self._tasks.pop(key, None)
|
||||
if task:
|
||||
task.cancel()
|
||||
self._subs.pop(key, None)
|
||||
|
||||
return queue, _unsubscribe
|
||||
|
||||
async def ensure_stream(self, symbol: str, timeframe: str = "1m") -> None:
|
||||
key = CSVKey(symbol.upper().replace("/", ""), timeframe)
|
||||
async with self._lock:
|
||||
self._pinned.add(key)
|
||||
self._subs.setdefault(key, set())
|
||||
if key not in self._tasks:
|
||||
self._tasks[key] = asyncio.create_task(self._run_replay(key))
|
||||
|
||||
def set_speed(self, speed: float) -> None:
|
||||
self._speed = max(0.1, speed)
|
||||
|
||||
async def _run_replay(self, key: CSVKey) -> None:
|
||||
file_path = self._resolve_file(key.symbol, key.timeframe)
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"Replay file not found: {file_path}")
|
||||
|
||||
df = self._load_file(file_path)
|
||||
for row in df.itertuples():
|
||||
evt = {
|
||||
"symbol": key.symbol,
|
||||
"timeframe": key.timeframe,
|
||||
"open_time": datetime.utcfromtimestamp(int(row.time)).isoformat(),
|
||||
"close_time": datetime.utcfromtimestamp(int(row.time)).isoformat(),
|
||||
"open": float(row.open),
|
||||
"high": float(row.high),
|
||||
"low": float(row.low),
|
||||
"close": float(row.close),
|
||||
"volume": float(getattr(row, "volume", 0.0)),
|
||||
"is_closed": True,
|
||||
"source": "csv_replay",
|
||||
}
|
||||
live_store.ingest_bar(
|
||||
symbol=key.symbol,
|
||||
timeframe=key.timeframe,
|
||||
bar={
|
||||
"time": int(row.time),
|
||||
"open": evt["open"],
|
||||
"high": evt["high"],
|
||||
"low": evt["low"],
|
||||
"close": evt["close"],
|
||||
"volume": evt["volume"],
|
||||
},
|
||||
)
|
||||
subs = self._subs.get(key) or set()
|
||||
for queue in list(subs):
|
||||
try:
|
||||
if queue.full():
|
||||
queue.get_nowait()
|
||||
queue.put_nowait(evt)
|
||||
except Exception:
|
||||
subs.discard(queue)
|
||||
await asyncio.sleep((60 / self._speed)) # default 1m bars -> 1 minute
|
||||
|
||||
def _resolve_file(self, symbol: str, timeframe: str) -> Path:
|
||||
filename = f"{symbol}_{timeframe}.parquet"
|
||||
return self._data_dir / filename
|
||||
|
||||
def _load_file(self, path: Path) -> pd.DataFrame:
|
||||
if path.suffix == ".csv":
|
||||
return pd.read_csv(path)
|
||||
return pd.read_parquet(path)
|
||||
|
||||
|
||||
csv_feed = CSVFeedProvider()
|
||||
Reference in New Issue
Block a user