Files
robinhood/backend/app/streaming/csv_feed.py
T
Krikorios 48e60d015f 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
2025-11-27 10:23:58 +02:00

139 lines
4.8 KiB
Python

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()