Closed ghost closed 4 years ago
Hello there!
I know the current "market standard lib" (that shall not be named) approaches the market info load as stated in Solution 1 by always checking the cache of the markets for the exchange and loads the data lazily by calling the exchange's market info endpoint when the cache is empty. I think this is a valid approach that could be enhaced by providing the lib user a mechanism to "warm up" that cache (i.e. loading the data from a local file at bootstap instead of doing the API call at runtime).
The enum sounds like a reasonable option for amounts (although not sure about the default to Round) but it should be separate from the rounding of prices. The desired behavior of the price rounding will very likely vary depending on the side of the book where the order is being placed.
Updated the issue with the precision_rule_qty and precision_rule_price.
I thought about this for a while. Assuming that the goal of the library is to be perpetually accurate about a pair's precision for a long-running trading application, the pair information for an exchange (which includes its precision) must be kept up-to-date all of the time, using a technique such as a central cache (lookup table). That much is for certain and we all seem to agree on that premise.
I believe Solution 2 would be the most efficient and type-safe approach, allowing a pair to be looked up ahead of order activity being submitted. What would be returned to the user is a handle to the pair named e.g. TradePair
or something, which has immutable references to the precision information located in a central cache. This TradePair
would be submitted with orders. The library would perform a read-only lookup of the precision information located in the central cache, any time it needed that information (i.e. validating an order prior to submitting it). The library would offer a mechanism to refresh exchange information, which would include precision information for all pairs on an exchange. When this mechanism discovers an update to a pair's precision info, it would take a read-write lock on the info, update it, and release it. All outstanding handles to the pair info would reference the newly updated information.
The above to me sounds like the best approach for performance, type-safety, and perpetual accuracy of exchange information in general, including a pair's precision information. It avoids the hash table lookup cost on every order submission, which when amortized over time is a lot more expensive than a RwLock where there are far more reads than writes.
I can write an example of this data model and share it here for visual clarity.
Challenging approach. I would suggest to start without the update mechanism.
The update mechanism is very straightforward. The whole model is straightforward in fact.
Here's a basic implementation of the idea. https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=803c29d1f47f513d9e493fd39ac5a09a An improvement to this would be in the refresh()
logic, to first do a read()
lock on the pair to inspect if anything has changed about it...if not, continue to next pair...if so, upgrade the read lock to a write lock and then do the update. This improvement would avoid any stalls on pairs in use if there were not any updates to their info from the exchange, which is the majority case.
@jnicholls when will this be loaded? At bootstrap or at first request? Keep in mind that if it's done at bootstrap, this could mean doing so for 10, 20, 50, 100 exchanges, depending on the implementor's include pattern and the extension of this library. Otherwise this would need some form of lazy loading (as the leading library does).
The challenge is also that some exchanges have dynamic precision based on a rule.
Bitfinex's pricing precision model is based on 5 numerical places, meaning that if BTC's price is below 10k, rounding is to 1 decimal place, but the moment the price is 10k but below 100k, the rounding is to the dollar (integer), but when it crosses 100k the increments are done in 10 dollars now.
I think that the following are sensible requirements/features:
One extra note, though. Using a string name for the pair has given us lots of nighmares and it's hard to use for more advanced stuff. A better option is to have the base & quote as separate fields, as it allows for better management & pairing with individual coins (i.e. matching balances to markets) or even mixing markets (multi-market arbitrage).
One extra note, though. Using a string name for the pair has given us lots of nighmares and it's hard to use for more advanced stuff. A better option is to have the base & quote as separate fields, as it allows for better management & pairing with individual coins (i.e. matching balances to markets) or even mixing markets (multi-market arbitrage).
How do you deal with pairs that are more complex like futures or products like btcbull?
That's a fair question, but I've never worked with other than spot markets, so I don't know how futures or btcbull work. I'm not saying it must be something complex, but having like:
struct Pair {
base: String,
quote: String,
base_increment: f64,
quote_increment: f64
}
Makes things much easier than having a single field for the pair name.
A particular example comes to mind: pair/symbol format for API calls. Doing calls to the ETH/BTC market is different per exchange:
But they are all the same market, so as a library user I would like to say "I want to trade on ETH/BTC both in Binance and Bitfinex" but I don't care how the underlying API call is formed.
Would it make sense to separate models by product type (spot, funding, futures, etc)?
I agree with using separate base and quote properties for describing a market. The example was simply to showcase the mechanics of shared handles to centrally manage the exchange market information, including the precision details.
Got a first version on master now.
The first approach rounds size using BankersRounding
Price will be rounded up when selling and down when buying.
https://docs.rs/rust_decimal/0.11.1/rust_decimal/enum.RoundingStrategy.html
This is a great start! I wasn't aware of that crate, which looks quite nice. Not sure if I've mentioned this before to you, but there's a particularity on Bitfinex. Right now it's not a biggie, but if an asset goes over 100k, the rouding strategy is no longer based in decimal places, but it will be in increments of 10.
I think that should be ok Decimal library should be able to handle it.
@steffenix are you sure? I agree the decimal library works pretty well when you know in advance the amount of decimal places to use, and this will rarely vary on an individual market. The problem with Bitfinex is whenever the orders of magnitude change, the amount of decimal places is reduced. I.e. on [1000, 10000)
the decimal places is 1, while [10000,100000)
is 0 and then, when reaching [100000,1000000)
due to their significant figures strategy, now the "price steps" will be 10, instead of 1 (or a -1 decimal places)
Lets say that the current top bid is 9999.9 and top ask is 10000, meaning a mid-price of 9999.95. If I want to place my sell order at 0.01% from the mid-price, I would have to round to 0 decimal places, while the buy order would need a rounding of 1 decimal place. I'm struggling to see how the rust_decimal
crate could be used in this scenario
Understood Bittfinex isn't part of the scope now. That can be tough when we have to add it.
Is that logic exposed as part of an API endpoint?
If it's always the same logic with the same precision (5) we would be able to have a custom conversion mechanism for bitfinex.
Issue
The decimal precision can't be too high otherwise the exchange API will reject the request.
eg: When using binance API, if I want to buy 123.45 USDT value of BTC at 9,500.01, the request will be:
But binance API only accepts 8 decimal on the BTC asset for this trading pair., the request should be converted to:
Proposed solutions
The exchanges provides a list of the pairs with the precision expected. coinbase binance ftx as you can see some exchange are using quote and price precision others are using a price and size increment.
Solution 1
When creating an order, the exchange SDK will fetch the exchange information (from a cache) in order to get the required precision/increment information and apply the rounding/truncating. This has some performances implication when you have to look for a specific pair every times you want to create a new order especially when you are using an exchange with a lots of pairs like binance (881 now).
Solution 2
Create a new type for
pair
that replaceString
that will keep the price and qty increments for the pair.An exchanges trait will be added in order to create the pair type, the implementation will look for the exchanges information to get the increments values.
When creating an order the price and qty increments will be fetched from the pair type.
Usage
I suggest adding a new parameter to the OrderRequest types named
precision_rule_qty
andprecision_rule_price
with a default value set. For a sell price precision will be defaulted to Round, for buy it will be Truncate, qty will default to Truncate.The precision rule could also be set on the exchange instead of specified on every order.
Notes
Binance
After trying to submit some orders, they are rejected because they should also fit the step size filter, step size seems to be a better indicator of the precision.
Nash
Exchanges
This exchanges supports getting the market pairs with the price/qty increment