tradebot.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import time
  2. import logging
  3. import pandas as pd
  4. from binance.client import Client
  5. from binance.enums import *
  6. from binance import ThreadedWebsocketManager
  7. from twisted.internet import reactor
  8. from abc import ABC, abstractmethod # For abstract base class
  9. # Configure logging
  10. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
  11. logger = logging.getLogger(__name__)
  12. # Signal Actions (extensible enum-like)
  13. ACTION_BUY = 'buy'
  14. ACTION_SELL_TP = 'sell_tp' # Take profit
  15. ACTION_SELL_SL = 'sell_sl' # Stop loss
  16. ACTION_CANCEL = 'cancel' # Cancel existing order
  17. class BaseStrategy(ABC):
  18. """Abstract base class for strategies. Inherit and implement generate_signal()."""
  19. def __init__(self, params=None):
  20. self.params = params or {} # Strategy-specific params (e.g., {'sma_periods': 50})
  21. @abstractmethod
  22. def generate_signal(self, symbol, data, current_position):
  23. """
  24. Generate a signal based on data.
  25. - data: pd.DataFrame with OHLCV.
  26. - current_position: dict {'amount': float, 'entry_price': float}.
  27. Returns: dict like {'action': 'buy', 'amount': float, 'limit_price': float} or None.
  28. """
  29. pass
  30. def pre_process_data(self, data):
  31. """Optional hook: Pre-process data before signal generation."""
  32. return data
  33. def post_signal(self, signal):
  34. """Optional hook: Post-process signal (e.g., adjust for risk)."""
  35. return signal
  36. class SMAStrategy(BaseStrategy):
  37. """Example strategy: Buy if price > SMA, sell on TP/SL."""
  38. def generate_signal(self, symbol, data, current_position):
  39. data = self.pre_process_data(data)
  40. periods = self.params.get('sma_periods', 50)
  41. buy_discount_pct = self.params.get('buy_discount_pct', 0.005)
  42. take_profit_pct = self.params.get('take_profit_pct', 0.05)
  43. stop_loss_pct = self.params.get('stop_loss_pct', -0.03)
  44. df = pd.DataFrame(data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume', 'close_time', 'quote_av', 'trades', 'tb_base_av', 'tb_quote_av', 'ignore'])
  45. df['close'] = df['close'].astype(float)
  46. sma = df['close'].rolling(window=periods).mean().iloc[-1]
  47. current_price = df['close'].iloc[-1]
  48. if current_position['amount'] > 0: # Check exit
  49. pnl_pct = (current_price - current_position['entry_price']) / current_position['entry_price']
  50. if pnl_pct >= take_profit_pct:
  51. limit_price = current_price * (1 + self.params.get('sell_premium_pct', 0.005))
  52. return self.post_signal({'action': ACTION_SELL_TP, 'amount': current_position['amount'], 'limit_price': limit_price})
  53. elif pnl_pct <= stop_loss_pct:
  54. limit_price = current_price * (1 - self.params.get('sell_premium_pct', 0.005))
  55. return self.post_signal({'action': ACTION_SELL_SL, 'amount': current_position['amount'], 'limit_price': limit_price, 'stop_price': limit_price})
  56. else: # Check entry
  57. if current_price > sma:
  58. amount = self.params.get('position_size_usd', 100) / current_price
  59. limit_price = current_price * (1 - buy_discount_pct)
  60. return self.post_signal({'action': ACTION_BUY, 'amount': amount, 'limit_price': limit_price})
  61. return None
  62. class TradingBot:
  63. def __init__(self, api_key, api_secret, strategy, config=None):
  64. self.client = Client(api_key, api_secret, testnet=False) # Set testnet=True for testing
  65. self.bm = ThreadedWebsocketManager(api_key, api_secret, testnet=False)
  66. self.strategy = strategy
  67. self.config = config or {
  68. 'symbols': ['BTCUSDT'],
  69. 'timeframe': '1h',
  70. 'poll_interval': 30,
  71. 'loop_interval': 60,
  72. 'order_timeout': 300 # Seconds before canceling unfilled orders
  73. }
  74. self.positions = {sym: {'amount': 0, 'entry_price': 0, 'order_id': None, 'order_timestamp': None} for sym in self.config['symbols']}
  75. self.conn_key = None
  76. try:
  77. balance = self.client.get_account()
  78. print(balance) # Should print account details
  79. except Exception as e:
  80. print(e)
  81. self.start_websockets()
  82. def start_websockets(self):
  83. #self.conn_key = self.bm.user_socket(self.process_user_message)
  84. self.bm.start()
  85. self.bm.start_user_socket(callback=self.process_user_message)
  86. def process_user_message(self, msg):
  87. if msg['e'] == 'executionReport':
  88. symbol = msg['s']
  89. order_id = msg['i']
  90. status = msg['X']
  91. side = msg['S']
  92. filled_amount = float(msg['z'])
  93. logger.info(f"[{symbol}] Order Update: ID={order_id}, Status={status}, Side={side}, Filled={filled_amount}")
  94. if symbol in self.positions:
  95. pos = self.positions[symbol]
  96. if status in ['FILLED', 'PARTIALLY_FILLED']:
  97. if side == 'BUY':
  98. pos['amount'] += filled_amount
  99. pos['entry_price'] = float(msg['L'])
  100. elif side == 'SELL':
  101. pos['amount'] -= filled_amount
  102. if status == 'FILLED':
  103. pos['order_id'] = None
  104. pos['order_timestamp'] = None
  105. elif status == 'CANCELED':
  106. pos['order_id'] = None
  107. pos['order_timestamp'] = None
  108. def fetch_data(self, symbol):
  109. return self.client.get_klines(symbol=symbol, interval=self.config['timeframe'], limit=self.strategy.params.get('sma_periods', 50) + 1)
  110. def place_limit_order(self, symbol, signal):
  111. action = signal['action']
  112. amount = signal['amount']
  113. limit_price = signal['limit_price']
  114. if (limit_price is not None):
  115. limit_price = round(limit_price, 2) # Round to 2 decimal places for USDT pairs
  116. stop_price = signal.get('stop_price')
  117. if (stop_price is not None):
  118. stop_price = round(stop_price, 2)
  119. if action == ACTION_BUY:
  120. order_type = ORDER_TYPE_LIMIT
  121. side = SIDE_BUY
  122. elif action in [ACTION_SELL_TP, ACTION_SELL_SL]:
  123. order_type = ORDER_TYPE_LIMIT if action == ACTION_SELL_TP else ORDER_TYPE_STOP_LOSS_LIMIT
  124. side = SIDE_SELL
  125. else:
  126. return None
  127. try:
  128. amount = round(amount, 4)
  129. order_params = {
  130. 'symbol': symbol,
  131. 'side': side,
  132. 'type': order_type,
  133. 'timeInForce': TIME_IN_FORCE_GTC,
  134. 'quantity': amount,
  135. 'price': limit_price
  136. }
  137. if stop_price:
  138. order_params['stopPrice'] = stop_price
  139. order = self.client.create_order(**order_params)
  140. logger.info(f"[{symbol}] Placed Order: Action={action}, Amount={amount}, Limit Price={limit_price}")
  141. return order['orderId']
  142. except Exception as e:
  143. logger.error(f"[{symbol}] Order placement error: {e}")
  144. return None
  145. def cancel_order(self, symbol, order_id):
  146. try:
  147. self.client.cancel_order(symbol=symbol, orderId=order_id)
  148. logger.info(f"[{symbol}] Canceled Order: ID={order_id}")
  149. except Exception as e:
  150. logger.error(f"[{symbol}] Cancel error: {e}")
  151. def poll_updates(self):
  152. for symbol in self.config['symbols']:
  153. try:
  154. pos = self.positions[symbol]
  155. if pos['order_id']:
  156. order = self.client.get_order(symbol=symbol, orderId=pos['order_id'])
  157. logger.info(f"[{symbol}] Polled Order: ID={order['orderId']}, Status={order['status']}, Filled={order['executedQty']}")
  158. if order['status'] == 'FILLED':
  159. pos['order_id'] = None
  160. pos['order_timestamp'] = None
  161. # Cancel if timed out
  162. if pos['order_timestamp'] and time.time() - pos['order_timestamp'] > self.config['order_timeout']:
  163. self.cancel_order(symbol, pos['order_id'])
  164. pos['order_id'] = None
  165. pos['order_timestamp'] = None
  166. balance = self.client.get_asset_balance(asset=symbol[:-4]) # e.g., 'BTC' from 'BTCUSDT'
  167. usdt_balance = self.client.get_asset_balance(asset='USDT')
  168. logger.info(f"[{symbol}] Polled Position: Asset={balance['free']}, USDT={usdt_balance['free']}")
  169. pos['amount'] = float(balance['free']) # Sync position
  170. except Exception as e:
  171. logger.error(f"[{symbol}] Polling error: {e}")
  172. def run(self):
  173. while True:
  174. try:
  175. for symbol in self.config['symbols']:
  176. data = self.fetch_data(symbol)
  177. current_position = self.positions[symbol]
  178. signal = self.strategy.generate_signal(symbol, data, current_position)
  179. if signal and current_position['order_id'] is None: # No active order
  180. order_id = self.place_limit_order(symbol, signal)
  181. if order_id:
  182. self.positions[symbol]['order_id'] = order_id
  183. self.positions[symbol]['order_timestamp'] = time.time()
  184. self.poll_updates()
  185. time.sleep(self.config['loop_interval'])
  186. except Exception as e:
  187. logger.error(f"Error in main loop: {e}")
  188. time.sleep(10) # Retry
  189. if __name__ == "__main__":
  190. # Example usage: SMA Strategy with custom params
  191. strategy = SMAStrategy(params={
  192. 'sma_periods': 50,
  193. 'buy_discount_pct': 0.005,
  194. 'sell_premium_pct': 0.005,
  195. 'take_profit_pct': 0.05,
  196. 'stop_loss_pct': -0.03,
  197. 'position_size_usd': 100
  198. })
  199. bot = TradingBot(
  200. 'BJbjlf7yTcso2VS7lxMKQbybqDkzJt68CIYDdi005orbvxtgUJGYg5Jz5S3YED43', '9AMYjDGpfmxS8k7kNuwoVnwgJX9HSDaERYyR3Wl9qHsyviMef3mo8pyn6OfWTsHD',
  201. strategy=strategy,
  202. config={
  203. 'symbols': ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT', 'ADAUSDT', 'DOGEUSDT',
  204. 'AVAXUSDT', 'SHIBUSDT', 'DOTUSDT', 'LINKUSDT', 'TRXUSDT', 'UNIUSDT', 'LTCUSDT'], # Multi-symbol support
  205. 'timeframe': '1h',
  206. 'poll_interval': 30,
  207. 'loop_interval': 60,
  208. 'order_timeout': 300
  209. }
  210. )
  211. bot.run()