File size: 4,561 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
"""Market data fetching — Alpaca historical bars with yfinance fallback."""
 
from __future__ import annotations
 
import logging
import time
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING
 
import pandas as pd
 
if TYPE_CHECKING:
    from trading_cli.execution.alpaca_client import AlpacaClient
 
logger = logging.getLogger(__name__)
 
 
def fetch_ohlcv_alpaca(
    client: "AlpacaClient",
    symbol: str,
    days: int = 90,
) -> pd.DataFrame:
    """Fetch OHLCV bars from Alpaca historical data API."""
    try:
        from alpaca.data.requests import StockBarsRequest
        from alpaca.data.timeframe import TimeFrame
 
        end = datetime.now(tz=timezone.utc)
        start = end - timedelta(days=days + 10)  # extra buffer for weekends
 
        request = StockBarsRequest(
            symbol_or_symbols=symbol,
            timeframe=TimeFrame.Day,
            start=start,
            end=end,
            feed="iex",
        )
        bars = client.historical_client.get_stock_bars(request)
        df = bars.df
        if isinstance(df.index, pd.MultiIndex):
            df = df.xs(symbol, level=0) if symbol in df.index.get_level_values(0) else df
        df.index = pd.to_datetime(df.index, utc=True)
        df = df.rename(columns={"open": "Open", "high": "High", "low": "Low",
                                 "close": "Close", "volume": "Volume"})
        return df.tail(days)
    except Exception as exc:
        logger.warning("Alpaca OHLCV fetch failed for %s: %s — falling back to yfinance", symbol, exc)
        return fetch_ohlcv_yfinance(symbol, days)
 
 
def fetch_ohlcv_yfinance(symbol: str, days: int = 90) -> pd.DataFrame:
    """Fetch OHLCV bars from yfinance. Period can be long for daily interval."""
    try:
        import yfinance as yf
        # No more 730d cap for 1d data; yfinance handles 10y+ easily for daily.
        period = f"{days}d"
        df = yf.download(symbol, period=period, interval="1d", progress=False, auto_adjust=True)
        if df.empty:
            return pd.DataFrame()
        
        # Flatten MultiIndex columns if present (common in newer yfinance versions)
        if isinstance(df.columns, pd.MultiIndex):
            df.columns = df.columns.get_level_values(0)
            
        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_alpaca(client: "AlpacaClient", symbol: str) -> float | None:
    """Get latest trade price from Alpaca."""
    try:
        from alpaca.data.requests import StockLatestTradeRequest
 
        req = StockLatestTradeRequest(symbol_or_symbols=symbol, feed="iex")
        trades = client.historical_client.get_stock_latest_trade(req)
        return float(trades[symbol].price)
    except Exception as exc:
        logger.warning("Alpaca latest quote failed for %s: %s", symbol, exc)
        return None
 
 
def get_latest_quote_yfinance(symbol: str) -> float | None:
    """Get latest price from yfinance (free tier fallback)."""
    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
 
 
def get_latest_quotes_batch(
    client: "AlpacaClient | None",
    symbols: list[str],
) -> dict[str, float]:
    """Return {symbol: price} dict for multiple symbols."""
    prices: dict[str, float] = {}
    if client and not client.demo_mode:
        try:
            from alpaca.data.requests import StockLatestTradeRequest
 
            req = StockLatestTradeRequest(symbol_or_symbols=symbols, feed="iex")
            trades = client.historical_client.get_stock_latest_trade(req)
            for sym, trade in trades.items():
                prices[sym] = float(trade.price)
            return prices
        except Exception as exc:
            logger.warning("Batch Alpaca quote failed: %s — falling back", exc)
 
    # yfinance fallback
    for sym in symbols:
        price = get_latest_quote_yfinance(sym)
        if price:
            prices[sym] = price
        time.sleep(0.2)  # avoid hammering
    return prices