trading-openenv / server /simulator.py
prakharb01's picture
Upload folder using huggingface_hub
1b9c890 verified
"""
Simulator Engine Module
This module contains the historical data backtesting engine for the Trading Environment.
"""
import json
import logging
from typing import Dict, Any, List
from datetime import datetime
import pandas as pd
import yfinance as yf
class SimulatedBroker:
"""
Simulates a broker using historical data. Mocking Alpaca MCP interface.
"""
def __init__(self, asset_universe: List[str]):
self.asset_universe = asset_universe
self.data_store: Dict[str, pd.DataFrame] = {}
self.current_step = 0
self.max_steps = 0
self.load_data()
def load_data(self):
"""Loads historical data for the asset universe using yfinance."""
print(f"Loading historical data for {self.asset_universe} using yfinance (1y daily)...")
for sym in self.asset_universe:
try:
# yf.Ticker(sym).history(period="1y") gets 1 yr of daily bars
df = yf.Ticker(sym).history(period="1y")
if not df.empty:
self.data_store[sym] = df
else:
print(f"Warning: No data fetched for {sym}")
except Exception as e:
print(f"Error fetching data for {sym}: {e}")
if self.data_store:
# Find the minimum max_steps across all assets to prevent out of bounds
lengths = [len(df) for df in self.data_store.values()]
self.max_steps = min(lengths) - 1
print(f"✅ Loaded {len(self.data_store)} assets. Capable of {self.max_steps} daily steps from {self.data_store[self.asset_universe[0]].index[0].date()} to {self.data_store[self.asset_universe[0]].index[-1].date()}")
else:
raise ValueError("Failed to load any historical data.")
def reset_time(self, start_step: int = 0):
"""Resets the simulator to a specific step."""
self.current_step = min(start_step, self.max_steps)
def step_time(self):
"""Advances the internal clock by one timestep."""
if self.current_step < self.max_steps:
self.current_step += 1
def call_tool(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Polymorphic implementation of the MCP client's call_tool."""
if name == "get_stock_latest_quote":
# Match recent fix: using 'symbols'
symbol = arguments.get("symbols", "")
if not symbol or symbol not in self.data_store:
return {"success": False, "error": f"Symbol {symbol} not in loaded universe."}
df = self.data_store[symbol]
idx = min(self.current_step, len(df) - 1)
row = df.iloc[idx]
try:
price = float(row["Close"])
except Exception:
price = 100.0 # Extreme fallback
try:
date_str = str(df.index[idx].date())
except Exception:
date_str = "2026-01-01"
# Spoof the exact Alpaca format our environment parses!
# Format: {"quotes":{"AAPL":{"ap":266.91}}}
spoof_json = {
"quotes": {
symbol: {
"ap": price,
"bp": price,
"t": date_str
}
}
}
return {
"success": True,
"result": [{"type": "text", "text": json.dumps(spoof_json)}]
}
elif name == "place_stock_order":
symbol = arguments.get("symbol")
qty = arguments.get("qty")
side = arguments.get("side")
# Since the environment's VirtualLedger tracks exactly whether an order is allowed
# we just need to return a success to ensure the environment knows we accepted it.
return {
"success": True,
"result": [{"type": "text", "text": f"Simulated order placed. {side} {qty} {symbol}."}]
}
else:
return {"success": True, "result": [{"type": "text", "text": f"Simulated call mocked for {name}."}]}