Rejected Sell Trades Can Lose 10% Of Your Portfolio... Or More
#1
I had seen this issue for sometime but I finally came up with a really rough fix. I would really appreciate if someone can write the code to do this asynchronously.

Issue: (Known to occur in Coinbase Pro, but can occur for any exchange that lets you use limit orders and a post flag that prevent executing market orders) When Gekko issues a sell order, in the seconds that it receives the current price from the exchange and before it sends the sell order to the exchange, the price went up. Normally, that's a good thing. This shows there's so much volume that your order shouldn't have any problem getting filled. The problem is, when the order goes to the exchange, the exchange rejects it because Gekko is now trying to sell below the ask price. Once the order is rejected, Gekko doesn't retry the order. Technically, even if Gekko did, it would get a message back from the exchange "HTTP 400 Error: order not found" and it will continue to get this message until the retry attempts are exhausted. So that's the issue. If your strategy issued a sell order to get out of the market in anticipation of further drops, you're now screwed!!! Gekko will be holding the bag for you while the crypto goes down in price from 2% to 5% to 10% (on a bad day) or more if you are trading an extremely volatile crypto.

To Test This: Modify sticky.js inside the exchange/orders folder. (You need to check your trade pair in the Coinbase Pro UI to see the spread first. See note directly below). In line 129, replace this:

Code:
return r(ticker.ask);

with:

(for Coinbase Pro)
Code:
return r(ticker.ask - 1);

If you want to see rejected buy trades, you can modify line 111 from this:

Code:
return r(ticker.bid);

with:

(for Coinbase Pro)
Code:
return r(ticker.bid + 1);

***Note: I previously modified it from 0.03 to 10% but in my recent test, I was getting insufficient funds error instead of the rejected trade error. I now changed it to 1 but it really comes down to the trade pair. The optimum number is a number that is slightly larger than the spread.***

This essentially tricks Gekko into selling below ask or buying above bid prices. For rejected buy trades, they don't cost you anything except for potential gain. But that could be a lot! Anyway, the next time your strategy issues a buy/sell order, you will see the rejected trade error in the console. It looks something like this.
Quote:2019-01-14 16:07:18 (INFO): Trader Received advice to go long. Buying  ETC

2019-01-14 16:07:18 (DEBUG): Creating order to buy 2.174301930195067 ETC



Mon Jan 14 2019 16:07:23 GMT-0500 (EST) {}

sticky create

buy

2019-01-14 16:07:23 (DEBUG): [ORDER] statusChange: SUBMITTED

2019-01-14 16:07:23 (DEBUG): [ORDER] statusChange: OPEN

2019-01-14 16:07:24 (DEBUG): [ORDER] statusChange: REJECTED

2019-01-14 16:07:24 (INFO): [ORDER] summary: { price: 0,

  amount: 0,

  date: moment("1969-12-31T19:00:00.000"),

  side: 'buy',

  orders: 1 }
2019-01-14 16:07:24 (DEBUG): syncing private data
I know ETC was 51% attacked recently. This is just a test and ETC is the cheapest crypto I have access to on Coinbase Pro. But do note the erroneous 1969 date listed in console. It usually is 12/31/1969 or 1/1/1970. If you ever seen this in the past, your trade failed to execute.

Solution: I am not an advanced coder in Javascript, so this isn't the cleanest solution, but it works. The idea is to have your strategy issue another buy order after getting the "rejected" message from the exchange. One thing I noticed while coming up with this is if you try to issue another buy order, Gekko completely ignores it and doesn't even output a message to console. I'm guessing this is because older strategies send multiple buy orders and Gekko has to ignore them. So the only way to get Gekko to accept a new buy order is to issue a sell order first.

The files I modified are:
config file
gdax.js
strategy file

In the config file, we are going to store the state of "rejected" so the other files can read/write to them. I just added the following before the last line. You can probably add them anywhere.

Code:
config.IssueState = {
 rejected: false,
 side: 'sell',

}

In gdax.js, we are going to give it the ability to write to the rejected variable in the config file, so we need to add this at the top where the other required files are declared.

Code:
var config = require('../../core/util.js').getConfig();

Now, in the checkOrder function, in the if (status == 'pending') section, I changed it from:

Code:
if(status == 'pending') {
     // technically not open yet, but will be soon
     return callback(undefined, { executed: false, open: true, filledAmount: 0 });
   } if (status === 'done' || status === 'settled') {
     return callback(undefined, { executed: true, open: false });
   } else if (status === 'rejected') {
     return callback(undefined, { executed: false, open: false });
   } else if(status === 'open' || status === 'active') {
     return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.filled_size) });
   }

