|
6 | 6 | from binance.websocket.spot.websocket_client import SpotWebsocketClient as WebsocketClient |
7 | 7 |
|
8 | 8 | from algotrader.entities.candle import Candle |
| 9 | +from algotrader.entities.order_direction import OrderDirection |
9 | 10 | from algotrader.entities.serializable import Deserializable, Serializable |
10 | 11 | from algotrader.entities.timespan import TimeSpan |
11 | 12 |
|
12 | 13 | StreamedCandleCallback = Callable[[Candle], None] |
13 | 14 |
|
| 15 | +PRODUCTION = 'https://api.binance.com' |
| 16 | +TESTNET = 'https://testnet.binance.vision' |
| 17 | + |
14 | 18 |
|
15 | 19 | class BinanceProvider(Serializable, Deserializable): |
16 | 20 | logger = logging.getLogger('BinanceProvider') |
17 | 21 |
|
18 | | - def __init__(self, api_key: Optional[str] = '', api_secret: Optional[str] = '', enable_websocket: bool = False): |
| 22 | + def __init__(self, api_key: Optional[str] = '', api_secret: Optional[str] = '', |
| 23 | + enable_websocket: bool = False, testnet: bool = False): |
19 | 24 | self.api_key = api_key |
20 | 25 | self.api_secret = api_secret |
21 | 26 | self.enable_websocket = enable_websocket |
22 | | - self.client = Spot(api_key, api_secret) |
| 27 | + self.client = Spot(api_key, api_secret, base_url=TESTNET if testnet else PRODUCTION) |
23 | 28 |
|
24 | 29 | self.wsManager = WebsocketClient() |
25 | 30 | if enable_websocket: |
@@ -70,6 +75,35 @@ def _deserialize_candle(self, symbol: str, interval: TimeSpan, data: Dict) -> Ca |
70 | 75 |
|
71 | 76 | return Candle(symbol, interval, timestamp, open, close, high, low, volume) |
72 | 77 |
|
| 78 | + def send_bracket_order(self, symbol: str, direction: OrderDirection, quantity: float, |
| 79 | + triggering_price: float, position_entry_grace: float, spread: float, |
| 80 | + time_in_force: str = 'GTC'): |
| 81 | + |
| 82 | + grace_price = triggering_price * (1 + position_entry_grace) if direction == OrderDirection.BUY else \ |
| 83 | + triggering_price * (1 - position_entry_grace) |
| 84 | + |
| 85 | + take_profit_price = triggering_price * (1 + spread) if direction == OrderDirection.BUY else \ |
| 86 | + triggering_price * (1 - spread) |
| 87 | + |
| 88 | + stop_loss_price = triggering_price * (1 - spread) if direction == OrderDirection.BUY else \ |
| 89 | + triggering_price * (1 + spread) |
| 90 | + |
| 91 | + side = self._direction_to_side(direction) |
| 92 | + logging.info(f'Sending order for {symbol} {side} {quantity} at {grace_price}...') |
| 93 | + order_response = self.client.new_order(symbol=symbol, side=side, type='LIMIT', |
| 94 | + quantity=quantity, price=grace_price, |
| 95 | + timeInForce=time_in_force) |
| 96 | + |
| 97 | + logging.info(f'Order response: {order_response}') |
| 98 | + if order_response['status'] == 'FILLED': |
| 99 | + logging.info(f'Order filled, sending take profit and stop loss... ' |
| 100 | + f'take profit: {take_profit_price}, stop loss: {stop_loss_price}') |
| 101 | + |
| 102 | + opposite_side = self._direction_to_opposite_side(direction) |
| 103 | + self.client.new_oco_order(symbol=symbol, side=opposite_side, quantity=quantity, price=take_profit_price, |
| 104 | + stopPrice=stop_loss_price, time_in_force='GTC') |
| 105 | + return order_response |
| 106 | + |
73 | 107 | def get_symbol_history(self, symbol: str, interval: TimeSpan, start_time: datetime, |
74 | 108 | end_time: datetime = datetime.now()) -> List[Candle]: |
75 | 109 | self.logger.info(f'Getting {symbol} history from {start_time} to {end_time}...') |
@@ -111,6 +145,14 @@ def deserialize(cls, data: Dict): |
111 | 145 | def _timestamp_to_datetime(timestamp: int) -> datetime: |
112 | 146 | return datetime.fromtimestamp(timestamp / 1000) |
113 | 147 |
|
| 148 | + @staticmethod |
| 149 | + def _direction_to_side(direction: OrderDirection) -> str: |
| 150 | + return 'BUY' if direction == OrderDirection.Buy else 'SELL' |
| 151 | + |
| 152 | + @staticmethod |
| 153 | + def _direction_to_opposite_side(direction: OrderDirection) -> str: |
| 154 | + return 'SELL' if direction == OrderDirection.Buy else 'BUY' |
| 155 | + |
114 | 156 | @staticmethod |
115 | 157 | def _timespan_to_interval(timespan: TimeSpan) -> str: |
116 | 158 | if timespan == TimeSpan.Second: |
|
0 commit comments