def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bitso-market-maker-keeper') parser.add_argument( "--bitso-api-server", type=str, default="https://api.bitso.com", help= "Address of the bitso API server (default: 'https://api.bitso.com')" ) parser.add_argument("--bitso-api-key", type=str, required=True, help="API key for the Bitso API") parser.add_argument( "--bitso-secret-key", type=str, required=True, help="RSA Private Key for signing requests to the BitsoX API") parser.add_argument( "--bitso-timeout", type=float, default=9.5, help= "Timeout for accessing the Bitso API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.bitso_api = BitsoApi(api_server=self.arguments.bitso_api_server, api_key=self.arguments.bitso_api_key, secret_key=self.arguments.bitso_secret_key, timeout=self.arguments.bitso_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.bitso_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.bitso_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.bitso_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start()
class BitsoMarketMakerKeeper: """Keeper acting as a market maker on Bitso.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bitso-market-maker-keeper') parser.add_argument( "--bitso-api-server", type=str, default="https://api.bitso.com", help= "Address of the bitso API server (default: 'https://api.bitso.com')" ) parser.add_argument("--bitso-api-key", type=str, required=True, help="API key for the Bitso API") parser.add_argument( "--bitso-secret-key", type=str, required=True, help="RSA Private Key for signing requests to the BitsoX API") parser.add_argument( "--bitso-timeout", type=float, default=9.5, help= "Timeout for accessing the Bitso API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.history = History() self.bitso_api = BitsoApi(api_server=self.arguments.bitso_api_server, api_key=self.arguments.bitso_api_key, secret_key=self.arguments.bitso_secret_key, timeout=self.arguments.bitso_timeout) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.bitso_api.get_orders(self.pair())) self.order_book_manager.get_balances_with( lambda: self.bitso_api.get_balances()) self.order_book_manager.cancel_orders_with( lambda order: self.bitso_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders() def pair(self): return self.arguments.pair.lower() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].lower() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].lower() def our_available_balance(self, our_balances: list, token: str) -> Wad: balance = list(filter(lambda x: x['currency'] == token, our_balances))[0]['total'] return Wad.from_number(balance) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders self.place_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance( order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), target_price=target_price)[0]) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount # Convert wad to float as Bitso limits amount decimal places to 8, and price to 2 float_price = round(Wad.__float__(new_order_to_be_placed.price), 2) float_amount = round(Wad.__float__(amount), 8) side = "sell" if new_order_to_be_placed.is_sell == True else "buy" order_id = self.bitso_api.place_order(book=self.pair(), side=side, price=float_price, amount=float_amount) timestamp = datetime.now(tz=timezone.utc).isoformat() return Order(str(order_id), timestamp, self.pair(), new_order_to_be_placed.is_sell, new_order_to_be_placed.price, amount) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
class TestBitso: def setup_method(self): self.bitso = BitsoApi(api_server="localhost", api_key="00000000-0000-0000-0000-000000000000", secret_key="bitsosecretkey", timeout=15.5) def test_convert_iso_to_unix(self): iso_timestamp = datetime.now(tz=timezone.utc).isoformat() assert (isinstance(iso_timestamp, str)) unix_timestamp = iso8601_to_unix(iso_timestamp) assert (isinstance(unix_timestamp, int)) def test_get_markets(self, mocker): mocker.patch("requests.request", side_effect=BitsoMockServer.handle_request) response = self.bitso.get_markets() assert (len(response) > 0) assert (any(x["book"] == "eth_mxn" for x in response)) def test_order(self): price = Wad.from_number(4.8765) amount = Wad.from_number(0.222) remaining_amount = Wad.from_number(0.153) order = Order(order_id="153153", timestamp=iso8601_to_unix( datetime.now(tz=timezone.utc).isoformat()), pair="eth_mxn", is_sell=False, price=price, amount=amount) assert (order.price == order.sell_to_buy_price) assert (order.price == order.buy_to_sell_price) def test_get_balances(self, mocker): mocker.patch("requests.request", side_effect=BitsoMockServer.handle_request) response = self.bitso.get_balances() assert (len(response) > 0) for balance in response: if "eth" in balance["currency"]: assert (float(balance["total"]) > 0) @staticmethod def check_orders(orders): by_oid = {} duplicate_count = 0 duplicate_first_found = -1 current_time = iso8601_to_unix( datetime.now(tz=timezone.utc).isoformat()) for index, order in enumerate(orders): assert (isinstance(order, Order)) assert (order.order_id is not None) assert (order.timestamp < current_time) # Check for duplicates if order.order_id in by_oid: duplicate_count += 1 if duplicate_first_found < 0: duplicate_first_found = index else: by_oid[order.order_id] = order if duplicate_count > 0: print(f"{duplicate_count} duplicate orders were found, " f"starting at index {duplicate_first_found}") else: print("no duplicates were found") assert (duplicate_count == 0) def test_get_orders(self, mocker): instrument_id = "eth_mxn" mocker.patch("requests.request", side_effect=BitsoMockServer.handle_request) response = self.bitso.get_orders(instrument_id) assert (len(response) > 0) for order in response: assert (isinstance(order.is_sell, bool)) assert (Wad(order.price) > Wad(0)) TestBitso.check_orders(response) def test_order_placement_and_cancellation(self, mocker): instrument_id = "eth_mxn" side = "sell" mocker.patch("requests.request", side_effect=BitsoMockServer.handle_request) order_id = self.bitso.place_order(instrument_id, side, 4400.000, .01) assert (isinstance(order_id, str)) assert (order_id is not None) cancel_result = self.bitso.cancel_order(order_id) assert (cancel_result is True) @staticmethod def check_trades(trades): by_tradeid = {} duplicate_count = 0 duplicate_first_found = -1 missorted_found = False last_timestamp = 0 for index, trade in enumerate(trades): assert (isinstance(trade, Trade)) if trade.trade_id in by_tradeid: print(f"found duplicate trade {trade.trade_id}") duplicate_count += 1 if duplicate_first_found < 0: duplicate_first_found = index else: by_tradeid[trade.trade_id] = trade if not missorted_found and last_timestamp > 0: if trade.timestamp > last_timestamp: print(f"missorted trade found at index {index}") missorted_found = True last_timestamp = trade.timestamp if duplicate_count > 0: print(f"{duplicate_count} duplicate trades were found, " f"starting at index {duplicate_first_found}") else: print("no duplicates were found") assert (duplicate_count == 0) assert (missorted_found is False) def test_get_trades(self, mocker): instrument_id = "eth_mxn" mocker.patch("requests.request", side_effect=BitsoMockServer.handle_request) response = self.bitso.get_trades(instrument_id) assert (len(response) > 0) TestBitso.check_trades(response)
def setup_method(self): self.bitso = BitsoApi(api_server="localhost", api_key="00000000-0000-0000-0000-000000000000", secret_key="bitsosecretkey", timeout=15.5)
# (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys from pyexchange.bitso import BitsoApi from pymaker.numeric import Wad bitso = BitsoApi('https://api.bitso.com', sys.argv[1], sys.argv[2], 9.5) print("Starting BitsoApi with the following parameters: ", sys.argv) # GET "/v3/balance/" # print(bitso.get_balances()) #print(bitso.get_markets()) # print(bitso.get_pair('ETH/USDC')) # GET "/api/v1/orders" # print(bitso.get_orders('eth_mxn')) # POST /api/v1/orders # print(bitso.place_order('eth_mxn', 'buy', 5200.000, .01)) # DELETE /api/v1/orders/{order_id}