├── .gitignore ├── README.md ├── backtest.py ├── main.py ├── markets ├── __init__.py ├── binance │ ├── __init__.py │ ├── binance_market.py │ ├── binance_rest_client.py │ └── binance_web_socket.py ├── import.py ├── market.py └── markets.py ├── misc ├── __init__.py ├── console.py └── schedule.py ├── programs ├── __init__.py ├── ma.py └── program.py ├── redfox.png ├── requirements.txt └── ui ├── __init__.py └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | __pycache__ 4 | venv 5 | cache 6 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cryptocurrency algorithmic trading framework through Binance API 2 | 3 | ## Implemented Brokers 4 | - Binance 5 | 6 | ## Usage 7 | To run RedWolf, first clone the repository. 8 | ```bash 9 | git clone https://github.com/justusip/redwolf.git 10 | cd redwolf 11 | ``` 12 | Prepare a .env file containing your Binance API key and secret placed at the project root. 13 | ``` 14 | BINANCE_API_KEY=... 15 | BINANCE_SECRET=... 16 | ``` 17 | Start the service via the following command. 18 | ```bash 19 | python3 main.py 20 | ``` 21 | ``` 22 | [+][22/04 00:07:25.061783][Core] Initializing... 23 | [+][22/04 00:07:25.068504][ UI ] Started interfacing server on localhost:2255 24 | [#][22/04 00:07:25.105159][BNRS] [->][GET] /api/v3/time {} 25 | [#][22/04 00:07:25.251526][BNRS] [<-][200] {'serverTime': 1650557245220} 26 | [+][22/04 00:07:25.251770][BNXX] Google NTP time: 1650557245099 (11ms behind). 27 | [+][22/04 00:07:25.251927][BNXX] Binance server time: 1650557245220 (115ms behind). 28 | [#][22/04 00:07:25.252059][BNRS] [->][GET] /api/v3/exchangeInfo {} 29 | [#][22/04 00:07:25.486054][BNRS] [<-][200] (hidden - 2745942 bytes) 30 | [#][22/04 00:07:25.517089][BNRS] [->][GET] /sapi/v1/asset/tradeFee {'timestamp': 1650557245516, 'signature': 'e9e11a38602766c74d9aac8f53b67850faa4da053ae49cd299871b3855564107'} 31 | [#][22/04 00:07:25.650099][BNRS] [<-][200] (hidden - 157181 bytes) 32 | [#][22/04 00:07:25.653106][BNRS] [->][GET] /api/v3/ticker/bookTicker {} 33 | [#][22/04 00:07:25.748239][BNRS] [<-][200] (hidden - 253997 bytes) 34 | [#][22/04 00:07:25.752183][BNRS] [->][GET] /api/v3/ticker/24hr {} 35 | [#][22/04 00:07:25.915073][BNRS] [<-][200] (hidden - 1109324 bytes) 36 | [+][22/04 00:07:25.921117][BNXX] Market definitions updated. 1449 active tickers were loaded. 37 | [+][22/04 00:07:25.921438][Core] 1 market(s) ready. 38 | [+][22/04 00:07:25.921492][Core] Initialized. (860ms) 39 | [+][22/04 00:07:25.921539][Core] 1 program(s) loaded. 40 | [+][22/04 00:07:25.921748][Core] 1 program(s) up and running. 41 | [+][22/04 00:07:25.921982][BNXX] Connecting to Binance's WebSocket market streams... 42 | [+][22/04 00:07:26.627556][BNXX] Connected to Binance's WebSocket market streams. 43 | ``` 44 | 45 | ## Implementing a Custom Trading Strategy 46 | Please refer to the files under the programs folder. 47 | -------------------------------------------------------------------------------- /backtest.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | import datetime as datetime 4 | 5 | con = sqlite3.connect('cache/cache.db') 6 | cur = con.cursor() 7 | 8 | rows = cur.execute(f"select * from 'prices' where symbol='ADAUSDT' order by symbol asc, time asc") 9 | start = datetime.datetime(2021, 8, 1) 10 | end = datetime.datetime(2021, 9, 1) 11 | 12 | print(f"Starting at {start} and ending at {end}.") 13 | 14 | i = 0 15 | for row in rows: 16 | print(row) 17 | 18 | con.close() 19 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import time 4 | from pathlib import Path 5 | 6 | from dotenv import load_dotenv 7 | 8 | from markets.binance.binance_market import BinanceMarket 9 | from markets.markets import Markets 10 | from misc.console import log 11 | from programs.ma import MovingAverage 12 | from programs.program import Program 13 | from ui.server import Server 14 | 15 | TAG = "Core" 16 | 17 | dotenv_path = Path(".").resolve() / '.env' 18 | load_dotenv(dotenv_path) 19 | 20 | 21 | class Core: 22 | def __init__(self): 23 | self.server = None 24 | self.markets: Markets = Markets() 25 | self.programs = [Program] 26 | 27 | async def main(self): 28 | start = time.time() 29 | log(TAG, "Initializing...") 30 | 31 | self.server = Server() 32 | await self.server.start() 33 | 34 | self.markets.append( 35 | BinanceMarket( 36 | os.environ.get("BINANCE_API_KEY"), 37 | os.environ.get("BINANCE_SECRET") 38 | ) 39 | ) 40 | 41 | await self.markets.init() 42 | self.markets.run() 43 | 44 | log(TAG, f"{len(self.markets)} market(s) ready.") 45 | log(TAG, f"Initialized. ({(time.time() - start) * 1000 :.0f}ms)") 46 | 47 | self.programs = [ 48 | MovingAverage() 49 | ] 50 | 51 | log(TAG, f"{len(self.programs)} program(s) loaded.") 52 | for program in self.programs: 53 | await program.init(self.markets) 54 | log(TAG, f"{len(self.programs)} program(s) up and running.") 55 | while True: 56 | for program in self.programs: 57 | await program.run_schedules(self.markets) 58 | await asyncio.sleep(.1) 59 | 60 | await self.server.stop() 61 | 62 | 63 | if __name__ == "__main__": 64 | core = Core() 65 | asyncio.run(core.main()) 66 | -------------------------------------------------------------------------------- /markets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusip/cryptowizard/f4556f58dc0c09be3d97d288ddb3ec0c5ca3ad0e/markets/__init__.py -------------------------------------------------------------------------------- /markets/binance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusip/cryptowizard/f4556f58dc0c09be3d97d288ddb3ec0c5ca3ad0e/markets/binance/__init__.py -------------------------------------------------------------------------------- /markets/binance/binance_market.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import math 3 | import time 4 | from decimal import Decimal 5 | from typing import Optional 6 | import ntplib 7 | 8 | import aiohttp 9 | from aiohttp import ClientWebSocketResponse, WSMessage 10 | 11 | from markets.binance.binance_rest_client import BinanceRestClient 12 | from markets.market import Market 13 | from misc.console import log, error 14 | 15 | TAG = "BNXX" 16 | 17 | 18 | class BinanceMarket(Market): 19 | def __init__(self, key, secret): 20 | super().__init__("BNCE") 21 | self.client = BinanceRestClient(key, secret) 22 | self.tickers: dict[str, Ticker] = {} 23 | self.ws: Optional[ClientWebSocketResponse] = None 24 | 25 | async def init(self): 26 | tk_google_res = ntplib.NTPClient().request('time.google.com') 27 | tk_google_time = tk_google_res.tx_time * 1000 28 | tk_google_diff = -tk_google_res.offset * 1000 29 | 30 | tk_binance_local = time.time() * 1000 31 | tk_binance_res = await self.client.send_unsigned("/api/v3/time") 32 | tk_binance_time = int(tk_binance_res["serverTime"]) 33 | tk_binance_diff = tk_binance_local - tk_binance_time 34 | 35 | log(TAG, f"Google NTP time: {tk_google_time:.0f} " 36 | f"({abs(tk_google_diff):.0f}ms {'ahead' if tk_google_diff > 0 else 'behind'}).") 37 | log(TAG, f"Binance server time: {tk_binance_time:.0f} " 38 | f"({abs(tk_binance_diff):.0f}ms {'ahead' if tk_binance_diff > 0 else 'behind'}).") 39 | 40 | if abs(tk_binance_diff) > 1000: 41 | raise Exception("Local time and Binance server has time difference greater than 1000ms.") 42 | 43 | for o in (await self.client.send_unsigned("/api/v3/exchangeInfo"))["symbols"]: 44 | if o["status"] != "TRADING": 45 | continue 46 | 47 | t = Ticker(o["symbol"], o["baseAsset"], o["quoteAsset"]) 48 | self.tickers[o["symbol"]] = t 49 | 50 | t.qty_precision = int(o["quotePrecision"]) 51 | t.price_precision = int(o["baseAssetPrecision"]) 52 | 53 | def filter_val(filter_type: str, key: str): 54 | return [f for f in o["filters"] if f["filterType"] == filter_type][0][key] 55 | 56 | t.price_min = Decimal(filter_val("PRICE_FILTER", "minPrice")) 57 | t.price_max = Decimal(filter_val("PRICE_FILTER", "maxPrice")) 58 | t.price_step = Decimal(filter_val("PRICE_FILTER", "tickSize")) 59 | if Decimal.is_zero(t.price_min) or Decimal.is_zero(t.price_max) or Decimal.is_zero(t.price_step): 60 | t.price_filter_disabled = True 61 | 62 | t.qty_min = Decimal(filter_val("LOT_SIZE", "minQty")) 63 | t.qty_max = Decimal(filter_val("LOT_SIZE", "maxQty")) 64 | t.qty_step = Decimal(filter_val("LOT_SIZE", "stepSize")) 65 | 66 | t.min_notional = Decimal(filter_val("MIN_NOTIONAL", "minNotional")) 67 | 68 | for o in await self.client.send_signed("GET", "/sapi/v1/asset/tradeFee"): 69 | if o["symbol"] not in self.tickers: 70 | continue 71 | t = self.tickers[o["symbol"]] 72 | t.trading_fee_maker = Decimal(o["makerCommission"]) 73 | t.trading_fee_taker = Decimal(o["takerCommission"]) 74 | 75 | for o in await self.client.send_unsigned("/api/v3/ticker/bookTicker"): 76 | if o["symbol"] not in self.tickers: 77 | continue 78 | t = self.tickers[o["symbol"]] 79 | t.best_bid_price = Decimal(o["bidPrice"]) 80 | t.best_bid_quantity = Decimal(o["bidQty"]) 81 | t.best_ask_price = Decimal(o["askPrice"]) 82 | t.best_ask_quantity = Decimal(o["askQty"]) 83 | 84 | for o in await self.client.send_unsigned("/api/v3/ticker/24hr"): 85 | if o["symbol"] not in self.tickers: 86 | continue 87 | t = self.tickers[o["symbol"]] 88 | t.high = Decimal(o["highPrice"]) 89 | t.low = Decimal(o["lowPrice"]) 90 | t.base_volume = Decimal(o["volume"]) 91 | t.quote_volume = Decimal(o["quoteVolume"]) 92 | 93 | log(TAG, f"Market definitions updated. {len(self.tickers)} active tickers were loaded.") 94 | 95 | async def run(self): 96 | bytes_recv = 0 97 | 98 | async def count(): 99 | nonlocal bytes_recv 100 | interval = 1 101 | while True: 102 | await asyncio.sleep(interval) 103 | bytes_per_sec = bytes_recv / interval 104 | log("WS", f"{bytes_per_sec / 1024:.3f}kb/s") 105 | bytes_recv = 0 106 | 107 | # asyncio.ensure_future(count()) 108 | 109 | try: 110 | log(TAG, "Connecting to Binance's WebSocket market streams...") 111 | async with aiohttp.ClientSession() as session: 112 | async with session.ws_connect('wss://stream.binance.com:9443/stream') as self.ws: 113 | log(TAG, "Connected to Binance's WebSocket market streams.") 114 | await self.ws.send_json({ 115 | "method": "SUBSCRIBE", 116 | "params": 117 | [ 118 | "!bookTicker", 119 | "!miniTicker@arr" 120 | ], 121 | "id": 1 122 | }) 123 | msg: WSMessage 124 | async for msg in self.ws: 125 | if msg.type == aiohttp.WSMsgType.TEXT: 126 | bytes_recv += len(msg.data) 127 | msg_json = msg.json() 128 | if "stream" in msg_json: 129 | payload = msg_json["data"] 130 | if msg_json["stream"] == "!bookTicker": 131 | if payload["s"] not in self.tickers: 132 | continue 133 | ticker = self.tickers[payload["s"]] 134 | ticker.ws_best_last_updated = time.time() 135 | ticker.best_bid_price = Decimal(payload["b"]) 136 | ticker.best_bid_quantity = Decimal(payload["B"]) 137 | ticker.best_ask_price = Decimal(payload["a"]) 138 | ticker.best_ask_quantity = Decimal(payload["A"]) 139 | elif msg_json["stream"] == "!miniTicker@arr": 140 | for p in payload: 141 | if p["s"] not in self.tickers: 142 | continue 143 | ticker = self.tickers[p["s"]] 144 | ticker.ws_24h_last_updated = time.time() 145 | ticker.high = Decimal(p["h"]) 146 | ticker.low = Decimal(p["l"]) 147 | ticker.base_volume = Decimal(p["v"]) 148 | ticker.quote_volume = Decimal(p["q"]) 149 | elif "result" in msg_json: 150 | pass 151 | else: 152 | log(TAG, msg_json) 153 | 154 | elif msg.type == aiohttp.WSMsgType.ERROR: 155 | error(TAG, f"WebSocket connection was closed unexpectedly {self.ws.exception()}") 156 | break 157 | log(TAG, "Disconnected from Binance's WebSocket market streams. Reconnecting...") 158 | except aiohttp.ClientError as ex: 159 | error(TAG, "Failed to connect to Binance's WebSocket market streams. " 160 | f"Retrying in a second... ({ex})") 161 | 162 | async def fetch_balance(self, asset: str) -> Optional[Decimal]: 163 | data = await self.client.send_signed("GET", "/api/v3/account") 164 | for o in data["balances"]: 165 | if asset == o["asset"]: 166 | return Decimal(o["free"]) 167 | return None 168 | 169 | async def quick_buy_at(self, symbol: str, ask_price: Decimal, quote_percent: float) -> bool: 170 | ticker = self.tickers[symbol] 171 | quote_balance = await self.fetch_balance(ticker.quote_asset) 172 | base_buy_qty = self.round_down_qty(symbol, quote_balance * Decimal(quote_percent) / ask_price) 173 | print(quote_balance * Decimal(quote_percent) / ask_price) 174 | print(base_buy_qty) 175 | return await self.buy(symbol, base_buy_qty, ask_price) 176 | 177 | async def quick_sell_at(self, symbol: str, bid_price: Decimal, base_percent: float) -> bool: 178 | ticker = self.tickers[symbol] 179 | base_balance = await self.fetch_balance(ticker.base_asset) 180 | base_sell_qty = self.round_down_qty(symbol, base_balance * Decimal(base_percent)) 181 | return await self.sell(symbol, base_sell_qty, bid_price) 182 | 183 | def round_down_qty(self, ticker: str, amt: Decimal) -> Optional[Decimal]: 184 | qty_step = self.tickers[ticker].qty_step 185 | return math.floor(amt / qty_step) * qty_step 186 | 187 | def round_down_price(self, ticker: str, price: Decimal) -> Optional[Decimal]: 188 | price_step = self.tickers[ticker].price_step 189 | return math.floor(price / price_step) * price_step 190 | 191 | def commission(self, symbol: str) -> Decimal: 192 | return self.tickers[symbol].trading_fee_taker 193 | 194 | def best_bid_price(self, symbol: str) -> Decimal: 195 | return self.tickers[symbol].best_ask_price 196 | 197 | def best_bid_quantity(self, symbol: str) -> Decimal: 198 | return self.tickers[symbol].best_bid_quantity 199 | 200 | def best_ask_price(self, symbol: str) -> Decimal: 201 | return self.tickers[symbol].best_ask_price 202 | 203 | def best_ask_quantity(self, symbol: str) -> Decimal: 204 | return self.tickers[symbol].best_ask_quantity 205 | 206 | def high(self, symbol: str) -> Decimal: 207 | return self.tickers[symbol].high 208 | 209 | def low(self, symbol: str) -> Decimal: 210 | return self.tickers[symbol].low 211 | 212 | def base_volume(self, symbol: str) -> Decimal: 213 | return self.tickers[symbol].base_volume 214 | 215 | def quote_volume(self, symbol: str) -> Decimal: 216 | return self.tickers[symbol].quote_volume 217 | 218 | async def buy(self, 219 | symbol: str, 220 | quantity: Decimal, 221 | price: Decimal) -> bool: 222 | return await self.order(symbol, True, quantity, price) 223 | 224 | async def sell(self, 225 | symbol: str, 226 | quantity: Decimal, 227 | price: Decimal) -> bool: 228 | return await self.order(symbol, False, quantity, price) 229 | 230 | async def order(self, 231 | symbol: str, 232 | buy: bool, 233 | quantity: Decimal, 234 | price: Decimal) -> bool: 235 | ticker = self.tickers[symbol] 236 | 237 | if (quantity - ticker.qty_min) % ticker.qty_step != Decimal(0): 238 | error(TAG, "Filter 1a failed.") 239 | return False 240 | if not quantity >= ticker.qty_min: 241 | error(TAG, "Filter 1b failed.") 242 | return False 243 | if not quantity <= ticker.qty_max: 244 | error(TAG, "Filter 1c failed.") 245 | return False 246 | 247 | if not ticker.price_filter_disabled: 248 | if (price - ticker.price_min) % ticker.price_step != Decimal(0): 249 | error(TAG, "Filter 2a failed.") 250 | return False 251 | if not price >= ticker.price_min: 252 | error(TAG, "Filter 2b failed.") 253 | return False 254 | if not price <= ticker.price_max: 255 | error(TAG, "Filter 2c failed.") 256 | return False 257 | 258 | if not price * quantity >= ticker.min_notional: 259 | error(TAG, "Filter 3 failed.") 260 | return False 261 | 262 | # TODO PERCENT_PRICE check 263 | 264 | log(TAG, "->" + { 265 | "symbol": symbol, 266 | "side": "BUY" if buy else "SELL", 267 | "type": "LIMIT", 268 | "timeInForce": "GTC", 269 | "quantity": f"{quantity:.{ticker.qty_precision}f}", 270 | "price": f"{price:.{ticker.price_precision}f}" 271 | }.__str__()) 272 | 273 | log(TAG, f"{'Buying' if buy else 'Selling'} {quantity} {ticker.base_asset} at {price} {ticker.quote_asset}...") 274 | start = time.time() 275 | end = None 276 | response = await self.client.send_signed("POST", "/api/v3/order", params={ 277 | "symbol": symbol, 278 | "side": "BUY" if buy else "SELL", 279 | "type": "LIMIT", 280 | "timeInForce": "GTC", 281 | "quantity": f"{quantity:.{ticker.qty_precision}f}", 282 | "price": f"{price:.{ticker.price_precision}f}" 283 | }) 284 | 285 | if response["status"] != "FILLED": 286 | while True: 287 | status = await self.client.send_signed("GET", "/api/v3/order", params={ 288 | "symbol": symbol, 289 | "orderId": response["orderId"] 290 | }) 291 | if status["status"] == "FILLED": 292 | break 293 | await asyncio.sleep(.1) 294 | end = time.time() 295 | 296 | log(TAG, f"{'Bought' if buy else 'Sold'} {quantity} {ticker.base_asset} at {price} {ticker.quote_asset} " 297 | f"({'instant' if end is None else f'{end - start:.2f}'}s)") 298 | return True 299 | 300 | 301 | class Ticker: 302 | def __init__(self, symbol, base_asset, quote_asset): 303 | self.symbol = symbol 304 | self.base_asset = base_asset 305 | self.quote_asset = quote_asset 306 | 307 | self.qty_precision = None 308 | self.qty_min = None 309 | self.qty_max = None 310 | self.qty_step = None 311 | 312 | self.price_precision = None 313 | self.price_filter_disabled = False 314 | self.price_min = None 315 | self.price_max = None 316 | self.price_step = None 317 | 318 | self.min_notional = None 319 | self.percent_price = None 320 | 321 | self.high = None 322 | self.low = None 323 | self.base_volume = None 324 | self.quote_volume = None 325 | 326 | self.trading_fee_maker = None 327 | self.trading_fee_taker = None 328 | 329 | self.best_bid_price = None 330 | self.best_bid_quantity = None 331 | self.best_ask_price = None 332 | self.best_ask_quantity = None 333 | 334 | self.ws_best_last_updated = None 335 | self.ws_24h_last_updated = None 336 | -------------------------------------------------------------------------------- /markets/binance/binance_rest_client.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | import json 4 | import time 5 | from urllib.parse import urlencode 6 | 7 | import aiohttp 8 | 9 | from misc.console import log, debug, error 10 | 11 | TAG = "BNRS" 12 | verbose = True 13 | 14 | class BinanceRestClient: 15 | BASE_URL = "https://api.binance.com" 16 | 17 | # BASE_URL = "https://testnet.binance.vision" 18 | 19 | def __init__(self, key, secret): 20 | self.key = key 21 | self.secret = secret 22 | self.session = aiohttp.ClientSession( 23 | headers={ 24 | "Accept": "application/json;charset=utf-8", 25 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", 26 | "X-MBX-APIKEY": self.key 27 | } 28 | ) 29 | 30 | def hash(self, string): 31 | return hmac.new(self.secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha256).hexdigest() 32 | 33 | def time(self): 34 | return int(time.time() * 1000) 35 | 36 | async def request(self, method, path, params): 37 | url = self.BASE_URL + path + "?" + urlencode(params, True) 38 | debug(TAG, f"&r[&a->&r][{method[:3]}] {path} {params}") 39 | async with { 40 | "GET": self.session.get, 41 | "POST": self.session.post, 42 | "PUT": self.session.put, 43 | "DELETE": self.session.delete 44 | }.get(method, "GET")(url=url, params={}) as res: 45 | if res.status < 200 or res.status > 300: 46 | try: 47 | res_json = await res.json() 48 | raise Exception(f"Error {res.status}: {res_json}") 49 | except ValueError: 50 | raise Exception(f"Error {res.status}") 51 | else: 52 | try: 53 | res_json = await res.json() 54 | debug(TAG, f"&r[&c<-&r][{res.status}] " 55 | f"{res_json if len(str(res_json)) < 128 else f'(hidden - {len(str(res_json))} bytes)'}") 56 | return res_json 57 | except ValueError: 58 | raise Exception("Response formatting error.") 59 | 60 | async def send_unsigned(self, path, params=None): 61 | if params is None: 62 | params = {} 63 | return await self.request("GET", path, params) 64 | 65 | async def send_signed(self, method, path, params=None): 66 | if params is None: 67 | params = {} 68 | params["timestamp"] = self.time() 69 | params["signature"] = self.hash(urlencode(params, True)) 70 | return await self.request(method, path, params) 71 | -------------------------------------------------------------------------------- /markets/binance/binance_web_socket.py: -------------------------------------------------------------------------------- 1 | # import asyncio 2 | # from typing import Optional 3 | # 4 | # import aiohttp 5 | # from aiohttp import ClientWebSocketResponse 6 | # 7 | # from misc.console import * 8 | # from misc.state import State 9 | # 10 | # TAG = "ws" 11 | # 12 | # 13 | # class Feed: 14 | # def __init__(self, streamer, streams: [str]): 15 | # self.streamer = streamer 16 | # self.streams = streams 17 | # self.on_receieved_event = asyncio.Event() 18 | # self.pending_data = None 19 | # 20 | # async def __aenter__(self): 21 | # while self.streamer.ws is None: 22 | # await asyncio.sleep(.1) 23 | # streams_to_subscribe = [o for o in self.streams if o not in self.streamer.subscribedStreams] 24 | # await self.streamer.ws.send_json({ 25 | # "method": "SUBSCRIBE", 26 | # "params": streams_to_subscribe, 27 | # "id": 1 28 | # }) 29 | # print(streams_to_subscribe) 30 | # return self 31 | # 32 | # async def __aexit__(self, type, value, traceback): 33 | # streams_in_use = [] 34 | # for f in self.streamer.feeds: 35 | # if f is self: 36 | # continue 37 | # streams_in_use.extend(f.streams) 38 | # streams_to_unsubscribe = [o for o in self.streams if o not in streams_in_use] 39 | # await self.streamer.ws.send_json({ 40 | # "method": "UNSUBSCRIBE", 41 | # "params": streams_to_unsubscribe, 42 | # "id": 1 43 | # }) 44 | # print(streams_to_unsubscribe) 45 | # 46 | # def __aiter__(self): 47 | # return self 48 | # 49 | # async def __anext__(self): 50 | # await self.on_receieved_event.wait() 51 | # self.on_receieved_event.clear() 52 | # return self.pending_data 53 | # 54 | # 55 | # class Streamer: 56 | # def __init__(self): 57 | # self.on_update = None 58 | # self.ws: Optional[ClientWebSocketResponse] = None 59 | # self.tracking_id = 0 60 | # self.feeds = [] 61 | # self.subscribedStreams = [] 62 | # 63 | # async def run(self): 64 | # while State.ok(): 65 | # try: 66 | # log(TAG, "Connecting to Binance's WebSocket market streams...") 67 | # async with aiohttp.ClientSession() as session: 68 | # async with session.ws_connect('wss://stream.binance.com:9443/stream') as self.ws: 69 | # log(TAG, "Connected to Binance's WebSocket market streams.") 70 | # async for msg in self.ws: 71 | # if msg.type == aiohttp.WSMsgType.TEXT: 72 | # data = msg.json() 73 | # if "stream" in data: 74 | # for f in self.feeds: 75 | # if data["stream"] in f.streams: 76 | # f.pending_data = data["data"] 77 | # f.on_receieved_event.set() 78 | # else: 79 | # print(data) 80 | # 81 | # elif msg.type == aiohttp.WSMsgType.ERROR: 82 | # error(TAG, f"WebSocket connection was closed unexpectedly {self.ws.exception()}") 83 | # break 84 | # log(TAG, "Disconnected from Binance's WebSocket market streams. Reconnecting...") 85 | # except aiohttp.ClientError as ex: 86 | # error(TAG, "Failed to connect to Binance's WebSocket market streams. " 87 | # f"Retrying in a second... ({ex})") 88 | # await asyncio.sleep(1) 89 | # 90 | # def subscribe(self, tickers: [str]) -> Feed: 91 | # f = Feed(self, tickers) 92 | # self.feeds.append(f) 93 | # return f 94 | -------------------------------------------------------------------------------- /markets/import.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import pickle 3 | 4 | import datetime as datetime 5 | 6 | old_con = sqlite3.connect('../historic_klines.db') 7 | old_cur = old_con.cursor() 8 | 9 | new_con = sqlite3.connect('../tmp/cache.db') 10 | new_cur = new_con.cursor() 11 | 12 | new_cur.execute(f"drop table if exists prices") 13 | new_cur.execute(f"create table prices (symbol text, time timestamp, price integer)") 14 | 15 | old_rows = old_cur.execute(f"select * from 'unnamed'") 16 | i = 0 17 | for old_row in old_rows: 18 | symbol, ts_str = old_row[0].split(" - ") 19 | ts = datetime.datetime.strptime(ts_str, "%d %b %Y %H:%M:%S") 20 | price = pickle.loads(bytes(old_row[1])) 21 | new_cur.execute(f"insert into prices values(?, ?, ?)", (symbol, ts, price)) 22 | i += 1 23 | if i % 1000 == 0: 24 | print(f"Imported {i}") 25 | 26 | old_con.commit() 27 | old_con.close() 28 | 29 | new_con.commit() 30 | new_con.close() 31 | -------------------------------------------------------------------------------- /markets/market.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from decimal import Decimal 3 | from typing import Optional 4 | 5 | 6 | class Market: 7 | def __init__(self, 8 | name: str): 9 | __metaclass__ = abc.ABCMeta 10 | self.name = name 11 | 12 | @abc.abstractmethod 13 | async def fetch_balance(self, 14 | asset: str) -> Optional[Decimal]: 15 | pass 16 | 17 | @abc.abstractmethod 18 | def best_bid_price(self, 19 | symbol: str) -> Decimal: 20 | pass 21 | 22 | @abc.abstractmethod 23 | def best_bid_quantity(self, 24 | symbol: str) -> Decimal: 25 | pass 26 | 27 | @abc.abstractmethod 28 | def best_ask_price(self, 29 | symbol: str) -> Decimal: 30 | pass 31 | 32 | @abc.abstractmethod 33 | def best_ask_quantity(self, 34 | symbol: str) -> Decimal: 35 | pass 36 | 37 | @abc.abstractmethod 38 | def base_volume(self, 39 | symbol: str) -> Decimal: 40 | pass 41 | 42 | @abc.abstractmethod 43 | def quote_volume(self, 44 | symbol: str) -> Decimal: 45 | pass 46 | -------------------------------------------------------------------------------- /markets/markets.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional 3 | 4 | from markets.market import Market 5 | 6 | 7 | class Markets: 8 | def __init__(self): 9 | self.markets: [Markets] = [] 10 | 11 | async def init(self): 12 | for market in self.markets: 13 | await market.init() 14 | 15 | def run(self): 16 | for market in self.markets: 17 | asyncio.ensure_future(market.run()) 18 | 19 | def append(self, market: Market): 20 | self.markets.append(market) 21 | 22 | def __getitem__(self, key: str) -> Optional[Market]: 23 | for market in self.markets: 24 | if market.name == key: 25 | return market 26 | return None 27 | 28 | def __len__(self): 29 | return self.markets.__len__() -------------------------------------------------------------------------------- /misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusip/cryptowizard/f4556f58dc0c09be3d97d288ddb3ec0c5ca3ad0e/misc/__init__.py -------------------------------------------------------------------------------- /misc/console.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | COLOURS = { 4 | "0": 30, 5 | "1": 34, 6 | "2": 32, 7 | "3": 36, 8 | "4": 31, 9 | "5": 35, 10 | "6": 33, 11 | "7": 37, 12 | "8": 90, 13 | "9": 94, 14 | "a": 92, 15 | "b": 96, 16 | "c": 91, 17 | "d": 95, 18 | "e": 93, 19 | "f": 97, 20 | "r": 0, 21 | "l": 1, 22 | "o": 3, 23 | "n": 4, 24 | "m": 9, 25 | } 26 | 27 | 28 | def _print(prefix, tag, msg): 29 | output = "&r[{}&r][&8{}&r][&7{}&r]&r {}&r".format(prefix, datetime.now().strftime("%d/%m %H:%M:%S.%f"), tag, msg) 30 | formatted = [] 31 | i = 0 32 | while i < len(output): 33 | if i + 1 < len(output) and output[i] == "&" and ( 34 | i - 1 <= 0 or output[i - 1] != "\\" and output[i + 1] in COLOURS): 35 | formatted.extend(list("\033[{}m".format(COLOURS[output[i + 1]]))) 36 | i += 1 37 | else: 38 | formatted.extend(output[i]) 39 | i += 1 40 | print("".join(formatted)) 41 | 42 | 43 | def log(tag, msg): 44 | _print("&a+", tag, msg) 45 | 46 | 47 | def error(tag, msg): 48 | _print("&c!", tag, "&c" + msg) 49 | 50 | 51 | def debug(tag, msg): 52 | _print("&8#", tag, msg) 53 | -------------------------------------------------------------------------------- /misc/schedule.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class Schedule: 5 | def __init__(self, sec, func): 6 | self.sec = sec 7 | self.func = func 8 | self.last_run = None 9 | 10 | async def run_schedule(self, *args, **kwargs): 11 | cur_time = time.time() 12 | if self.last_run is not None and cur_time < self.last_run + self.sec: 13 | return 14 | self.last_run = cur_time 15 | await self.func(*args, **kwargs) 16 | -------------------------------------------------------------------------------- /programs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusip/cryptowizard/f4556f58dc0c09be3d97d288ddb3ec0c5ca3ad0e/programs/__init__.py -------------------------------------------------------------------------------- /programs/ma.py: -------------------------------------------------------------------------------- 1 | from markets.binance.binance_market import BinanceMarket 2 | from markets.markets import Markets 3 | from programs.program import Program 4 | 5 | 6 | class MovingAverage(Program): 7 | def __init__(self): 8 | super().__init__() 9 | self.buy_price = None 10 | self.sell_price = None 11 | 12 | async def init(self, markets: Markets): 13 | self.every(self.loop, seconds=5) 14 | 15 | b: BinanceMarket = markets["BNCE"] 16 | # self.buy_price = b.best_ask_price("BTCUSDT") 17 | # self.sell_price = b.round_down_price("BTCUSDT", self.buy_price * Decimal(1.02)) 18 | # await b.quick_buy_at("BTCUSDT", self.buy_price, 1) 19 | # await b.quick_sell_at("BTCUSDT", self.sell_price, 1) 20 | 21 | async def loop(self, markets: Markets): 22 | b: BinanceMarket = markets["BNCE"] 23 | print(b.best_ask_price("BTCUSDT")) 24 | -------------------------------------------------------------------------------- /programs/program.py: -------------------------------------------------------------------------------- 1 | from markets.markets import Markets 2 | from misc.schedule import Schedule 3 | 4 | 5 | class Program: 6 | def __init__(self): 7 | self.schedules = [] 8 | pass 9 | 10 | def every(self, func, days=0, hours=0, minutes=0, seconds=0): 11 | self.schedules.append(Schedule( 12 | days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds, 13 | func 14 | )) 15 | 16 | async def run_schedules(self, *args, **kwargs): 17 | for schedule in self.schedules: 18 | await schedule.run_schedule(*args, **kwargs) 19 | 20 | async def init(self, markets: Markets): 21 | pass 22 | 23 | async def loop(self, markets: Markets): 24 | pass 25 | -------------------------------------------------------------------------------- /redfox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusip/cryptowizard/f4556f58dc0c09be3d97d288ddb3ec0c5ca3ad0e/redfox.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp~=3.7.4.post0 2 | ntplib~=0.4.0 3 | python-socketio -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusip/cryptowizard/f4556f58dc0c09be3d97d288ddb3ec0c5ca3ad0e/ui/__init__.py -------------------------------------------------------------------------------- /ui/server.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | import socketio 3 | 4 | from misc.console import * 5 | 6 | TAG = " UI " 7 | 8 | 9 | class Server: 10 | def __init__(self): 11 | self.app = web.Application() 12 | self.runner = web.AppRunner(self.app) 13 | self.io = socketio.AsyncServer() 14 | self.io.attach(self.app) 15 | 16 | @self.io.event 17 | def connect(sid, environ): 18 | log(TAG, f"Client {environ['REMOTE_ADDR']} with SID {sid} is connected.") 19 | 20 | @self.io.event 21 | def disconnect(sid): 22 | log(TAG, f"Client {sid} is disconnected.") 23 | 24 | @self.io.event 25 | def owo(sid, data): 26 | print(data) 27 | pass 28 | 29 | async def start(self): 30 | address = "localhost" 31 | port = 2255 32 | await self.runner.setup() 33 | await web.TCPSite(self.runner, address, port).start() 34 | log(TAG, f"Started interfacing server on {address}:{port}") 35 | 36 | async def stop(self): 37 | await self.runner.cleanup() 38 | log(TAG, f"Stopped interfacing server.") 39 | --------------------------------------------------------------------------------