to:

Code:
   if(status == 'pending') {
     // technically not open yet, but will be soon
     return callback(undefined, { executed: false, open: true, filledAmount: 0 });
   } if (status === 'done' || status === 'settled') {
     config.IssueState.rejected = false;
     return callback(undefined, { executed: true, open: false });
   } else if (status === 'rejected') {
       if (data.reject_reason == 'post only' ) {
         config.IssueState.rejected = true;
         config.IssueState.side = data.side;
       }
     return callback(undefined, { executed: false, open: false });
   } else if(status === 'open' || status === 'active') {
     return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.filled_size) });
   }
It essentially updates the "rejected" variable in the config file to true if the trade was rejected. It will also store the side so we know if it is a buy or sell order. If the trade is successfully placed, it will change the "rejected" variable in the config file to false.

In the strategy, I again have to give it the ability to access the config file by adding this at the top, where all the other required files are declared:

Code:
var config = require ('../core/util.js').getConfig();
The line is slightly different than the one for gdax.js because of the strategy file is stored in a different location than gdax.js.

I then added a variable to check how often to check if the "rejected" variable is still true. This is a global variable at the top where other variables are placed. 

Code:
var waitForRejectedRetry = 0;
This is where asynchronous code would make this much cleaner. Instead of checking after x minutes, you can have this run only when status changes from either rejected or completed/settled in gdax.js. But I couldn't figure out how to implement that.

So instead this is the code in the check function of the strategy:
Code:
 if (config.IssueState.rejected){
   if (waitForRejectedRetry == 0){
     if (config.IssueState.side == 'buy'){
       this.advice('short'); // To reset previous buy order by issuing a sell order
       this.advice('long');
     } else {
       this.advice('long'); // To reset previous sell order by issuing a buy order
       this.advice('short');
     }
     waitForRejectedRetry = 11;
   }
   if (waitForRejectedRetry > 0) {
     waitForRejectedRetry--;
   }

 }
