Phase 1: initial codes
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Position:
|
||||
symbol: str
|
||||
shares: float
|
||||
avg_cost: float
|
||||
market_value: float = 0.0
|
||||
current_price: float = 0.0
|
||||
pnl: float = 0.0
|
||||
pnl_pct: float = 0.0
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountBalance:
|
||||
total_equity: float = 0.0
|
||||
cash: float = 0.0
|
||||
buying_power: float = 0.0
|
||||
day_pnl: float = 0.0
|
||||
day_pnl_pct: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Order:
|
||||
order_id: str
|
||||
symbol: str
|
||||
side: str # buy / sell
|
||||
quantity: float
|
||||
price: float
|
||||
status: str
|
||||
order_type: str
|
||||
placed_at: str = ""
|
||||
|
||||
|
||||
class BaseBroker(ABC):
|
||||
name: str = "base"
|
||||
|
||||
@abstractmethod
|
||||
def connect(self) -> bool:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def is_connected(self) -> bool:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_positions(self) -> list[Position]:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_balance(self) -> AccountBalance:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_orders(self, limit: int = 25) -> list[Order]:
|
||||
...
|
||||
@@ -0,0 +1,98 @@
|
||||
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 []
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
from brokers.base_broker import BaseBroker, Position, AccountBalance, Order
|
||||
|
||||
|
||||
class SchwabBroker(BaseBroker):
|
||||
name = "schwab"
|
||||
|
||||
def __init__(self, app_key: str, app_secret: str):
|
||||
self._app_key = app_key
|
||||
self._app_secret = app_secret
|
||||
self._client = None
|
||||
self._connected = False
|
||||
|
||||
def connect(self) -> bool:
|
||||
if not self._app_key or not self._app_secret:
|
||||
return False
|
||||
try:
|
||||
import schwabdev
|
||||
self._client = schwabdev.Client(self._app_key, self._app_secret)
|
||||
self._connected = True
|
||||
return True
|
||||
except Exception as e:
|
||||
self._connected = False
|
||||
raise RuntimeError(f"Schwab connection failed: {e}")
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return self._connected and self._client is not None
|
||||
|
||||
def get_positions(self) -> list[Position]:
|
||||
if not self.is_connected():
|
||||
return []
|
||||
try:
|
||||
response = self._client.account_linked().json()
|
||||
account_hash = response[0].get("hashValue", "")
|
||||
positions_resp = self._client.account(account_hash, fields="positions").json()
|
||||
raw_positions = positions_resp.get("securitiesAccount", {}).get("positions", [])
|
||||
result = []
|
||||
for p in raw_positions:
|
||||
instr = p.get("instrument", {})
|
||||
symbol = instr.get("symbol", "")
|
||||
shares = float(p.get("longQuantity", 0))
|
||||
avg_cost = float(p.get("averagePrice", 0))
|
||||
market_value = float(p.get("marketValue", 0))
|
||||
current_price = market_value / shares if shares else 0
|
||||
cost_basis = avg_cost * shares
|
||||
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=instr.get("description", ""),
|
||||
))
|
||||
return result
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def get_balance(self) -> AccountBalance:
|
||||
if not self.is_connected():
|
||||
return AccountBalance()
|
||||
try:
|
||||
response = self._client.account_linked().json()
|
||||
account_hash = response[0].get("hashValue", "")
|
||||
acct = self._client.account(account_hash).json()
|
||||
balances = acct.get("securitiesAccount", {}).get("currentBalances", {})
|
||||
return AccountBalance(
|
||||
total_equity=float(balances.get("equity", 0)),
|
||||
cash=float(balances.get("cashBalance", 0)),
|
||||
buying_power=float(balances.get("buyingPower", 0)),
|
||||
day_pnl=float(acct.get("securitiesAccount", {}).get("currentBalances", {}).get("dayProfitLoss", 0)),
|
||||
)
|
||||
except Exception:
|
||||
return AccountBalance()
|
||||
|
||||
def get_orders(self, limit: int = 25) -> list[Order]:
|
||||
if not self.is_connected():
|
||||
return []
|
||||
try:
|
||||
response = self._client.account_linked().json()
|
||||
account_hash = response[0].get("hashValue", "")
|
||||
from datetime import datetime, timedelta
|
||||
from_time = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%S+0000")
|
||||
to_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S+0000")
|
||||
orders_resp = self._client.account_orders(account_hash, from_time, to_time, max_results=limit).json()
|
||||
result = []
|
||||
for o in orders_resp:
|
||||
legs = o.get("orderLegCollection", [{}])
|
||||
instr = legs[0].get("instrument", {}) if legs else {}
|
||||
result.append(Order(
|
||||
order_id=str(o.get("orderId", "")),
|
||||
symbol=instr.get("symbol", ""),
|
||||
side=legs[0].get("instruction", "").lower() if legs else "",
|
||||
quantity=float(o.get("quantity", 0)),
|
||||
price=float(o.get("price", 0)),
|
||||
status=o.get("status", ""),
|
||||
order_type=o.get("orderType", ""),
|
||||
placed_at=o.get("enteredTime", ""),
|
||||
))
|
||||
return result
|
||||
except Exception:
|
||||
return []
|
||||
Reference in New Issue
Block a user