FastAPI backend with a single-strategy async engine, paper and Binance brokers, pure-python indicators, a backtester that shares the engine's portfolio math, and a Vue dashboard with a live WebSocket feed. Adds the Gitea Actions CI/CD workflow that deploys to Traefik via docker_default.
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import time
|
|
from urllib.parse import urlencode
|
|
|
|
import httpx
|
|
|
|
from app.broker.base import Broker, Fill
|
|
from app.market.binance import BASE_URL, to_exchange_symbol
|
|
from app.models import OrderSide, utcnow
|
|
|
|
TESTNET_URL = "https://testnet.binance.vision"
|
|
|
|
|
|
class BinanceBroker(Broker):
|
|
"""Live Binance spot market-order execution (requires API credentials)."""
|
|
|
|
def __init__(
|
|
self,
|
|
api_key: str,
|
|
api_secret: str,
|
|
base_url: str = BASE_URL,
|
|
timeout: float = 10.0,
|
|
) -> None:
|
|
if not api_key or not api_secret:
|
|
raise ValueError("Binance broker requires EXCHANGE_API_KEY and EXCHANGE_API_SECRET")
|
|
self.api_key = api_key
|
|
self.api_secret = api_secret.encode()
|
|
self._client = httpx.AsyncClient(base_url=base_url, timeout=timeout)
|
|
|
|
def _sign(self, params: dict) -> dict:
|
|
params = {**params, "timestamp": int(time.time() * 1000), "recvWindow": 5000}
|
|
query = urlencode(params)
|
|
signature = hmac.new(self.api_secret, query.encode(), hashlib.sha256).hexdigest()
|
|
return {**params, "signature": signature}
|
|
|
|
async def _market_order(self, symbol: str, side: OrderSide, qty: float) -> Fill:
|
|
params = self._sign(
|
|
{
|
|
"symbol": to_exchange_symbol(symbol),
|
|
"side": side.value.upper(),
|
|
"type": "MARKET",
|
|
"quantity": f"{qty:.8f}".rstrip("0").rstrip("."),
|
|
}
|
|
)
|
|
response = await self._client.post(
|
|
"/api/v3/order",
|
|
params=params,
|
|
headers={"X-MBX-APIKEY": self.api_key},
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
executed = float(data.get("executedQty", qty))
|
|
quote = float(data.get("cummulativeQuoteQty", 0.0))
|
|
price = quote / executed if executed else 0.0
|
|
fee = sum(float(fill.get("commission", 0.0)) for fill in data.get("fills", []))
|
|
return Fill(
|
|
symbol=symbol,
|
|
side=side,
|
|
qty=executed,
|
|
price=price,
|
|
fee=fee,
|
|
ts=utcnow(),
|
|
)
|
|
|
|
async def buy(self, symbol: str, qty: float, ref_price: float) -> Fill:
|
|
return await self._market_order(symbol, OrderSide.BUY, qty)
|
|
|
|
async def sell(self, symbol: str, qty: float, ref_price: float) -> Fill:
|
|
return await self._market_order(symbol, OrderSide.SELL, qty)
|
|
|
|
async def close(self) -> None:
|
|
await self._client.aclose()
|