With this code, it will create a new buy/sell order if "rejected" is set to true in the config file. As mentioned previously, it has to issue a sell first if a buy order was rejected or buy first if a sell order was rejected. It will then wait ~10 minutes (the check order function doesn't always run every minute, assuming 1 minute candles). If the "rejected" variable is still valid, it will repeat this until the order is no longer rejected. Again, I wish it doesn't have to do this every x minutes but this is the only way to do it that I know of synchronously.

I tested this and confirmed that it works. Don't forget to remove the code in sticky.js otherwise every order will be rejected. If you're interested to simulating a scenario where every other trade starting with the first is a rejected trade, you can modify sticky.js to read from the "rejected" variable from the config file. I will include the code in a reply post if anyone is interested. 

I now have to implement this in all of the strategies that I use. I can't wait for the official fix from AskMike. I know it is on his to-do list.
If it isn't crypto, it isn't worth mining, it isn't worth speculating.
https://www.youtube.com/c/crypto49er
  Reply
#2
Hi Jack, great for digging into this issue. I am running into errored/aborted buy and sell advices also. It is hard to reproduce, they occur from time to time, starting with gekko 0.6. I know many fixes have been already applied but the prob is still there.

Occording your findings I modified line 111 also to see how buy trades will behave, here is my output: https://pastebin.com/My77muTD

So this change is applied: return r(ticker.bid + 0.03);
You mentioned the "exchange rejects it because Gekko is now trying to sell below the ask price.". I added market taking feature recently and used exactly this behaviour to achieve it, so I was wondering why an exchange should reject a limit buy order which is below the topmost ask price. In this case the exchange immediately executes it and applies market taking fees.

It think a problem with implicit string conversion happens with this expression: ticker.bid + 0.03
What you get is actually a new string like: 111.050000.03
This is causing a rejection by the exchange. Can you test and look if you get the conversion side effect also?

Maybe your workaround with buy/sell/buy still works, but we need to further investigate (and it is not caused by calculatePrice method)?
  Reply
#3
Hey Mark,

I did notice from your Pastebin that you're using Kraken. It's unfortunate Kraken isn't available where I live (NY), so I can't test this. 

I did run this multiple times in Coinbase Pro and was able to get the "rejected" error every time. Although I thought it was pointless to constantly get this error and that's why I ended up modifying the test code to trigger the "rejected" error only if it didn't get the error previously. This is what I modified in sticky.js.

Code:
     if(!this.outbid) {
       var moddedBid = config.IssueState.rejected ? ticker.bid : ticker.bid * 1.1;
       return r(moddedBid);
     }

I also modified the ticker.ask section similarly.
Code:
     if(!this.outbid) {
       var moddedAsk = config.IssueState.rejected ? ticker.ask : ticker.ask * 0.9;
       return r(moddedAsk);
     }

***I changed the price difference from 0.03 to 10% to account for trade pairs that have a larger spread than 0.03.***

I'm not sure if this modification has any affect on the implicit string conversion that you mentioned. But I ran this in VS Code to see what is stored inside the variable and it correctly stored 4.38 instead of "4.35000001.03" as the price of ETC on Coinbase Pro is $4.35 when the trade was sent to Coinbase Pro.
[Image: attachment.php?aid=331]

The "rejected" trade response came a few seconds after.
[Image: attachment.php?aid=332]

I'm guessing Coinbase Pro sends the ticker information differently than Kraken. Maybe Kraken's ticker info is sent as a string in which case you probably have to convert it to a float, add the 0.03, then convert it back to a string so Kraken accepts the modified price. But that's all guesswork since I don't have access to Kraken.


Attached Files
.png   Screen Shot 2019-01-15 at 5.57.38 PM.png (Size: 291.93 KB / Downloads: 163)
.png   Screen Shot 2019-01-15 at 5.58.03 PM.png (Size: 284.69 KB / Downloads: 161)
If it isn't crypto, it isn't worth mining, it isn't worth speculating.
https://www.youtube.com/c/crypto49er
  Reply
#4
Right, I am using kraken and have no coinbase pro account unfortunately.

The ticker.bid data type from line 111 might vary depending on exchange when you have no issues by adding 0.03. With kraken it is definately string, I get results like 108.340000.03

By changing it to return r(Number(ticker.bid)+0.03); I can add 0.03 and it should behave the same on kraken and coinbase. So after applying this change I tested again: https://pastebin.com/hrR9yhEM

The order gets executed pretty quick, so I am unable to force a rejected trade this way. Do you still use the "limit" order type? In case of kraken it is using the limit order type - I leave it this order type even when I do market taking. I only set a limit of e.g. 1% deep into the orderbook to force immediate execution. No need to switch to order type "market".
  Reply
#5
I'm glad we're able to figure out that Kraken uses string instead of float. 

I think I know why the trade executes (post to the order book) on Kraken. Although I can't open a Kraken account, I'm glad they have no restriction on accessing their interface. 
[Image: attachment.php?aid=333]

Looking at the spread for ETH/EUR, there's a difference of 21 cents between the bids and asks. So adding 0.03 would just put you on top of the order book, instead of trading across the book. I underestimated how big a spread can be for different exchanges even for fiat/crypto trade pairs. Since we are aiming to get the "rejected" trade error, we should go for as high a number as possible, adding 10% to the price of crypto should absolutely put the order across the book. So a r(Number(ticker.bid)*1.1) should do the trick.


Attached Files
.png   Screen Shot 2019-01-16 at 10.01.33 AM.png (Size: 288.59 KB / Downloads: 146)
If it isn't crypto, it isn't worth mining, it isn't worth speculating.
https://www.youtube.com/c/crypto49er
  Reply
#6
It immediately executes with a price of 10% into the orderbook, as expected: https://pastebin.com/TXN4cVmc
In this case the order does not move and executes immediately.

if(!this.outbid) {
return r(Number(ticker.bid)*1.1);
}

What happens when you buy with this explicit type conversion?

In the meanwhile I was looking into some older log files when my sticky order failed, on january the 3rd it happened the last time: https://pastebin.com/cAbFEhuN

The order was created properly, moved serveral times on the orderbook and got rejected after about 1 minute
  Reply
#7
I'm sorry that didn't work.

Looking at the output from Pastebin, even though the price you submitted is 10% above the order book price, which should roughly be ~118, it traded at market price of 107.77.

In Coinbase Pro, the "post only" flag prevents a trade to be executed at market price. I don't see a similar flag in the Kraken API wrapper, although the API itself does support this via the "post" flag. https://www.kraken.com/help/api#private-user-trading

But assuming you're using the standard API wrapper, the reason your sell trade was rejected on January 3rd is different from mines. The only way to find the error is to have both the err and data variables from the checkOrder function dumped out to console for every trade until you get a rejected trade.
If it isn't crypto, it isn't worth mining, it isn't worth speculating.
https://www.youtube.com/c/crypto49er
  Reply
#8
According to kraken's API call rate limit I would say it should not hit the limits: https://support.kraken.com/hc/en-us/arti...rate-limit-

I have set customInterval: 5000 inside the config file, only point 3) is not clear wheter it gets any penalties by canceling the order within the 15 second timeframe:

