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,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Set, Tuple
|
||||
|
||||
from app.streaming.live_store import live_store
|
||||
from app.services.price_simulator import gold_simulator
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FeedKey:
|
||||
symbol: str
|
||||
timeframe: str
|
||||
|
||||
|
||||
class LocalFeedProvider:
|
||||
"""In-memory price feed backed by the gold price simulator."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._tasks: Dict[FeedKey, asyncio.Task] = {}
|
||||
self._pinned: Set[FeedKey] = set()
|
||||
self._lock = asyncio.Lock()
|
||||
self._active_counts: Dict[FeedKey, int] = {}
|
||||
|
||||
def get_status(self) -> list[dict]:
|
||||
out: list[dict] = []
|
||||
for key, task in self._tasks.items():
|
||||
hist = live_store.get_history(key.symbol, key.timeframe)
|
||||
last_ts = hist[-1]["time"] if hist else None
|
||||
last_iso = None
|
||||
if isinstance(last_ts, (int, float)):
|
||||
try:
|
||||
last_iso = datetime.utcfromtimestamp(int(last_ts)).isoformat() + "Z"
|
||||
except Exception:
|
||||
last_iso = None
|
||||
out.append(
|
||||
{
|
||||
"symbol": key.symbol,
|
||||
"timeframe": key.timeframe,
|
||||
"subscribers": self._active_counts.get(key, 0),
|
||||
"last_event_time": last_iso,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
async def subscribe(self, symbol: str, timeframe: str = "1m") -> Tuple[asyncio.Queue, Any]:
|
||||
if timeframe != "1m":
|
||||
raise ValueError("LocalFeedProvider currently supports timeframe '1m' only")
|
||||
key = FeedKey(symbol=symbol.upper().replace("/", ""), timeframe=timeframe)
|
||||
queue: asyncio.Queue = asyncio.Queue(maxsize=100)
|
||||
live_store.subscribe(key.symbol, key.timeframe, queue)
|
||||
async with self._lock:
|
||||
self._active_counts[key] = self._active_counts.get(key, 0) + 1
|
||||
if key not in self._tasks:
|
||||
self._tasks[key] = asyncio.create_task(self._run_simulator_poller(key))
|
||||
|
||||
async def _unsubscribe() -> None:
|
||||
live_store.unsubscribe(key.symbol, key.timeframe, queue)
|
||||
async with self._lock:
|
||||
self._active_counts[key] = max(0, self._active_counts.get(key, 0) - 1)
|
||||
if self._active_counts.get(key, 0) == 0 and key not in self._pinned:
|
||||
task = self._tasks.pop(key, None)
|
||||
if task:
|
||||
task.cancel()
|
||||
self._active_counts.pop(key, None)
|
||||
|
||||
return queue, _unsubscribe
|
||||
|
||||
async def ensure_stream(self, symbol: str, timeframe: str = "1m") -> None:
|
||||
if timeframe != "1m":
|
||||
raise ValueError("LocalFeedProvider currently supports timeframe '1m' only")
|
||||
key = FeedKey(symbol=symbol.upper().replace("/", ""), timeframe=timeframe)
|
||||
async with self._lock:
|
||||
self._pinned.add(key)
|
||||
if key not in self._tasks:
|
||||
self._tasks[key] = asyncio.create_task(self._run_simulator_poller(key))
|
||||
|
||||
async def release_stream(self, symbol: str, timeframe: str = "1m") -> None:
|
||||
key = FeedKey(symbol=symbol.upper().replace("/", ""), timeframe=timeframe)
|
||||
async with self._lock:
|
||||
self._pinned.discard(key)
|
||||
if self._active_counts.get(key, 0) == 0:
|
||||
task = self._tasks.pop(key, None)
|
||||
if task:
|
||||
task.cancel()
|
||||
|
||||
async def _run_simulator_poller(self, key: FeedKey) -> None:
|
||||
symbol = key.symbol
|
||||
simulator = gold_simulator
|
||||
if simulator.current_price < 2400 or simulator.current_price > 2900:
|
||||
simulator.current_price = 2650.0
|
||||
simulator.base_price = 2650.0
|
||||
|
||||
last_ts: int | None = None
|
||||
poll_interval = 3
|
||||
|
||||
while True:
|
||||
try:
|
||||
candle = simulator.get_live_candle(interval="1min")
|
||||
tsec = candle.time
|
||||
|
||||
if last_ts is None or tsec > last_ts:
|
||||
dt = datetime.utcfromtimestamp(tsec)
|
||||
evt = {
|
||||
"symbol": symbol,
|
||||
"timeframe": key.timeframe,
|
||||
"open_time": dt.isoformat(),
|
||||
"close_time": dt.isoformat(),
|
||||
"open": candle.open,
|
||||
"high": candle.high,
|
||||
"low": candle.low,
|
||||
"close": candle.close,
|
||||
"volume": candle.volume or 0.0,
|
||||
"is_closed": True,
|
||||
"source": "local_simulator",
|
||||
}
|
||||
try:
|
||||
live_store.ingest_bar(
|
||||
symbol=symbol,
|
||||
timeframe="1m",
|
||||
bar={
|
||||
"time": tsec,
|
||||
"open": evt["open"],
|
||||
"high": evt["high"],
|
||||
"low": evt["low"],
|
||||
"close": evt["close"],
|
||||
"volume": evt["volume"],
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
last_ts = tsec
|
||||
|
||||
await asyncio.sleep(poll_interval)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
await asyncio.sleep(poll_interval)
|
||||
|
||||
|
||||
local_feed = LocalFeedProvider()
|
||||
Reference in New Issue
Block a user