-
Notifications
You must be signed in to change notification settings - Fork 134
Implement BitexResponse object, including formatted Exchange data #129
Comments
We could place a class inside the base interface class, something like: class RESTInterface(..):
class FormattedResponse(requests.Response):
@property
def formatted(self, request_response):
raise NotImplementedError and then each exchange interface re-implements it according to the kind of ticker response it receives: class ThisExchange(RESTInterface):
class ThisExchangeFormattedResponse(RESTInterface.FormattedResponse):
@property
def formatted(self, request_response):
j = request_response.json()
return (j["ask"], j["bid"], None, ..., j["time"])
def ticker(self, pair, ...):
req = self.request(...)
return ThisExchangeFormattedResponse(req) warning: code not tested, here for discussion only. (Explicitly calling the I'm unsure whether the inheritance from Anyway, if you think it can be a good idea, I'll dig into it and propose an implementation. |
I started a spreadsheet for comparing the different responses. https://docs.google.com/spreadsheets/d/1jMkXpAGmKcGR0Dm9fQNYv85lunIuVjDUGE2qcB_J2hE/edit?usp=sharing I've done most of the ticker responses so far and it seems like a good set of 'common' results would be:
All of the exchanges I've tested implement all of the above, with the exception of a few which don't implement timestamp (don't really need timestamp anyway, can just use time of the request - though might be worth specifying that the time is 'inaccurate' then). |
Great job, @mattisback. I've contributed to your spreadsheet by adding a ticker on binance. @nlsdfnbch any idea on how to proceed? do you approve the idea of the abstract wrapper class around requests, inside the |
@bebosudo, I don't see the benefit of defining the class inside the interface class. But putting them in the same file is okay. I'll see to it that I'll define some tests for it today. @mattisback i had a look at your spreadsheet, and can add that vaultoro only trades bitcoin/gold, which is why you probably weren't able to get a ticker out of it. |
Had a crack at implementing some of this. Let me know what you think of this approach. Added a folder called
import abc
import requests
class FormattedResponse(metaclass=abc.ABCMeta):
"""Formatted Response base class"""
def __init__(self, method, params, response):
assert (isinstance(response, requests.Response)) # can't remember if this is good practice in python
self.method = method # need to know the type of method so we know how to format it, just a string
self.method_params = params # do we need this?
self.raw = response # could be called response instead of raw
@property
def formatted(self):
func_name = '_format_' + self.method
func = getattr(self, func_name, None)
if func is None:
raise NotImplementedError
return func(self.raw)
@abc.abstractmethod
def _format_ticker(self, response):
raise NotImplementedError
# ... define other abstract methods here An example exchanges implementation (for one function at least) from bitex.interface.formatters import FormattedResponse
from datetime import datetime
class BitfinexFormattedResponse(FormattedResponse):
def _format_ticker(self, response):
response_data = response.json()
return {
'timestamp': datetime.fromtimestamp(float(response_data['timestamp'])),
'bid': float(response_data['bid']),
'ask': float(response_data['ask']),
'low': float(response_data['low']),
'high': float(response_data['high']),
'volume': float(response_data['volume']),
'last': float(response_data['last_price'])
}
def _format_order_book(self, response):
pass
# etc... Then the magic... To make it format a response use the following decorator, defined in def format_response(func):
"""Formats a requests.Response object, wrapping it in a FormattedResponse object
:param func the function being called (eg. ticker or order_book)
"""
@wraps(func)
def wrapped(self, *args, **kwargs):
"""Wrap function."""
try:
class_name = self.__class__.__name__
formatter_name = class_name + "FormattedResponse"
formatter_class = getattr(sys.modules['bitex.interface.formatters'], formatter_name)
except AttributeError:
raise NotImplementedError('Formatter \'' + formatter_name + '\' does not exist')
# run the function
response = func(self, *args, **kwargs)
return formatter_class(func.__name__, args, response)
return wrapped Then all you need to do is add the decorator in the exchange interface files. eg. ...
@format_response # <- wraps output in FormattedResponse (specifically BitfinexFormattedResponse)
def ticker(self, pair, *args, **kwargs):
"""
Return the ticker for the given pair.
"""
payload = {'symbol': pair}
payload.update(kwargs)
return self.request('GET', 'v1/ticker/24hr', params=payload)
... Lastly, it can then be used like this: formatted_response = bitex.Bitfinex().ticker(bitex.BTCUSD)
print(formatted_response.formatted)
# {'timestamp': datetime.datetime(2017, 12, 18, 20, 35, 44, 526019),
# 'bid': 18629.0,
# 'ask': 18630.0,
# 'low': 18010.0,
# 'volume': 63690.87027664,
# 'last': 18630.0,
# 'high': 19891.0}
# note we can still see all of the original response data with formatted_response.raw
print(formatted_response.raw.json())
# {'bid': '18629.0',
# 'timestamp': '1513589744.5260189',
# 'ask': '18630.0',
# 'mid': '18629.5',
# 'low': '18010.0',
# 'volume': '63690.87027664',
# 'last_price': '18630.0',
# 'high': '19891.0'} |
I like the principle implementation - however, see the unit test I uploaded to see what the changes required specifically are (it's mainly naming and access)! |
Yep, I'll pull your unit test and make some modifications to get it to pass. |
Great work @mattisback, just a couple of review notes on the excerpt of formatter you showed here (had no time yet to review the rest):
On the design, I don't see the point of placing the formatters in a separate module, and having another file for each formatter. using
It sounds a bit like a "hack" and more error-prone to me, but maybe is because I'm not used to metaprogramming. I'll try to implement a different version tomorrow, based on a Merry Christmas! 😄 |
Just having a look at your unit test, a couple of things:
Have a look at the implementation here: https://github.com/mattisback/bitex/tree/FormattedResponse_to_APIResponse |
@bebosudo Thanks for the feedback!
The separate files separate out distinct concepts and and help avoid big, messy files which would be the case if we put the formatters in the same files as the interface implementations. Especially if it was a sub-class. |
Thanks @mattisback, I used your approach and I separated the formatters and the interfaces modules. It took me a while, but I think I figured out a clean way to solve the problem, see PR #140. I created a
In the same file I also created a
Then we can easily create a formatter for each exchange (e.g.,
I've also used some black magic and created a decorator that can be applied to the ticker methods, to ease and make the process appear cleaner. It's stored in
And eventually, it all reduces to apply the decorator and pass it the customized class (on
An usage example is:
What do you think? Any suggestion? |
Hi @bebosudo @format_with(BitfinexFormattedResponse) as opposed to my 'magic' approach to choosing a formatter. Makes it more verbose and obvious what the code does, although it will introduce some code duplication (ie you'll need to specify BitfinexFormattedResponse) for every method that you want to format. What I don't understand with your method is how are multiple functions handled? For example, your code lets us format the So far in my branch I have working: all ticker formatters, support for multiple functions per exchange, integration with @nlsdfnbch 's desired formats (his unit test passes), fully working unit tests. Let me know if you want me to make any changes to my branch or to submit a pull request and I will. For now I don't have the time to redo all the work I've done on another branch so I'll continue working on mine for now but if the other approach is preferable then I'll happily come back to contributing to this repo at some point. |
Hey lads! |
It's simply because so far I'm using only the Let's wait for a feedback from @nlsdfnbch, before going on with this code battle 😉 Merry Christmas to all of you! |
@bebosudo sounds good, lets listen for @nlsdfnbch 's input :) |
Right! So - I finally had some time to look over both of your excellent contributions - thanks again for investing so much time and effort into this little project :) 👍 As for the implementation method, I'm strongly leaning towards @bebosudo 's implementation. The issue of implementing the various formatters for each endpoint, could be handled as follows: implementing a formatter in each class AbstractFormattedResponse(requests.Response):
def __init__(self, method, response_obj):
self.response = response_obj
self.method = method
@property
def formatted(self):
"""Return the formatted json data."""
getattr(self, self.method)
def __getattr__(self, attr):
"""Use methods of the encapsulated object, when not available in the wrapper."""
try:
return getattr(self.response, attr)
except AttributeError:
return getattr(self, attr)
def ticker(*args, **kwargs):
raise NotImplementedError
def order_book(*args, **kwargs):
raise NotImplementedError
def trades(*args, **kwargs):
raise NotImplementedError
def bid(*args, **kwargs):
raise NotImplementedError
def ask(*args, **kwargs):
raise NotImplementedError
... # etc. And then all we need to update would be the wrapper, like so: def format_with(formatter):
"""Pass a class to be used as a wrapper in the innermost function."""
def real_decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
return formatter(function.__name__, function(*args, **kwargs))
return wrapper
return real_decorator What do you guys think ? |
Cool. Fair enough if you prefer the Then there's the aliasing of the underlying response up to the def __getattr__(self, attr):
"""Use methods of the encapsulated object, when not available in the wrapper."""
try:
return getattr(self.response, attr)
except AttributeError:
return getattr(self, attr) vs self.__dict__ = response.__dict__ Yours seems like a fine way to do it. I kind of think a prefix on the function names could be a good idea. At the moment I use The other thing missing is passing through the arguments of the call in the interface through to the formatter. Take for example Poloniex. When we call Have a look at my current implementation of def _format_ticker(self, response):
response_data = response.json()
d = response_data[self.called_method_params[0]]
return {
'timestamp': response.receive_time,
'bid': float(d['highestBid']),
'ask': float(d['lowestAsk']),
'low': float(d['low24hr']),
'high': float(d['high24hr']),
'volume': float(d['baseVolume']),
'last': float(d['last'])
}
# {'BTC_CLAM': {'id': 20, 'highestBid': '0.00058379', 'percentChange': '0.32601675', 'low24hr': '0.00042656',
# 'baseVolume': '77.25725539', 'high24hr': '0.00058657', 'quoteVolume': '152023.08084615',
# 'lowestAsk': '0.00058719', 'isFrozen': '0', 'last': '0.00058720'}, '
# BTC_XPM': {'id': 116, 'highestBid': '0.00002168', 'percentChange': '0.02300469', 'low24hr': '0.00002107',
# 'baseVolume': '8.41716666', 'high24hr': '0.00002299', 'quoteVolume': '383520.44957097',
# 'lowestAsk': '0.00002178', 'isFrozen': '0', 'last': '0.00002179'},
# ... (for each coin!)
# } |
Concerning the args & kwargs, I think it'd be easiest to simply pass them along into the constructor and save them; that way they can be accessed by the formatting method if necessary? def __init__(self, method, response, *args, **kwargs):
self.method = method
self.response = response
self.method_args = args
self.method_kwargs = kwargs And regarding the possibility of the method name or attribute being implemented into |
@mattisback have you already implemented something following Nils's advice? Otherwise I'm going to work on it now |
I tried to implement a new version on multiple endpoints (a6832bf), and I created two working examples of the ticker method on bitfinex and poloniex (I'm just a bit unsure about whether to use the quoteVolume or the baseVolume field on the Poloniex ticker endpoint: a6832bf#diff-2f3b32f9747710655dbbd0c60f1a223eR27 ). Edit: if you think this implementation is reasonable, I'll implement the formatters for all the other exchanges. |
Since I'm working on a project, and I needed a standard way to access the ticker of many exchanges, I've implemented a dozen more exchange formatters in 0f389a6, available inside PR #140. All exchanges now return a standard tuple with these values: Regarding the exchanges that didn't return the timestamp in the request, I returned a utc datetime of the local machine. Feel free to provide suggestions! Edit: Kraken is down for maintenance, when it will come back I'll add its formatter. |
Congrats, you've encountered a perfect example for why I initially opted for dropping the formatter idea :'D As for the timestamp, it sort of depends on how exact we want the data to be. Substituting a non-present timestamp from the server with a unix timestamp (as an example) can be anything from ping time and more off. Personally, I'd say that's OK - if you want real-time data, REST isn't the way to go to begin with so this shouldn't be much of an issue. Also, I've seen you're returning some data as |
In fact I'm already using
I liked the idea of the timestamp being inside the tuple returned from the formatter, so that everything needed is contained inside that tuple, but we can move the timestamp outside, on the I'd like to discuss also what we want to return: a plain unix timestamp (an int, a string, a float?) or a datetime aware object? I'm inclined for the latter. (Now I remember that in my implementation only a couple of timestamps in exchange formatters are datetime aware, most of them are naive: I need to fix it in case this is approved).
As I discussed with @mattisback in issue #138, the problem is that many exchange return floats inside their json, and interpreting them as Looking forward to close this issue and see it in an officially released package in pip ;-) |
No worries - I, too, would say a timestamp in the return value is necessary; it just needs to be obvious which is which.
It should be a
values in a json string are strings before they are converted -therefore, the most conservative way to convert them is to a Essentially, what happens under the hood is this: # Take a sample json string
json_str = '{"price":1.909420}'
# when calling json.loads(), the json module parses the string for values to convert - when reaching our float it does the following:
parsed_value = selected_parser_parser(json_str[9:17]) While it's a good thing you want to spread the word about the possibility of overflow errors when converting json strings to float, I don't believe it to be our job to do so. Furthermore, complex calculations by professionals would probably be done using |
Ok for the
at least in the
Ok, then I suggest we at least use
Do you agree? |
We can leave it as unix timestamp - but that's not very readable. Hence I'd strftime it - besides this format is well known and supported widely by many tools for storing and reading time series and related data (which can parse it easily from a string). |
ok, never tried with Regarding the timestamps, then you suggest separating them from the response class, and add them as new properties, right?
and
|
@property
def received_at(self):
return self.recv_at_dt.strftime('%-Y%m-%d %H:%M:%S.%f %z') Or similar. What do you think? |
To me is fine creating the two parameters inside
Is there a reason you want to use that particular format instead of the ISO one? |
It was my impression that is that not ISO? If not, I leave the formatting to you then :D edit: I just saw I used a different format string in my previous comment - either way, I'm absolutely ok with using the ISO format - I just didn't pay attention to the format I used. |
Unfortunately I'd just like to avoid the "yet another standard" approach: https://xkcd.com/927/ ;-) I'll implement the Edit: I'm implementing it, but I don't see the need to create two custom properties. Placing them right inside the
|
Formatter implementation using delegation #129
As discussed in #107 , the addition of a
ResponseObject
is favorable.However, since
BitEx
is primarily an API wrapper, the customResponse
object should not get in the way of the user - it needs to feel, look and handle like arequests.Response
object.One way to achieve this, is to store formatted data and the original
requests.Response
object in a new class, which additionally exposes all public attributes of therequests.Response
as properties using the@property
decorator.Update:
The class has been implemented with #140, thanks to @bebosudo ! Also thanks to @mattisback for the valuable feedback.
Currently,the following methods have a formatter method implemented.
The text was updated successfully, but these errors were encountered: