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,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Utility script to pull historical bars from MetaTrader 5 into live_store.
|
||||
|
||||
Run inside the backend virtualenv with DATA_PROVIDER=metatrader.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
try:
|
||||
import MetaTrader5 as mt5 # type: ignore
|
||||
except Exception as exc: # pragma: no cover
|
||||
raise SystemExit("MetaTrader5 package not installed.") from exc
|
||||
|
||||
from app.streaming.live_store import live_store
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Import MT5 history into live_store.")
|
||||
parser.add_argument("symbol", help="Symbol, e.g., XAUUSD")
|
||||
parser.add_argument("timeframe", help="MT5 timeframe, e.g., 1m, 5m, 1h", default="1m")
|
||||
parser.add_argument("bars", type=int, default=1000, nargs="?", help="Number of bars to fetch")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def resolve_timeframe(name: str):
|
||||
mapping = {
|
||||
"1m": mt5.TIMEFRAME_M1,
|
||||
"5m": mt5.TIMEFRAME_M5,
|
||||
"15m": mt5.TIMEFRAME_M15,
|
||||
"30m": mt5.TIMEFRAME_M30,
|
||||
"60m": mt5.TIMEFRAME_H1,
|
||||
"1h": mt5.TIMEFRAME_H1,
|
||||
"4h": mt5.TIMEFRAME_H4,
|
||||
"1d": mt5.TIMEFRAME_D1,
|
||||
}
|
||||
return mapping.get(name.lower(), mt5.TIMEFRAME_M1)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
if not mt5.initialize():
|
||||
raise SystemExit(f"Failed to initialize MetaTrader5: {mt5.last_error()}")
|
||||
|
||||
timeframe = resolve_timeframe(args.timeframe)
|
||||
rates = mt5.copy_rates_from_pos(args.symbol, timeframe, 0, args.bars)
|
||||
if not rates:
|
||||
raise SystemExit("No rates returned. Check symbol/timeframe.")
|
||||
|
||||
for row in rates:
|
||||
live_store.ingest_bar(
|
||||
symbol=args.symbol,
|
||||
timeframe=args.timeframe,
|
||||
bar={
|
||||
"time": int(row['time']),
|
||||
"open": float(row['open']),
|
||||
"high": float(row['high']),
|
||||
"low": float(row['low']),
|
||||
"close": float(row['close']),
|
||||
"volume": float(row.get('tick_volume', 0.0)),
|
||||
},
|
||||
)
|
||||
|
||||
print(f"Imported {len(rates)} bars for {args.symbol} {args.timeframe}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Utility for priming LiveStore and flushing sample candles to parquet.
|
||||
|
||||
This script spins up the configured streaming data provider (local simulator by
|
||||
default), keeps it running for a short window, and forces flushes so that
|
||||
parquet partitions are materialized under ``data/parquet/live``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import sys
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.streaming.live_store import live_store
|
||||
from app.streaming.data_provider import data_provider
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Prime LiveStore and flush parquet output for a symbol/timeframe")
|
||||
parser.add_argument("--symbol", default="XAUUSD", help="Symbol to stream (default: XAUUSD)")
|
||||
parser.add_argument("--timeframe", default="1m", help="Timeframe to stream (default: 1m)")
|
||||
parser.add_argument(
|
||||
"--duration",
|
||||
type=int,
|
||||
default=120,
|
||||
help="How long to keep the stream hot (seconds, default: 120)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--flush-interval",
|
||||
type=int,
|
||||
default=30,
|
||||
help="How often to flush to parquet during the run (seconds, default: 30)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
async def _maybe_consume(queue: asyncio.Queue) -> None:
|
||||
"""Continuously drain the queue so it doesn't grow unbounded."""
|
||||
|
||||
try:
|
||||
while True:
|
||||
item = await queue.get()
|
||||
if item is None:
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
async def _prime_store(symbol: str, timeframe: str, duration: int, flush_interval: int) -> Tuple[int, Path]:
|
||||
symbol = symbol.upper().replace("/", "")
|
||||
timeframe = timeframe.lower()
|
||||
|
||||
os.makedirs(live_store.root, exist_ok=True)
|
||||
target_dir = Path(live_store.root) / symbol / timeframe
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
queue, unsubscribe = await data_provider.subscribe(symbol, timeframe=timeframe)
|
||||
consumer_task = asyncio.create_task(_maybe_consume(queue))
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
start = loop.time()
|
||||
next_flush = flush_interval
|
||||
|
||||
while True:
|
||||
elapsed = loop.time() - start
|
||||
if elapsed >= duration:
|
||||
break
|
||||
sleep_for = max(0.0, min(next_flush - elapsed, 1.0))
|
||||
await asyncio.sleep(sleep_for)
|
||||
elapsed = loop.time() - start
|
||||
if elapsed + 1e-6 >= next_flush:
|
||||
await live_store.flush_parquet()
|
||||
next_flush += flush_interval
|
||||
finally:
|
||||
await unsubscribe()
|
||||
consumer_task.cancel()
|
||||
with contextlib.suppress(Exception):
|
||||
await consumer_task
|
||||
|
||||
await live_store.flush_parquet()
|
||||
history = live_store.get_history(symbol, timeframe)
|
||||
return len(history), target_dir
|
||||
|
||||
|
||||
async def _async_main() -> None:
|
||||
args = _parse_args()
|
||||
bars, target_dir = await _prime_store(
|
||||
symbol=args.symbol,
|
||||
timeframe=args.timeframe,
|
||||
duration=args.duration,
|
||||
flush_interval=args.flush_interval,
|
||||
)
|
||||
|
||||
partitions = sorted(target_dir.glob("date=*"))
|
||||
total_files = sum(len(list(part.glob("*.parquet"))) for part in partitions)
|
||||
latest_partition = partitions[-1] if partitions else None
|
||||
|
||||
print("\nLiveStore setup complete:")
|
||||
print(f" Symbol: {args.symbol.upper().replace('/', '')}")
|
||||
print(f" Timeframe: {args.timeframe}")
|
||||
print(f" Bars in memory: {bars}")
|
||||
print(f" Root directory: {target_dir}")
|
||||
print(f" Partitions found: {len(partitions)}")
|
||||
print(f" Parquet files: {total_files}")
|
||||
if latest_partition:
|
||||
print(f" Latest partition: {latest_partition}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
asyncio.run(_async_main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user