Skip to main content
Glama
simulation_service.py7.16 kB
# src/server/domain/services/simulation_service.py """Simulation Service for Paper Trading. Manages virtual accounts and executes simulated trades using Redis for state. """ import json from datetime import datetime from decimal import Decimal from typing import Any, Dict, Optional from src.server.domain.adapter_manager import AdapterManager from src.server.infrastructure.connections.redis_connection import RedisConnection from src.server.utils.logger import logger class SimulationService: """Service for managing simulated trading accounts and execution.""" def __init__(self, redis_connection: RedisConnection, adapter_manager: AdapterManager): self.redis = redis_connection.get_client() self.adapter_manager = adapter_manager self._fee_rate = Decimal("0.001") # 0.1% default fee def _get_balance_key(self, account_id: str) -> str: return f"sim:account:{account_id}:balance" def _get_positions_key(self, account_id: str) -> str: return f"sim:account:{account_id}:positions" async def init_account( self, account_id: str, initial_cash: float = 100000.0, currency: str = "USD" ) -> Dict[str, Any]: """Initialize or reset a simulation account.""" try: balance_key = self._get_balance_key(account_id) positions_key = self._get_positions_key(account_id) # Reset state await self.redis.delete(balance_key) await self.redis.delete(positions_key) # Set initial balance await self.redis.hset(balance_key, currency, str(initial_cash)) logger.info(f"Initialized simulation account {account_id} with {initial_cash} {currency}") return { "account_id": account_id, "status": "initialized", "balance": {currency: initial_cash}, "positions": {} } except Exception as e: logger.error(f"Failed to init account {account_id}: {e}") raise async def get_account_state(self, account_id: str) -> Dict[str, Any]: """Get current account balance and positions.""" try: balance_key = self._get_balance_key(account_id) positions_key = self._get_positions_key(account_id) # Fetch data balances_raw = await self.redis.hgetall(balance_key) positions_raw = await self.redis.hgetall(positions_key) # Convert to proper types balances = {k: float(v) for k, v in balances_raw.items()} positions = {k: float(v) for k, v in positions_raw.items()} # Calculate total equity (requires fetching current prices, optional for speed) # For now, just return raw state return { "account_id": account_id, "balances": balances, "positions": positions } except Exception as e: logger.error(f"Failed to get account state {account_id}: {e}") return {"error": str(e)} async def execute_order( self, account_id: str, symbol: str, side: str, quantity: float, order_type: str = "market", price: Optional[float] = None ) -> Dict[str, Any]: """Execute a simulated order.""" try: # 1. Get Real-time Price if order_type == "market": current_price_obj = await self.adapter_manager.get_real_time_price(symbol) if not current_price_obj: raise ValueError(f"Could not fetch price for {symbol}") exec_price = Decimal(str(current_price_obj.price)) else: # For limit orders in simulation, we assume immediate fill if price is reasonable # This is a simplification. Real simulation would need an order book. if price is None: raise ValueError("Price required for limit order") exec_price = Decimal(str(price)) # Apply slippage (random or fixed, here we use simple fixed logic for now) # Buy: pay more, Sell: get less slippage = Decimal("0.0005") # 0.05% if side.lower() == "buy": final_price = exec_price * (Decimal("1") + slippage) else: final_price = exec_price * (Decimal("1") - slippage) qty = Decimal(str(quantity)) notional = final_price * qty fee = notional * self._fee_rate # 2. Check Balance/Positions balance_key = self._get_balance_key(account_id) positions_key = self._get_positions_key(account_id) # Assume USD for simplicity, or derive from symbol (e.g. BTC-USD -> USD) currency = "USD" if side.lower() == "buy": # Check cash current_cash_str = await self.redis.hget(balance_key, currency) current_cash = Decimal(current_cash_str) if current_cash_str else Decimal("0") total_cost = notional + fee if current_cash < total_cost: return { "status": "rejected", "reason": f"Insufficient funds. Have {current_cash}, need {total_cost}" } # Update State await self.redis.hincrbyfloat(balance_key, currency, float(-total_cost)) await self.redis.hincrbyfloat(positions_key, symbol, float(qty)) elif side.lower() == "sell": # Check position current_pos_str = await self.redis.hget(positions_key, symbol) current_pos = Decimal(current_pos_str) if current_pos_str else Decimal("0") if current_pos < qty: return { "status": "rejected", "reason": f"Insufficient position. Have {current_pos}, need {qty}" } # Update State proceeds = notional - fee await self.redis.hincrbyfloat(balance_key, currency, float(proceeds)) await self.redis.hincrbyfloat(positions_key, symbol, float(-qty)) else: return {"status": "error", "reason": f"Invalid side {side}"} # 3. Return Receipt return { "status": "filled", "account_id": account_id, "symbol": symbol, "side": side, "type": order_type, "quantity": float(qty), "price": float(final_price), "fee": float(fee), "notional": float(notional), "timestamp": datetime.now().isoformat() } except Exception as e: logger.error(f"Execute order failed: {e}", exc_info=True) return {"status": "error", "reason": str(e)}

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/huweihua123/stock-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server