99 lines
3.7 KiB
Python
99 lines
3.7 KiB
Python
from __future__ import annotations
|
|
from brokers.base_broker import BaseBroker, Position, AccountBalance, Order
|
|
|
|
|
|
class RobinhoodBroker(BaseBroker):
|
|
name = "robinhood"
|
|
|
|
def __init__(self, username: str, password: str):
|
|
self._username = username
|
|
self._password = password
|
|
self._connected = False
|
|
|
|
def connect(self) -> bool:
|
|
if not self._username or not self._password:
|
|
return False
|
|
try:
|
|
import robin_stocks.robinhood as rh
|
|
rh.login(self._username, self._password)
|
|
self._connected = True
|
|
return True
|
|
except Exception as e:
|
|
self._connected = False
|
|
raise RuntimeError(f"Robinhood login failed: {e}")
|
|
|
|
def is_connected(self) -> bool:
|
|
return self._connected
|
|
|
|
def get_positions(self) -> list[Position]:
|
|
if not self.is_connected():
|
|
return []
|
|
try:
|
|
import robin_stocks.robinhood as rh
|
|
holdings = rh.account.build_holdings()
|
|
result = []
|
|
for symbol, data in holdings.items():
|
|
shares = float(data.get("quantity", 0))
|
|
avg_cost = float(data.get("average_buy_price", 0))
|
|
current_price = float(data.get("price", 0))
|
|
market_value = shares * current_price
|
|
cost_basis = shares * avg_cost
|
|
pnl = market_value - cost_basis
|
|
pnl_pct = (pnl / cost_basis * 100) if cost_basis else 0
|
|
result.append(Position(
|
|
symbol=symbol,
|
|
shares=shares,
|
|
avg_cost=avg_cost,
|
|
market_value=market_value,
|
|
current_price=current_price,
|
|
pnl=pnl,
|
|
pnl_pct=pnl_pct,
|
|
description=data.get("name", ""),
|
|
))
|
|
return result
|
|
except Exception:
|
|
return []
|
|
|
|
def get_balance(self) -> AccountBalance:
|
|
if not self.is_connected():
|
|
return AccountBalance()
|
|
try:
|
|
import robin_stocks.robinhood as rh
|
|
profile = rh.profiles.load_portfolio_profile()
|
|
return AccountBalance(
|
|
total_equity=float(profile.get("equity", 0)),
|
|
cash=float(profile.get("withdrawable_amount", 0)),
|
|
buying_power=float(profile.get("buying_power", profile.get("withdrawable_amount", 0))),
|
|
day_pnl=float(profile.get("equity_previous_close", 0)) - float(profile.get("equity", 0)),
|
|
)
|
|
except Exception:
|
|
return AccountBalance()
|
|
|
|
def get_orders(self, limit: int = 25) -> list[Order]:
|
|
if not self.is_connected():
|
|
return []
|
|
try:
|
|
import robin_stocks.robinhood as rh
|
|
orders = rh.orders.get_all_stock_orders()[:limit]
|
|
result = []
|
|
for o in orders:
|
|
symbol = ""
|
|
try:
|
|
instr = rh.stocks.get_instrument_by_url(o.get("instrument", ""))
|
|
symbol = instr.get("symbol", "") if instr else ""
|
|
except Exception:
|
|
pass
|
|
result.append(Order(
|
|
order_id=o.get("id", ""),
|
|
symbol=symbol,
|
|
side=o.get("side", ""),
|
|
quantity=float(o.get("quantity", 0)),
|
|
price=float(o.get("price") or o.get("average_price") or 0),
|
|
status=o.get("state", ""),
|
|
order_type=o.get("type", ""),
|
|
placed_at=o.get("created_at", ""),
|
|
))
|
|
return result
|
|
except Exception:
|
|
return []
|