Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions brokers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""
Broker module for PowerTrader AI.
Provides a unified interface for different cryptocurrency exchanges.
"""

import os
from typing import Optional

from .base import BrokerAPI
from .robinhood import RobinhoodBroker
from .bitvavo import BitvavoBroker
from .paper import PaperBroker


# Available brokers (real trading)
BROKERS = {
"robinhood": RobinhoodBroker,
"bitvavo": BitvavoBroker,
}


def get_broker(
broker_name: str,
base_dir: Optional[str] = None,
paper_trading: bool = False,
paper_balance: float = 10000.0,
) -> BrokerAPI:
"""
Factory function to create a broker instance.

Args:
broker_name: Name of the broker ('robinhood' or 'bitvavo')
base_dir: Base directory for credential files (default: current dir)
paper_trading: If True, wrap broker in paper trading simulator
paper_balance: Initial balance for paper trading

Returns:
Configured broker instance

Raises:
ValueError: If broker_name is not supported
SystemExit: If credentials are not found
"""
broker_name = broker_name.lower().strip()

if broker_name not in BROKERS:
raise ValueError(
f"Unsupported broker: {broker_name}. "
f"Available brokers: {', '.join(BROKERS.keys())}"
)

if base_dir is None:
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Create the real broker
if broker_name == "robinhood":
real_broker = _create_robinhood_broker(base_dir)
elif broker_name == "bitvavo":
real_broker = _create_bitvavo_broker(base_dir)

# Wrap in paper trading if enabled
if paper_trading:
state_file = os.path.join(base_dir, "paper_trading_state.json")
return PaperBroker(
price_source=real_broker,
initial_balance=paper_balance,
base_currency=real_broker.base_currency,
state_file=state_file,
)

return real_broker


def _create_robinhood_broker(base_dir: str) -> RobinhoodBroker:
"""Create and configure Robinhood broker."""
key_path = os.path.join(base_dir, "r_key.txt")
secret_path = os.path.join(base_dir, "r_secret.txt")

api_key = ""
private_key = ""

try:
with open(key_path, "r", encoding="utf-8") as f:
api_key = (f.read() or "").strip()
with open(secret_path, "r", encoding="utf-8") as f:
private_key = (f.read() or "").strip()
except Exception:
pass

if not api_key or not private_key:
print(
"\n[PowerTrader] Robinhood API credentials not found.\n"
"Open the GUI and go to Settings → Robinhood API → Setup / Update.\n"
"That wizard will generate your keypair, tell you where to paste "
"the public key on Robinhood,\n"
"and will save r_key.txt + r_secret.txt so this trader can authenticate.\n"
)
raise SystemExit(1)

return RobinhoodBroker(api_key, private_key)


def _create_bitvavo_broker(base_dir: str) -> BitvavoBroker:
"""Create and configure Bitvavo broker."""
key_path = os.path.join(base_dir, "b_key.txt")
secret_path = os.path.join(base_dir, "b_secret.txt")

api_key = ""
api_secret = ""

try:
with open(key_path, "r", encoding="utf-8") as f:
api_key = (f.read() or "").strip()
with open(secret_path, "r", encoding="utf-8") as f:
api_secret = (f.read() or "").strip()
except Exception:
pass

if not api_key or not api_secret:
print(
"\n[PowerTrader] Bitvavo API credentials not found.\n"
"Open the GUI and go to Settings → Bitvavo API → Setup / Update.\n"
"Create API keys at https://account.bitvavo.com/user/api\n"
"and save them to b_key.txt + b_secret.txt.\n"
)
raise SystemExit(1)

return BitvavoBroker(api_key, api_secret)


__all__ = [
"BrokerAPI",
"RobinhoodBroker",
"BitvavoBroker",
"PaperBroker",
"get_broker",
"BROKERS",
]
Binary file added brokers/__pycache__/__init__.cpython-314.pyc
Binary file not shown.
Binary file added brokers/__pycache__/base.cpython-314.pyc
Binary file not shown.
Binary file added brokers/__pycache__/bitvavo.cpython-314.pyc
Binary file not shown.
Binary file added brokers/__pycache__/robinhood.cpython-314.pyc
Binary file not shown.
149 changes: 149 additions & 0 deletions brokers/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""
Abstract base class for broker implementations.
All broker integrations (Robinhood, Bitvavo, etc.) should inherit from this class.
"""

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Tuple


class BrokerAPI(ABC):
"""Abstract base class defining the interface for all broker implementations."""

# Broker identification
name: str = "base"
base_currency: str = "USD"

@abstractmethod
def get_account(self) -> Optional[Dict[str, Any]]:
"""
Get account information including buying power.

Returns:
Dict with account info or None on failure.
Expected keys: 'buying_power', 'buying_power_currency'
"""
pass

@abstractmethod
def get_holdings(self) -> Optional[Dict[str, Any]]:
"""
Get current holdings/positions.

Returns:
Dict with holdings info or None on failure.
Expected format: {'results': [{'asset_code': 'BTC', 'total_quantity': '0.5'}, ...]}
"""
pass

@abstractmethod
def get_trading_pairs(self) -> List[Dict[str, Any]]:
"""
Get available trading pairs.

Returns:
List of trading pair dicts or empty list on failure.
"""
pass

@abstractmethod
def get_orders(self, symbol: str) -> Optional[Dict[str, Any]]:
"""
Get order history for a symbol.

Args:
symbol: Trading pair symbol (e.g., 'BTC-USD' or 'BTC-EUR')

Returns:
Dict with orders info or None on failure.
Expected format: {'results': [order1, order2, ...]}
"""
pass

@abstractmethod
def get_price(self, symbols: List[str]) -> Tuple[Dict[str, float], Dict[str, float], List[str]]:
"""
Get current bid/ask prices for symbols.

Args:
symbols: List of trading pair symbols

Returns:
Tuple of (buy_prices, sell_prices, valid_symbols)
- buy_prices: {symbol: ask_price}
- sell_prices: {symbol: bid_price}
- valid_symbols: list of symbols that returned valid prices
"""
pass

@abstractmethod
def place_buy_order(
self,
client_order_id: str,
side: str,
order_type: str,
symbol: str,
amount_in_base_currency: float,
) -> Optional[Dict[str, Any]]:
"""
Place a buy order.

Args:
client_order_id: Unique order identifier
side: 'buy'
order_type: 'market' or 'limit'
symbol: Trading pair symbol
amount_in_base_currency: Amount to spend in base currency (USD/EUR)

Returns:
Order response dict or None on failure.
"""
pass

@abstractmethod
def place_sell_order(
self,
client_order_id: str,
side: str,
order_type: str,
symbol: str,
asset_quantity: float,
) -> Optional[Dict[str, Any]]:
"""
Place a sell order.

Args:
client_order_id: Unique order identifier
side: 'sell'
order_type: 'market' or 'limit'
symbol: Trading pair symbol
asset_quantity: Amount of asset to sell

Returns:
Order response dict or None on failure.
"""
pass

def format_symbol(self, coin: str) -> str:
"""
Format a coin symbol to the broker's trading pair format.

Args:
coin: Base coin symbol (e.g., 'BTC')

Returns:
Formatted trading pair (e.g., 'BTC-USD' or 'BTC-EUR')
"""
return f"{coin}-{self.base_currency}"

def extract_coin(self, symbol: str) -> str:
"""
Extract the coin symbol from a trading pair.

Args:
symbol: Trading pair (e.g., 'BTC-USD')

Returns:
Coin symbol (e.g., 'BTC')
"""
return symbol.split("-")[0]
Loading