Spaces:
Runtime error
Runtime error
File size: 6,418 Bytes
d5b7ee9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | """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__)
@register_adapter
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")
@property
def adapter_id(self) -> str:
return "yfinance"
@property
def supports_paper_trading(self) -> bool:
return True # Simulated trading
@property
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",
)
|