File size: 7,994 Bytes
d5b7ee9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
"""Binance adapter stub β€” crypto trading via Binance API.

This is a stub implementation. To enable:
1. Install ccxt: `uv add ccxt`
2. Add your Binance API keys to config
3. Implement the TODO sections below
"""

from __future__ import annotations

import logging
from datetime import datetime, timedelta, timezone

import pandas as pd

from trading_cli.execution.adapters.base import (
    AccountInfo,
    MarketClock,
    OrderResult,
    Position,
    TradingAdapter,
)
from trading_cli.execution.adapters.registry import register_adapter

logger = logging.getLogger(__name__)


@register_adapter
class BinanceAdapter(TradingAdapter):
    """Binance adapter for cryptocurrency trading.

    Requires: ccxt library (`uv add ccxt`)
    Config keys:
        binance_api_key: Your Binance API key
        binance_api_secret: Your Binance API secret
        binance_sandbox: Use sandbox/testnet (default: False)
    """

    def __init__(self, config: dict) -> None:
        self._config = config
        self._api_key = config.get("binance_api_key", "")
        self._api_secret = config.get("binance_api_secret", "")
        self._sandbox = config.get("binance_sandbox", False)
        self._demo = not (self._api_key and self._api_secret)

        if self._demo:
            logger.info("BinanceAdapter: no API keys found, stub mode only")
            return

        try:
            import ccxt
            self._exchange = ccxt.binance({
                "apiKey": self._api_key,
                "secret": self._api_secret,
                "enableRateLimit": True,
            })
            if self._sandbox:
                self._exchange.set_sandbox_mode(True)
            logger.info("BinanceAdapter connected (sandbox=%s)", self._sandbox)
        except ImportError:
            logger.warning("ccxt not installed. Run: uv add ccxt")
            self._demo = True
            self._exchange = None
        except Exception as exc:
            logger.error("Failed to connect to Binance: %s", exc)
            self._demo = True
            self._exchange = None

    @property
    def adapter_id(self) -> str:
        return "binance"

    @property
    def supports_paper_trading(self) -> bool:
        return self._sandbox  # Binance testnet

    @property
    def is_demo_mode(self) -> bool:
        return self._demo

    # ── Account & Positions ───────────────────────────────────────────────────

    def get_account(self) -> AccountInfo:
        if self._demo or not self._exchange:
            return AccountInfo(
                equity=100000.0,
                cash=100000.0,
                buying_power=100000.0,
                portfolio_value=100000.0,
            )
        # TODO: Implement real account fetch using self._exchange.fetch_balance()
        balance = self._exchange.fetch_balance()
        # Extract USDT balance as cash equivalent
        cash = float(balance.get("USDT", {}).get("free", 0))
        return AccountInfo(
            equity=cash,  # Simplified
            cash=cash,
            buying_power=cash,
            portfolio_value=cash,
        )

    def get_positions(self) -> list[Position]:
        if self._demo or not self._exchange:
            return []
        # TODO: Implement real position fetch
        # For crypto, positions are balances with non-zero amounts
        positions = []
        balance = self._exchange.fetch_balance()
        for currency, amount_info in balance.items():
            if isinstance(amount_info, dict) and amount_info.get("total", 0) > 0:
                if currency in ("free", "used", "total", "info"):
                    continue
                total = amount_info.get("total", 0)
                positions.append(
                    Position(
                        symbol=f"{currency}/USDT",
                        qty=total,
                        avg_entry_price=0.0,  # TODO: Track entry prices
                        current_price=0.0,  # TODO: Fetch current price
                        unrealized_pl=0.0,
                        unrealized_plpc=0.0,
                        market_value=0.0,
                        side="long",
                    )
                )
        return positions

    # ── Orders ───────────────────────────────────────────────────────────────

    def submit_market_order(self, symbol: str, qty: int, side: str) -> OrderResult:
        if self._demo or not self._exchange:
            return OrderResult(
                order_id=f"BINANCE-DEMO-{datetime.now().timestamp()}",
                symbol=symbol,
                action=side,
                qty=qty,
                status="filled",
                filled_price=0.0,
            )
        # TODO: Implement real order submission
        try:
            # Convert to ccxt format: 'BTC/USDT'
            order = self._exchange.create_market_order(symbol, side.lower(), qty)
            return OrderResult(
                order_id=order.get("id", "unknown"),
                symbol=symbol,
                action=side,
                qty=qty,
                status=order.get("status", "filled"),
                filled_price=float(order.get("average") or order.get("price") or 0),
            )
        except Exception as exc:
            logger.error("Binance order failed for %s %s %d: %s", side, symbol, qty, exc)
            raise

    def close_position(self, symbol: str) -> OrderResult | None:
        if self._demo or not self._exchange:
            return None
        # TODO: Implement position close
        # Need to look up current position qty and sell all
        return None

    # ── Market Data ───────────────────────────────────────────────────────────

    def fetch_ohlcv(self, symbol: str, days: int = 90) -> pd.DataFrame:
        if self._demo or not self._exchange:
            return pd.DataFrame()
        try:
            # Binance uses 'BTC/USDT' format
            ohlcv = self._exchange.fetch_ohlcv(symbol, timeframe="1d", limit=days)
            df = pd.DataFrame(
                ohlcv,
                columns=["timestamp", "Open", "High", "Low", "Close", "Volume"],
            )
            df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
            df.set_index("timestamp", inplace=True)
            return df
        except Exception as exc:
            logger.warning("Binance OHLCV fetch failed for %s: %s", symbol, exc)
            return pd.DataFrame()

    def get_latest_quote(self, symbol: str) -> float | None:
        if self._demo or not self._exchange:
            return None
        try:
            ticker = self._exchange.fetch_ticker(symbol)
            return float(ticker.get("last") or 0)
        except Exception as exc:
            logger.warning("Binance quote failed for %s: %s", symbol, exc)
            return None

    # ── Market Info ───────────────────────────────────────────────────────────

    def get_market_clock(self) -> MarketClock:
        # Crypto markets are 24/7
        return MarketClock(
            is_open=True,
            next_open="24/7",
            next_close="24/7",
        )

    # ── News ──────────────────────────────────────────────────────────────────

    def fetch_news(self, symbol: str, max_articles: int = 50,
                   days_ago: int = 0) -> list[tuple[str, float]]:
        # Binance doesn't provide news via API
        return []