9da2e12473
- 24 autofix safe (SIM105 contextlib.suppress, F401 unused imports, I001 import order, B007 unused loop var, F811 redef, F841 unused). - 15 unsafe-fix (UP038 X|Y in isinstance, SIM108 ternary, ecc.). - Manual fix: SIM102 nested if in deribit term_structure, E402 imports in test_cot.py + sentiment server.py. - Ignore E741 (variabili 'l' in list comprehensions deribit/client.py — stilistico, non bug). Tests: 478/478 verdi.
386 lines
14 KiB
Python
386 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import datetime as _dt
|
|
from typing import Any
|
|
|
|
from alpaca.data.historical import (
|
|
CryptoHistoricalDataClient,
|
|
OptionHistoricalDataClient,
|
|
StockHistoricalDataClient,
|
|
)
|
|
from alpaca.data.requests import (
|
|
CryptoBarsRequest,
|
|
CryptoLatestQuoteRequest,
|
|
CryptoLatestTradeRequest,
|
|
OptionBarsRequest,
|
|
OptionChainRequest,
|
|
OptionLatestQuoteRequest,
|
|
StockBarsRequest,
|
|
StockLatestQuoteRequest,
|
|
StockLatestTradeRequest,
|
|
StockSnapshotRequest,
|
|
)
|
|
from alpaca.data.timeframe import TimeFrame, TimeFrameUnit
|
|
from alpaca.trading.client import TradingClient
|
|
from alpaca.trading.enums import (
|
|
AssetClass,
|
|
OrderSide,
|
|
QueryOrderStatus,
|
|
TimeInForce,
|
|
)
|
|
from alpaca.trading.requests import (
|
|
ClosePositionRequest,
|
|
GetAssetsRequest,
|
|
GetOrdersRequest,
|
|
LimitOrderRequest,
|
|
MarketOrderRequest,
|
|
ReplaceOrderRequest,
|
|
StopOrderRequest,
|
|
)
|
|
|
|
_TF_MAP = {
|
|
"1min": TimeFrame(1, TimeFrameUnit.Minute),
|
|
"5min": TimeFrame(5, TimeFrameUnit.Minute),
|
|
"15min": TimeFrame(15, TimeFrameUnit.Minute),
|
|
"30min": TimeFrame(30, TimeFrameUnit.Minute),
|
|
"1h": TimeFrame(1, TimeFrameUnit.Hour),
|
|
"1d": TimeFrame(1, TimeFrameUnit.Day),
|
|
"1w": TimeFrame(1, TimeFrameUnit.Week),
|
|
}
|
|
|
|
_ASSET_CLASSES = {"stocks", "crypto", "options"}
|
|
|
|
|
|
def _tf(interval: str) -> TimeFrame:
|
|
if interval in _TF_MAP:
|
|
return _TF_MAP[interval]
|
|
raise ValueError(f"unsupported timeframe: {interval}")
|
|
|
|
|
|
def _asset_class_enum(ac: str) -> AssetClass:
|
|
ac = ac.lower()
|
|
if ac == "stocks":
|
|
return AssetClass.US_EQUITY
|
|
if ac == "crypto":
|
|
return AssetClass.CRYPTO
|
|
if ac == "options":
|
|
return AssetClass.US_OPTION
|
|
raise ValueError(f"invalid asset_class: {ac}")
|
|
|
|
|
|
def _serialize(obj: Any) -> Any:
|
|
"""Recursively convert pydantic/datetime objects → json-safe."""
|
|
if obj is None or isinstance(obj, str | int | float | bool):
|
|
return obj
|
|
if isinstance(obj, _dt.datetime | _dt.date):
|
|
return obj.isoformat()
|
|
if isinstance(obj, dict):
|
|
return {k: _serialize(v) for k, v in obj.items()}
|
|
if isinstance(obj, list | tuple):
|
|
return [_serialize(v) for v in obj]
|
|
if hasattr(obj, "model_dump"):
|
|
return _serialize(obj.model_dump())
|
|
if hasattr(obj, "__dict__"):
|
|
return _serialize(vars(obj))
|
|
return str(obj)
|
|
|
|
|
|
class AlpacaClient:
|
|
def __init__(
|
|
self,
|
|
api_key: str,
|
|
secret_key: str,
|
|
paper: bool = True,
|
|
trading: Any | None = None,
|
|
stock_data: Any | None = None,
|
|
crypto_data: Any | None = None,
|
|
option_data: Any | None = None,
|
|
) -> None:
|
|
self.api_key = api_key
|
|
self.secret_key = secret_key
|
|
self.paper = paper
|
|
self._trading = trading or TradingClient(
|
|
api_key=api_key, secret_key=secret_key, paper=paper
|
|
)
|
|
self._stock = stock_data or StockHistoricalDataClient(
|
|
api_key=api_key, secret_key=secret_key
|
|
)
|
|
self._crypto = crypto_data or CryptoHistoricalDataClient(
|
|
api_key=api_key, secret_key=secret_key
|
|
)
|
|
self._option = option_data or OptionHistoricalDataClient(
|
|
api_key=api_key, secret_key=secret_key
|
|
)
|
|
|
|
async def _run(self, fn, /, *args, **kwargs):
|
|
return await asyncio.to_thread(fn, *args, **kwargs)
|
|
|
|
# ── Account / positions ──────────────────────────────────────
|
|
|
|
async def get_account(self) -> dict:
|
|
acc = await self._run(self._trading.get_account)
|
|
return _serialize(acc)
|
|
|
|
async def get_positions(self) -> list[dict]:
|
|
pos = await self._run(self._trading.get_all_positions)
|
|
return [_serialize(p) for p in pos]
|
|
|
|
async def get_activities(self, limit: int = 50) -> list[dict]:
|
|
acts = await self._run(self._trading.get_account_activities)
|
|
data = [_serialize(a) for a in acts]
|
|
return data[:limit]
|
|
|
|
# ── Assets ──────────────────────────────────────────────────
|
|
|
|
async def get_assets(
|
|
self, asset_class: str = "stocks", status: str = "active"
|
|
) -> list[dict]:
|
|
req = GetAssetsRequest(
|
|
asset_class=_asset_class_enum(asset_class),
|
|
status=status,
|
|
)
|
|
assets = await self._run(self._trading.get_all_assets, req)
|
|
return [_serialize(a) for a in assets[:500]]
|
|
|
|
# ── Market data ─────────────────────────────────────────────
|
|
|
|
async def get_ticker(self, symbol: str, asset_class: str = "stocks") -> dict:
|
|
ac = asset_class.lower()
|
|
if ac == "stocks":
|
|
req = StockLatestTradeRequest(symbol_or_symbols=symbol)
|
|
data = await self._run(self._stock.get_stock_latest_trade, req)
|
|
trade = data.get(symbol)
|
|
q_req = StockLatestQuoteRequest(symbol_or_symbols=symbol)
|
|
qdata = await self._run(self._stock.get_stock_latest_quote, q_req)
|
|
quote = qdata.get(symbol)
|
|
return {
|
|
"symbol": symbol,
|
|
"asset_class": "stocks",
|
|
"last_price": getattr(trade, "price", None),
|
|
"bid": getattr(quote, "bid_price", None),
|
|
"ask": getattr(quote, "ask_price", None),
|
|
"bid_size": getattr(quote, "bid_size", None),
|
|
"ask_size": getattr(quote, "ask_size", None),
|
|
"timestamp": _serialize(getattr(trade, "timestamp", None)),
|
|
}
|
|
if ac == "crypto":
|
|
req = CryptoLatestTradeRequest(symbol_or_symbols=symbol)
|
|
data = await self._run(self._crypto.get_crypto_latest_trade, req)
|
|
trade = data.get(symbol)
|
|
q_req = CryptoLatestQuoteRequest(symbol_or_symbols=symbol)
|
|
qdata = await self._run(self._crypto.get_crypto_latest_quote, q_req)
|
|
quote = qdata.get(symbol)
|
|
return {
|
|
"symbol": symbol,
|
|
"asset_class": "crypto",
|
|
"last_price": getattr(trade, "price", None),
|
|
"bid": getattr(quote, "bid_price", None),
|
|
"ask": getattr(quote, "ask_price", None),
|
|
"timestamp": _serialize(getattr(trade, "timestamp", None)),
|
|
}
|
|
if ac == "options":
|
|
req = OptionLatestQuoteRequest(symbol_or_symbols=symbol)
|
|
data = await self._run(self._option.get_option_latest_quote, req)
|
|
quote = data.get(symbol)
|
|
return {
|
|
"symbol": symbol,
|
|
"asset_class": "options",
|
|
"bid": getattr(quote, "bid_price", None),
|
|
"ask": getattr(quote, "ask_price", None),
|
|
"timestamp": _serialize(getattr(quote, "timestamp", None)),
|
|
}
|
|
raise ValueError(f"invalid asset_class: {asset_class}")
|
|
|
|
async def get_bars(
|
|
self,
|
|
symbol: str,
|
|
asset_class: str = "stocks",
|
|
interval: str = "1d",
|
|
start: str | None = None,
|
|
end: str | None = None,
|
|
limit: int = 1000,
|
|
) -> dict:
|
|
tf = _tf(interval)
|
|
start_dt = _dt.datetime.fromisoformat(start) if start else (
|
|
_dt.datetime.now(_dt.UTC) - _dt.timedelta(days=30)
|
|
)
|
|
end_dt = _dt.datetime.fromisoformat(end) if end else _dt.datetime.now(_dt.UTC)
|
|
ac = asset_class.lower()
|
|
if ac == "stocks":
|
|
req = StockBarsRequest(
|
|
symbol_or_symbols=symbol, timeframe=tf,
|
|
start=start_dt, end=end_dt, limit=limit,
|
|
)
|
|
data = await self._run(self._stock.get_stock_bars, req)
|
|
elif ac == "crypto":
|
|
req = CryptoBarsRequest(
|
|
symbol_or_symbols=symbol, timeframe=tf,
|
|
start=start_dt, end=end_dt, limit=limit,
|
|
)
|
|
data = await self._run(self._crypto.get_crypto_bars, req)
|
|
elif ac == "options":
|
|
req = OptionBarsRequest(
|
|
symbol_or_symbols=symbol, timeframe=tf,
|
|
start=start_dt, end=end_dt, limit=limit,
|
|
)
|
|
data = await self._run(self._option.get_option_bars, req)
|
|
else:
|
|
raise ValueError(f"invalid asset_class: {asset_class}")
|
|
bars_dict = getattr(data, "data", {}) or {}
|
|
rows = bars_dict.get(symbol, []) or []
|
|
bars = [
|
|
{
|
|
"timestamp": _serialize(getattr(b, "timestamp", None)),
|
|
"open": getattr(b, "open", None),
|
|
"high": getattr(b, "high", None),
|
|
"low": getattr(b, "low", None),
|
|
"close": getattr(b, "close", None),
|
|
"volume": getattr(b, "volume", None),
|
|
}
|
|
for b in rows
|
|
]
|
|
return {"symbol": symbol, "asset_class": ac, "interval": interval, "bars": bars}
|
|
|
|
async def get_snapshot(self, symbol: str) -> dict:
|
|
req = StockSnapshotRequest(symbol_or_symbols=symbol)
|
|
data = await self._run(self._stock.get_stock_snapshot, req)
|
|
return _serialize(data.get(symbol))
|
|
|
|
async def get_option_chain(
|
|
self,
|
|
underlying: str,
|
|
expiry: str | None = None,
|
|
) -> dict:
|
|
kwargs: dict[str, Any] = {"underlying_symbol": underlying}
|
|
if expiry:
|
|
kwargs["expiration_date"] = _dt.date.fromisoformat(expiry)
|
|
req = OptionChainRequest(**kwargs)
|
|
data = await self._run(self._option.get_option_chain, req)
|
|
return {
|
|
"underlying": underlying,
|
|
"expiry": expiry,
|
|
"contracts": _serialize(data),
|
|
}
|
|
|
|
# ── Orders ──────────────────────────────────────────────────
|
|
|
|
async def get_open_orders(self, limit: int = 50) -> list[dict]:
|
|
req = GetOrdersRequest(status=QueryOrderStatus.OPEN, limit=limit)
|
|
orders = await self._run(self._trading.get_orders, filter=req)
|
|
return [_serialize(o) for o in orders]
|
|
|
|
async def place_order(
|
|
self,
|
|
symbol: str,
|
|
side: str,
|
|
qty: float | None = None,
|
|
notional: float | None = None,
|
|
order_type: str = "market",
|
|
limit_price: float | None = None,
|
|
stop_price: float | None = None,
|
|
tif: str = "day",
|
|
asset_class: str = "stocks",
|
|
) -> dict:
|
|
side_enum = OrderSide.BUY if side.lower() == "buy" else OrderSide.SELL
|
|
tif_enum = TimeInForce(tif.lower())
|
|
ot = order_type.lower()
|
|
common = {
|
|
"symbol": symbol,
|
|
"side": side_enum,
|
|
"time_in_force": tif_enum,
|
|
}
|
|
if qty is not None:
|
|
common["qty"] = qty
|
|
if notional is not None:
|
|
common["notional"] = notional
|
|
if ot == "market":
|
|
req = MarketOrderRequest(**common)
|
|
elif ot == "limit":
|
|
if limit_price is None:
|
|
raise ValueError("limit_price required for limit order")
|
|
req = LimitOrderRequest(**common, limit_price=limit_price)
|
|
elif ot == "stop":
|
|
if stop_price is None:
|
|
raise ValueError("stop_price required for stop order")
|
|
req = StopOrderRequest(**common, stop_price=stop_price)
|
|
else:
|
|
raise ValueError(f"unsupported order_type: {order_type}")
|
|
order = await self._run(self._trading.submit_order, req)
|
|
return _serialize(order)
|
|
|
|
async def amend_order(
|
|
self,
|
|
order_id: str,
|
|
qty: float | None = None,
|
|
limit_price: float | None = None,
|
|
stop_price: float | None = None,
|
|
tif: str | None = None,
|
|
) -> dict:
|
|
kwargs: dict[str, Any] = {}
|
|
if qty is not None:
|
|
kwargs["qty"] = qty
|
|
if limit_price is not None:
|
|
kwargs["limit_price"] = limit_price
|
|
if stop_price is not None:
|
|
kwargs["stop_price"] = stop_price
|
|
if tif is not None:
|
|
kwargs["time_in_force"] = TimeInForce(tif.lower())
|
|
req = ReplaceOrderRequest(**kwargs)
|
|
order = await self._run(self._trading.replace_order_by_id, order_id, req)
|
|
return _serialize(order)
|
|
|
|
async def cancel_order(self, order_id: str) -> dict:
|
|
await self._run(self._trading.cancel_order_by_id, order_id)
|
|
return {"order_id": order_id, "canceled": True}
|
|
|
|
async def cancel_all_orders(self) -> list[dict]:
|
|
resp = await self._run(self._trading.cancel_orders)
|
|
return [_serialize(r) for r in resp]
|
|
|
|
# ── Position close ──────────────────────────────────────────
|
|
|
|
async def close_position(
|
|
self, symbol: str, qty: float | None = None, percentage: float | None = None
|
|
) -> dict:
|
|
req = None
|
|
if qty is not None or percentage is not None:
|
|
kwargs: dict[str, Any] = {}
|
|
if qty is not None:
|
|
kwargs["qty"] = str(qty)
|
|
if percentage is not None:
|
|
kwargs["percentage"] = str(percentage)
|
|
req = ClosePositionRequest(**kwargs)
|
|
order = await self._run(
|
|
self._trading.close_position, symbol, close_options=req
|
|
)
|
|
return _serialize(order)
|
|
|
|
async def close_all_positions(self, cancel_orders: bool = True) -> list[dict]:
|
|
resp = await self._run(
|
|
self._trading.close_all_positions, cancel_orders=cancel_orders
|
|
)
|
|
return [_serialize(r) for r in resp]
|
|
|
|
# ── Clock / calendar ────────────────────────────────────────
|
|
|
|
async def get_clock(self) -> dict:
|
|
clock = await self._run(self._trading.get_clock)
|
|
return _serialize(clock)
|
|
|
|
async def get_calendar(
|
|
self, start: str | None = None, end: str | None = None
|
|
) -> list[dict]:
|
|
from alpaca.trading.requests import GetCalendarRequest
|
|
|
|
kwargs: dict[str, Any] = {}
|
|
if start:
|
|
kwargs["start"] = _dt.date.fromisoformat(start)
|
|
if end:
|
|
kwargs["end"] = _dt.date.fromisoformat(end)
|
|
req = GetCalendarRequest(**kwargs) if kwargs else None
|
|
cal = await self._run(
|
|
self._trading.get_calendar, filters=req
|
|
) if req else await self._run(self._trading.get_calendar)
|
|
return [_serialize(c) for c in cal]
|