"3. Placing orders rate limit is based on time on book and rate limited per pair by account. The longer the order is left on the book, the more you can trade. Canceled orders penalize more than filled ones. The penalty curve is high until 15 seconds and then becomes negligible if the order is on the book for more than 5 minutes."

I am just moving all strategy advices to 1% limit orders for (most probably) immediate execution - and see if the error will ever happen again

objcontext.advice({
direction: 'long',
setTakerLimit: config[config.tradingAdvisor.method].setTakerLimit,
origin: 'T5fasciatus',
date: moment(),
infomsg: 'Main strategy, 60M candles: New advice to go LONG, ' + candle.close,
});
  Reply
#9
I don't think it is rate limit issue either as once you hit it, you are suspended from access for 15 minutes. They mentioned this in point 2.

"2. Private calls have a counter per API key. Each key's "call counter" starts at 0. Certain calls will increase the counter. If the counter exceeds the key's maximum call count (based on user's verification level), API access is suspended for 15 minutes."

It could one of the other errors, like hitting the open positions limit, https://support.kraken.com/hc/en-us/arti...-positions-, but that depends on how they consider open positions. Is 5 cancelled orders within 1 minute still considered opened? The article didn't specify and it also didn't specify what would happen if you hit that limit.

I don't wish for you to get the rejected trade error again. But if you did, I'm sure you will code up a better version to recreate/retry the order as it is painful to wake up to Gekko failing to exit when it was supposed to.
If it isn't crypto, it isn't worth mining, it isn't worth speculating.
https://www.youtube.com/c/crypto49er
  Reply
#10
In case of kraken the rejected error are not the result of hitting the API call rate limit, the kraken docu is not clear, but your assumption was correct. I forced to hit the limit by removing the orderbook movement delays:

2019-01-16 17:51:31 (DEBUG): [ORDER] statusChange: OPEN
2019-01-16 17:51:32 (DEBUG): [ORDER] statusChange: MOVING
2019-01-16T16:51:34.483Z 'Order:Rate limit exceeded'
2019-01-16T16:51:38.955Z 'Order:Rate limit exceeded'
2019-01-16 17:51:43 (DEBUG): [ORDER] statusChange: OPEN
2019-01-16 17:51:44 (DEBUG): [ORDER] statusChange: MOVING
2019-01-16T16:51:45.952Z 'Order:Rate limit exceeded'
2019-01-16T16:51:49.825Z 'Order:Rate limit exceeded'
2019-01-16 17:51:49 (DEBUG): Requested ETH/EUR trade data from Kraken ...
2019-01-16 17:51:50 (DEBUG): Processing 1 new trades. From 2019-01-16 16:51:41 UTC to 2019-01-16 16:51:41 UTC. (a few seconds). Last price: 106.61
2019-01-16 17:51:53 (DEBUG): [ORDER] statusChange: OPEN
2019-01-16 17:51:54 (INFO): [ORDER] partial sell fill, total filled: 0.09909065
2019-01-16 17:51:54 (DEBUG): [ORDER] statusChange: FILLED
2019-01-16 17:51:55 (INFO): [ORDER] summary: { price: 106.65000000000002,
amount: 0.09909065,
date: moment("2019-01-16T17:51:53.970"),
side: 'sell',
orders: 1,
feePercent: 0.16 }

It is hitting the rate limit but gets finally executed later on without throwing any rejected error.

It is hard to test here, but the silent exit in any case of a rejection error leaves the possible trouble of missing a proper sell. So I added a little retry loop to force a new createOrder in case of a rejection, 3 attempts with one minute delay in between. Maybe you can test with your coinbase pro if it needs further tweaks or is able to solve your sell problem? Just commited into my repo. I remember you had a prob with my telegram bot also to use the portfolio function. I am working on a heavily improved telegram bot version, not ready yet to release, but I have backported and commited the portfolio fix - it happend while gekko is in history warmup state.
  Reply


Forum Jump:


Users browsing this thread: