Spaces:
Runtime error
Runtime error
| """yFinance adapter β free market data with mock trading.""" | |
| from __future__ import annotations | |
| import logging | |
| import random | |
| import time | |
| from datetime import datetime, timedelta, timezone | |
| import pandas as pd | |
| from trading_cli.execution.adapters.base import ( | |
| AccountInfo, | |
| MarketClock, | |
| OrderResult, | |
| Position, | |
| TradingAdapter, | |
| ) | |
| from trading_cli.execution.adapters.registry import register_adapter | |
| logger = logging.getLogger(__name__) | |
| class YFinanceAdapter(TradingAdapter): | |
| """yFinance adapter for free market data with simulated trading. | |
| Provides: | |
| - Real OHLCV data from Yahoo Finance | |
| - Real latest quotes from Yahoo Finance | |
| - Simulated account and positions (demo mode) | |
| """ | |
| def __init__(self, config: dict) -> None: | |
| self._config = config | |
| self._cash = config.get("initial_cash", 100_000.0) | |
| self._positions: dict[str, dict] = {} | |
| self._order_counter = 1000 | |
| self._base_prices = { | |
| "AAPL": 175.0, "TSLA": 245.0, "NVDA": 875.0, | |
| "MSFT": 415.0, "AMZN": 185.0, "GOOGL": 175.0, | |
| "META": 510.0, "SPY": 520.0, | |
| } | |
| logger.info("YFinanceAdapter initialized in demo mode") | |
| def adapter_id(self) -> str: | |
| return "yfinance" | |
| def supports_paper_trading(self) -> bool: | |
| return True # Simulated trading | |
| def is_demo_mode(self) -> bool: | |
| return True | |
| # ββ Account & Positions βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_account(self) -> AccountInfo: | |
| portfolio = sum( | |
| p["qty"] * self._get_mock_price(sym) | |
| for sym, p in self._positions.items() | |
| ) | |
| equity = self._cash + portfolio | |
| return AccountInfo( | |
| equity=equity, | |
| cash=self._cash, | |
| buying_power=self._cash * 4, | |
| portfolio_value=equity, | |
| ) | |
| def get_positions(self) -> list[Position]: | |
| positions = [] | |
| for sym, p in self._positions.items(): | |
| cp = self._get_mock_price(sym) | |
| ep = p["avg_price"] | |
| pl = (cp - ep) * p["qty"] | |
| plpc = (cp - ep) / ep if ep else 0.0 | |
| positions.append( | |
| Position(sym, p["qty"], ep, cp, pl, plpc, cp * p["qty"]) | |
| ) | |
| return positions | |
| # ββ Orders βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def submit_market_order(self, symbol: str, qty: int, side: str) -> OrderResult: | |
| price = self._get_mock_price(symbol) | |
| self._order_counter += 1 | |
| order_id = f"YF-{self._order_counter}" | |
| if side.upper() == "BUY": | |
| cost = price * qty | |
| if cost > self._cash: | |
| return OrderResult(order_id, symbol, side, qty, "rejected") | |
| self._cash -= cost | |
| if symbol in self._positions: | |
| p = self._positions[symbol] | |
| total_qty = p["qty"] + qty | |
| p["avg_price"] = (p["avg_price"] * p["qty"] + price * qty) / total_qty | |
| p["qty"] = total_qty | |
| else: | |
| self._positions[symbol] = {"qty": qty, "avg_price": price} | |
| else: # SELL | |
| if symbol not in self._positions or self._positions[symbol]["qty"] < qty: | |
| return OrderResult(order_id, symbol, side, qty, "rejected") | |
| self._cash += price * qty | |
| self._positions[symbol]["qty"] -= qty | |
| if self._positions[symbol]["qty"] == 0: | |
| del self._positions[symbol] | |
| return OrderResult(order_id, symbol, side, qty, "filled", price) | |
| def close_position(self, symbol: str) -> OrderResult | None: | |
| if symbol not in self._positions: | |
| return None | |
| qty = self._positions[symbol]["qty"] | |
| return self.submit_market_order(symbol, qty, "SELL") | |
| def _get_mock_price(self, symbol: str) -> float: | |
| """Get a mock price with small random walk for realism.""" | |
| base = self._base_prices.get(symbol, 100.0) | |
| noise = random.gauss(0, base * 0.002) | |
| return round(max(1.0, base + noise), 2) | |
| # ββ Market Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fetch_ohlcv(self, symbol: str, days: int = 90) -> pd.DataFrame: | |
| """Fetch OHLCV from yfinance.""" | |
| try: | |
| import yfinance as yf | |
| period = f"{min(days, 730)}d" | |
| df = yf.download(symbol, period=period, interval="1d", progress=False, auto_adjust=True) | |
| if df.empty: | |
| return pd.DataFrame() | |
| return df.tail(days) | |
| except Exception as exc: | |
| logger.error("yfinance fetch failed for %s: %s", symbol, exc) | |
| return pd.DataFrame() | |
| def get_latest_quote(self, symbol: str) -> float | None: | |
| """Get latest price from yfinance.""" | |
| try: | |
| import yfinance as yf | |
| ticker = yf.Ticker(symbol) | |
| info = ticker.fast_info | |
| price = getattr(info, "last_price", None) or getattr(info, "regularMarketPrice", None) | |
| if price: | |
| return float(price) | |
| hist = ticker.history(period="2d", interval="1d") | |
| if not hist.empty: | |
| return float(hist["Close"].iloc[-1]) | |
| return None | |
| except Exception as exc: | |
| logger.warning("yfinance latest quote failed for %s: %s", symbol, exc) | |
| return None | |
| # ββ Market Info βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_market_clock(self) -> MarketClock: | |
| now = datetime.now(tz=timezone.utc) | |
| # Mock: market open weekdays 9:30β16:00 ET (UTC-5) | |
| hour_et = (now.hour - 5) % 24 | |
| is_open = now.weekday() < 5 and 9 <= hour_et < 16 | |
| return MarketClock( | |
| is_open=is_open, | |
| next_open="09:30 ET", | |
| next_close="16:00 ET", | |
| ) | |