-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ccxt Input Abstract Plugin #80
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is cool. As I was reading the code I got a new design insight: I realized that pagination loop logic should probably be captured using custom iterators. It's a more natural fit and it's probably worth spending a little time exploring this approach. Custom iterators are already used extensively in the rest of the code. For a simple example of custom iterator see https://github.com/eprbell/rp2/blob/main/src/rp2/abstract_accounting_method.py.
The idea is that there is a container class (in the example it's AcquiredLotCandidates
) we want to iterate on, so we define an __iter__
method which returns a custom iterator on that container (AccountingMethodIterator
). The iterator class is custom defined: what makes AccountingMethodIterator
a Python iterator is the __next__
method, which returns the next element (in this case it's an InTransaction). When there are no more elements to iterate on __next__
raises StopIteration
.
So translating the above example to our case here:
AbstractPaginationDetails
becomesAbstractPaginationDetailSet
,DateBasedPaginationDetails
becomesDateBasedPaginationDetailSet
, etc. (to capture the idea that they are container classes)- Add "element" classes
AbstractPaginationDetails
,DateBasedPaginationDetails
, etc. These are the elements contained in the container class and returned by the custom iterator (equivalent toInTransaction
in the example above). These classes are very simple and contain only the fields necessary to do actual pagination: i.e.symbol
,since
,limit
,parameters
, etc. Perhaps these can be NamedTuples or dataclasses with frozen=true, to make sure they are read-only. - add
def __iter__(self) -> "AbstractPaginationDetailsIterator"
method toAbstractPaginationDetailSet
(it's not implemented in the abstract class but only in the subclasses) - Add
DateBasedPaginationDetailsIterator
class. Its constructor receives all the information necessary to iterate: markets, current_results, etc. This class defines and updates iteration indexes - Add
def __next__(self) -> DateBasedPaginationDetails:
method toDateBasedPaginationDetailsIterator
which returns the next object and updates indexes. If there are no more objects it returnsStopIteration
With this approach the loop becomes something like:
pagination_detail_set: AbstractPaginationDetailSet = self.get_process_trades_pagination_detail_set(trades)
while pagination_details in pagination_detail_set:
...
I think this will pay off in terms of design clarity: we want to make this superclass as clean as possible because it will be used by every CCXT plugin.
I hope this all makes sense. Let me know if you have any questions or run into issues.
The iterator is going to need to know what comes back from the exchange in order to return the next "element". So, we will need to do something like this:
The iterator will need to know the live data coming back from the server in order to calculate the next date range. If there are more records and we hit the limit, we need to pull the window back. We will not know that information until we start pulling the data. So, is it possible to update the iterator in the middle of the iteration? |
That's a really good point. To solve the problem I think we need to use the lower-level iterator API. Here's what I mean:
Note that Examples of this pattern in the code:
|
It looks like the failed checks are due to a server outage. |
We may want to mock test_plugin_ccxt.py to avoid these problems. |
I'm wondering what to name What would be a good clear name? Sector, grouping? |
No strong opinion: perhaps symbol or product. We can also rename it later if we think of a better name. |
I have to do some cleaning, but core functionality should be available. I"m going to work on the implicit API and how to integrate that smoothly. |
Hm, one more thing: it looks like CCXT may not be thread-safe (see ccxt/ccxt#2252) Two potential solutions:
Anyway, let's keep the parallel infrastructure in and keep exploring it: if we don't find any way of making CCXT work in parallel, we'll hardcode n_threads=1 (or remove the multithreading code), but I'm cautiously optimistic we can apply one of the above solutions successfully. |
Thinking about the implicit API. Do you think it would be better if the abstract class uses
Each implicit API function would also need a custom Do you think this is too complicated? or should there simply be a I guess another option would be to have a |
The two approaches are not mutually exclusive: I was thinking we could do the simple approach first ( |
Ok, I added in gains. I made it a separate function from |
I just need to add dust trades, and then I should be able to wrap this up. |
OK, this is all wrapped up. Should I add something to the docs? |
Great! I'll start reviewing. Yes, you can add reference to the abstract class to https://github.com/eprbell/dali-rp2/blob/main/README.dev.md#data-loader-plugin-development |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor changes.
Could you get back to me on the questions above? re: |
I like explicit behavior even if it's a little more work, so I would say leave it fully abstract in the superclass (no |
Oh, sorry if I missed anything... I can't find any unresolved and unanswered questions in this PR (but Github can be a bit cumbersome with its folding of content): could you repeat the questions here? |
Huh, that's strange. It lists these all unresolved for me. Here they are: You commented about making
However, that would make them unavailable in the subclass right? I have the subclass setup to return results from single transaction processes with this tuple to support multi-threading. I also use Also, my comment about renaming I'm not that familiar with Python naming, but basically what this function is doing is parsing 3 variables into more useful information, so I think My comments on
If I guess I saw these functions just covering the basic unified API and anything else needs to be covered in Also, I don't think it is a good idea to encourage these to be overwritten since it might make mult-threading more complicated. If we know these only cover the unified API it will be easier to optimize and prevent race conditions. |
I found the comments in file view (still not in the conversation view), but your reply isn't there: did you submit your comments? In any case I'll reply here.
True: moot point, then.
That's fine: it was just a suggestion and it's a matter of personal preference. Let's keep
Another good point. Ok, let's leave it as is, then.
Yeah, I was thinking the user might add implicit "deposit" APIs to their own version of
So it sounds like we can ignore all of these comments and move on. I'll review everything once more and then we can merge. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few minor fixes needed.
try: | ||
while True: | ||
pagination_details: PaginationDetails = next(pagination_detail_iterator) | ||
trades: Optional[List[Dict[str, Union[str, float]]]] = self._client.fetch_my_trades( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any thoughts on this?
self._client: Exchange = self._initialize_client() | ||
self._thread_count = thread_count if thread_count else self.__DEFAULT_THREAD_COUNT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All fields still need to start with double underscore (our data models are encapsulated and we never access fields directly outside the class in which they are defined). To implement protected behavior they need a property that starts with single underscore (to access in the subclass). Sorry if my description wasn't clear when we discussed this.
Here's an updated text that's hopefully clearer:
- all class fields have two leading underscores because our data models are encapsulated and we never access fields directly outside the class in which they are defined
- private fields: visible only to the class, nothing else to do (access within the class via the double-underscore filed)
- protected fields: visible to the class and to its subclasses, needs to be encapsulated with a property or an accessor function that starts with one leading underscore
- public fields: visible to everybody, needs to be encapsulated with a property or acccessor function that starts with no leading underscore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, unfortunately we keep going around and around on this. I guess I understand the basic concept, but how does this look when implemented? So for client
and logger
, I should have double underscore:
self.__client
self.__thread_count
And then, property accessors:
@property
def client(self):
return self.__client
@property
def thread_count(self):
return self.__thread_count
Or do I use _get
accessors?
(These fields are used in the subclasses.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, actual fields have leading double underscore:
self.__client
self.__thread_count
Then:
- if we want private access (this class only) we do nothing else
- if we want protected access (this class and its subclasses), we add a property with a single underscore (or an accessor method starting with
_get
) - if we want public access (everybody), we add a property with no underscore (or an accessor method starting with
get
)
Whether to use a property or a get function is up to you: I tend to use properties if the access logic is fairly simple and get functions if access logic is more complicated. But again, either way works. Hope this is clearer, sorry if I didn't articulate it clearly enough earlier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a data encapsulation paragraph (with the above description) to the Design Guidelines section of README.dev.md both in RP2 and DaLI. Let me know if you think it needs more explanation: I'd like to fix the docs so that nobody trips on it in the future.
I just want to double check that you can see my most recent comments on |
Unfortunately I cannot :-( When you look at your comments do they show up as "pending"? If so you need to submit them to make them publicly visible. Not sure what else this could be, short of a site malfunction... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, now, you should see all comments, right?
try: | ||
while True: | ||
pagination_details: PaginationDetails = next(pagination_detail_iterator) | ||
trades: Optional[List[Dict[str, Union[str, float]]]] = self._client.fetch_my_trades( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, unfortunately.
self._client: Exchange = self._initialize_client() | ||
self._thread_count = thread_count if thread_count else self.__DEFAULT_THREAD_COUNT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, unfortunately we keep going around and around on this. I guess I understand the basic concept, but how does this look when implemented? So for client
and logger
, I should have double underscore:
self.__client
self.__thread_count
And then, property accessors:
@property
def client(self):
return self.__client
@property
def thread_count(self):
return self.__thread_count
Or do I use _get
accessors?
(These fields are used in the subclasses.)
try: | ||
while True: | ||
pagination_details: PaginationDetails = next(pagination_detail_iterator) | ||
trades: Optional[List[Dict[str, Union[str, float]]]] = self._client.fetch_my_trades( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, unfortunately. CCXT outputs floats for some reason. It also accepts floats for its other functions for amounts.
Huh, so you don't see my other comment on this? That's so bizarre, it's like Github made them all disappear for you.
Ok, now, everything should show. I have no idea how I have used Github for so long and not realized you have to submit all your comments. I have no idea what I was doing before. Sorry about that. |
Now I see it: mystery solved :-) Will take a look in the next 2-3 days (currently traveling). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I think I replied to all of the comments. Let me know if anything is still missing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More answers.
self._client: Exchange = self._initialize_client() | ||
self._thread_count = thread_count if thread_count else self.__DEFAULT_THREAD_COUNT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, actual fields have leading double underscore:
self.__client
self.__thread_count
Then:
- if we want private access (this class only) we do nothing else
- if we want protected access (this class and its subclasses), we add a property with a single underscore (or an accessor method starting with
_get
) - if we want public access (everybody), we add a property with no underscore (or an accessor method starting with
get
)
Whether to use a property or a get function is up to you: I tend to use properties if the access logic is fairly simple and get functions if access logic is more complicated. But again, either way works. Hope this is clearer, sorry if I didn't articulate it clearly enough earlier.
try: | ||
while True: | ||
pagination_details: PaginationDetails = next(pagination_detail_iterator) | ||
trades: Optional[List[Dict[str, Union[str, float]]]] = self._client.fetch_my_trades( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, sounds good.
I think I more or less wrapped everything up. I overrode the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor typos. I'll do one more full review once those are fixed and then we'll merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Grats! This is an important merge. |
It's good to see this go through! Thanks for all the help on the differences with protected / private. |
This PR addresses issue #22 .
This only has
_process_trades()
functioning. I commented out code that I'll edit later.Basically, I just want to submit this to see if this is the architecture you were thinking and if everything looks right. If it looks good to you, I'll continue on with deposits, withdrawals, and then the implicit API.
I think I can implement pagebased pagination from the Coinbase code, but idbased might be difficult to do without a real-world example.
Let me know what you think.