Trading fees can be enourmous if you're trading any serious volume with market buy/sell orders. A strategy can be rendered unprofitable as a result of high trading fees, especially if you're entering and exiting a market frequently or using leverage (fees being proportional to trade volume). Most exchanges charge fees on market orders (taker) but have no fees, or sometimes even rebates on filled limit orders (maker). Here we code a short script to aggressively place limit orders at the top of the orderbook, chasing the price until we get filled and ensuring we are always the maker and never pay taker fees.
Some suites and terminals (paid/free) will have features that will do this for you. But it's even better to code it ourselves, in order to protect your exchange API keys, and tweak the design to our liking. We can also use the limit chase as a foundation for cheap Dollar Cost Averaging or TWAPing into an asset (Time Weighted Average Price).
One thing to note is that this approach is not always better than just doing a market buy, the main reason being that the price may instantly jump against you (pros/cons discussed at bottom of page).
Let's use the ccxt Python module (install:$ pip install ccxt
) to connect to a cryptocurrency exchange (im using FTX) and provide us with all the basic functions we need. Obviously you can switch this with the API of your choosing. Here we're going to focus on the buy-side limit chase, and you can replicate for sells afterwards.
The script will need to get the current bid and ask price, and then place our limit order at the top of the orderbook:
import ccxt, time, random
ex = ccxt.ftx({
'apiKey': '',
'secret': '',
'headers': {
'ftx-subaccount' : ''
}})
ticker = "BTC-PERP"
amount = 0.01
ticker_data = ex.fetch_ticker(ticker)
new_bid, new_ask = ticker_data['bid'], ticker_data['ask']
order = ex.create_limit_buy_order(ticker, amount, new_bid, {"postOnly": True})
Note here we are using a `postOnly` flag, ensuring the order will cancel if it were to cross the book (taker) upon the order being recieved by the exchange. For example, if we were to send a buy limit at $60k USD but the price moved down to $59,995 by the time the order reaches the exchange, rather than the order "crossing the book" and acting as a market buy (taker), the order is cancelled.
Now let's refresh our order and see if we've been filled:
def refresh_order(order):
updated_orders = ex.fetch_orders()
for updated_order in updated_orders:
if updated_order["id"] == order["id"]:
return updated_order
print("Failed to find order {}".format(order["id"]))
return None
order = refresh_order(order)
print(order)
amount_bought = float(order["info"]["filledSize"])
Chuck all this into a loop, continually cancelling and replacing buys at the top of the orderbook. We track the amount_traded, and loop until we have bought enough:
ticker = "BTC-PERP"
# Get minimum trade size for this ticker
market = [mk for mk in ex.fetch_markets() if mk["symbol"] == ticker][0]
min_size = market["limits"]["amount"]["min"]
amount = 0.01
amount_traded = 0
while amount - amount_traded > min_size:
ticker_data = ex.fetch_ticker(ticker)
new_bid, new_ask = ticker_data['bid'], ticker_data['ask']
# place order
order = ex.create_limit_buy_order(ticker, amount, new_bid, {"postOnly": True})
time.sleep(1)
# cancel order
try:
ex.cancel_order(order["id"])
except Exception as e:
print(e)
# refresh order details and track how much we got filled
order = refresh_order(order)
amount_traded += float(order["info"]["filledSize"])
See that we loop until we have less than `min_size` left to buy, this will avoid annoying errors saying you can't buy tiny amounts of BTC when you didn't completely buy everything you wanted; the remaining size is below the threshold amount.
This is good and works, but we don't want to be closing our order every time we loop. We should only close our order if the price has changed, ensuring we're maximising the time we have an order in the book.
Now to track whether the price has changed, we're going to begin with use some starting values for prices bid=0 and ask=10^10 (v big number), as well as an empty order=None. This will set prepare us for any conditional if statements we put in our loop; eg. `if order is not None:`.
Upon the first run, the new_bid price will be different to bid. We then place our first buy order, updating the values for bid, ask, and order. Then on subsequent runs we do not cancel and replace the order unless the price has changed.
Complete code for a limit chase buy:
import ccxt, time, random ftx_keys = { 'apiKey': '', 'secret': '', 'headers': { 'ftx-subaccount' : '' } } ex = ccxt.ftx(ftx_keys) def refresh_order(order): updated_orders = ex.fetch_orders() for updated_order in updated_orders: if updated_order["id"] == order["id"]: return updated_order print("Failed to find order {}".format(order["id"])) return None ticker = "BTC-PERP" market = [mk for mk in ex.fetch_markets() if mk["symbol"] == ticker][0] min_size = market["limits"]["amount"]["min"] amount = 0.01 amount_traded = 0 # Initialise empty order and prices, preparing for loop order = None bid, ask = 0, 1e10 while amount - amount_traded > min_size: move = False ticker_data = ex.fetch_ticker(ticker) new_bid, new_ask = ticker_data['bid'], ticker_data['ask'] if bid != new_bid: bid = new_bid # If an order exists then cancel it if order is not None: # cancel order try: ex.cancel_order(order["id"]) except Exception as e: print(e) # refresh order details and track how much we got filled order = refresh_order(order) amount_traded += float(order["info"]["filledSize"]) # Exit now if we're done! if amount - amount_traded < min_size: break # place order order = ex.create_limit_buy_order(ticker, amount, new_bid, {"postOnly": True}) print("Buy {} {} at {}".format(amount, ticker, new_bid)) time.sleep(random.random()) # Even if the price has not moved, check how much we have filled. if order is not None: order = refresh_order(order) amount_traded += float(order["info"]["filledSize"]) time.sleep(0.1) print("Finished buying {} of {}".format(amount, ticker))
Some points of interest:
time.sleep(random.random())
. You can vary position size, create some set of N buy limits down to a certain price, or whatever else you can think of.
This implementation isn't flawless and certainly not fast or competitive on tiny timescales, but this approach works and could save you thousands (tens of thousands if you're a trader) in fees. If you learnt something or if I save you money, please support me:
BTC: bc1q64qjl9fs9fqutx5krmwsh0j78pfmzj4rxevapw
XMR: 8BctFibTfaefTA71ZAJt27b6k58Egc4D2LQbGn18AtKT1sTo5jDu8nXeyA6bRV2NK3LRdRidrffDrZsYJnDCdRhbCnG97FS