happydasch / btoandav20

Support for Oanda-V20 API in backtrader
Apache License 2.0
131 stars 52 forks source link

Trailing Stop by percent gives error #46

Closed booboothefool closed 4 years ago

booboothefool commented 4 years ago

Using trailamount seems to be ok, but trailpercent gives error.

self.long_order2 = self.buy_bracket(exectype=bt.Order.Stop, stopexec=bt.Order.StopTrail, size=qty,
            limitprice=TP2_price,

            price=entry_price,

            # stopprice=stoploss_price,
            stopargs={
                # 'trailamount': stop_dist,
                'trailpercent': 0.01,
            },
        )
  File "brokers/backtrader/algo.py, line 574, in buy_risk
    'trailpercent': 0.01,
  File "/usr/local/lib/python3.7/site-packages/backtrader/strategy.py", line 1169, in buy_bracket
    olimit = self.sell(**kargs)
  File "/usr/local/lib/python3.7/site-packages/backtrader/strategy.py", line 969, in sell
    **kwargs)
  File "/usr/local/lib/python3.7/site-packages/btoandav20/brokers/oandav20broker.py", line 315, in sell
    return self._transmit(order)
  File "/usr/local/lib/python3.7/site-packages/btoandav20/brokers/oandav20broker.py", line 272, in _transmit
    self.o.order_create(parent, stopside, takeside)
  File "/usr/local/lib/python3.7/site-packages/btoandav20/stores/oandav20store.py", line 385, in order_create
    '.%df' % order.data.contractdetails['displayPrecision']),
TypeError: unsupported format string passed to NoneType.__format__

At a glance, it looks like there might not be support for trailing stop by percent? https://github.com/ftomassetti/backtrader-oandav20/blob/master/btoandav20/stores/oandav20store.py#L384 If so, could it be added?

Thank you for the library! 👍

happydasch commented 4 years ago

I will look into this, but for now, use trailamount. There was an issue, why I did not add trailpercent, but don't really remember why.

booboothefool commented 4 years ago

Thanks I really appreciate it! However now that I let it run on paper trade for a bit, it seems like i can't get a StopTrail even with just trailamount to fully Completed at all. It is able to place the order e.g. a bracket of Stop+StopTrail+TakeProfit or a Market+StopTrail+TakeProfit are Submitted, but the entry Stop/Market order appears to get Rejected every time if it is paired with a StopTrail.

2020-05-18 14:06:05 | Stop Order | GBP/USD | 7 | Buy | 1.21986 | 1.22178 | 1.21922 |   |   |   |   |   |  
-- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | --
2020-05-18 14:06:05 | Stop Order | GBP/USD | 7 | Buy | 1.21986 | 1.22114 | 1.21922 |   |   |   |   |   |  
^ without `stopexec=bt.Order.StopTrail`

v with `stopexec=bt.Order.StopTrail`
2020-05-18 13:54:45 | Market Order Reject |   |   |   |   |   |   |   |   |   |   |   |  
2020-05-18 13:54:45 | Market Order Reject |   |   |   |   |   |   |   |   |   |   |   |  
2020-05-18 13:41:25 | Stop Order Reject |   |   |   |   |   |   |   |   |   |   |   |  
2020-05-18 13:41:25 | Stop Order Reject

The Rejected Stop and Market orders you see there used:

self.long_order2 = self.buy_bracket(exectype=bt.Order.Stop, stopexec=bt.Order.StopTrail, size=qty,
            limitprice=TP2_price,
            price=entry_price,
            # stopprice=stoploss_price,
            stopargs={
                'trailamount': stop_dist,
            },
        )

self.long_order2 = self.buy_bracket(exectype=bt.Order.Market, stopexec=bt.Order.StopTrail, size=qty,
            limitprice=TP2_price,
            price=entry_price,
            # stopprice=stoploss_price,
            stopargs={
                'trailamount': stop_dist,
            },
        )

The orders that were Accepted used:

self.long_order2 = self.buy_bracket(exectype=bt.Order.Stop, size=qty,
            limitprice=TP2_price,
            price=entry_price,
            stopprice=stoploss_price,
            # stopargs={
            #     'trailamount': stop_dist,
            # },
        )

Note that I removed stopexec=bt.Order.StopTrail to get it to go through, but of course now I don't have the trailing stop that I want. stop_dist is just a value like 0.0003 for 3 pips.

I am not sure if the issue is related or if it's supposed to work and I just did my input wrong, however everything worked perfectly during backtesting. Is there a way to log out the actual errors Oanda API is sending? That would probably reveal what the issue is.

happydasch commented 4 years ago

The order is rejected mostly because of a too close trail stop (GBP/USD is 5 pips I think)

booboothefool commented 4 years ago

Oh, you're right. I see it's actually happening on a few of my regular non-StopTrail orders too. Also it's trading the 5 second chart so the price and calculated stop locations might have moved by the time it gets a signal and tries to create an order. Makes sense. Are there some docs somewhere on the minimum stop requirements so I can make sure I'm in bounds?

But wow, even though it doesn't even work properly as I intended, it's actually still profitable! Thanks a bunch!

happydasch commented 4 years ago

you can check the trailing stop distance with the data from contract details of the feed:

data.contractdetails['minimumTrailingStopDistance']

and

data.contractdetails['maximumTrailingStopDistance']

the value should be in pips, for the pip location you can use:

data.contractdetails['pipLocation']

# 
# The location of the “pip” for this instrument. The decimal position of
# the pip in this Instrument’s price can be found at 10 ^ pipLocation (e.g.
# -4 pipLocation results in a decimal pip position of 10 ^ -4 = 0.0001).
# 
pipLocation : (integer),

# 
# The maximum trailing stop distance allowed for a trailing stop loss
# created for this instrument. Specified in price units.
# 
maximumTrailingStopDistance : (DecimalNumber),

# 
# The minimum trailing stop distance allowed for a trailing stop loss
# created for this instrument. Specified in price units.
# 
minimumTrailingStopDistance : (DecimalNumber),

See also:

https://developer.oanda.com/rest-live-v20/primitives-df/ Instrument definition

happydasch commented 4 years ago

i have committed a implementation for the trailpercent, please check this and give me some feedback for it. Thanks

booboothefool commented 4 years ago

I believe it worked! With this code:

self.long_order2 = self.buy_bracket(exectype=bt.Order.Stop, stopexec=bt.Order.StopTrail, size=qty,
            limitprice=TP2_price,
            price=entry_price,
            # stopprice=stoploss_price,
            stopargs={
                # 'trailamount': stop_dist,
                # 'trailpercent': 0.01,
                'trailpercent': 0.02,
            },
        )
  File "/usr/local/lib/python3.7/site-packages/btoandav20/stores/oandav20store.py", line 385, in order_create
    '.%df' % order.data.contractdetails['displayPrecision']),
TypeError: unsupported format string passed to NoneType.__format__

Error no longer appears and the order goes through! 👍

I wonder if this is intended behavior - in TradingView it does not show up under "Trailing Stop" but just spam many "Stop". Maybe somewhere Oanda/Backtrader/library it doesn't understand that it is "Trailing Stop" so it just updates by creating more orders? I think it's ok? Screenshot 2020-05-18 20 18 45

Oh well, I did notice it increased the profit of my winning strategy, so I am very happy! Soon I will be able to quit my day job! Just have to learn how to calculate pips so I can increase quantity of trade... I'm new to trading and FOREX and 0.001 pip is very confusing to me!! 💸🎉

Screenshot 2020-05-18 20 30 26

Do you know if it is possible to add ATR trailing stop? For example:

self.atr = bt.ind.ATR()
'trailamount': self.atr[0]

Or that only set the first distance and it just uses only that distance? It does not dynamically calculate the new ATR every time it moves, does it? Cause that would be amazing and even more profit! If not, it is no problem. Thank you very much!

happydasch commented 4 years ago

for the atr value, it will not update the created order. if you want to update an existing order, you would need to cancel them manually and create new ones.

happydasch commented 4 years ago

will check the new code later and close this issue if it works probably.

happydasch commented 4 years ago

Oh well, I did notice it increased the profit of my winning strategy, so I am very happy! Soon I will be able to quit my day job! Just have to learn how to calculate pips so I can increase quantity of trade... I'm new to trading and FOREX and 0.001 pip is very confusing to me!! 💸🎉

just be careful, since test code may generate different results than reallife code execution. hope the best for your strategies.

happydasch commented 4 years ago

for pips calculations, the value from

data.contractdetails['pipLocation']

is useful, when calculating pips for different instruments. also check out the different sizers, which provides position size calculations based on risk percent or risk amount.

https://github.com/ftomassetti/backtrader-oandav20/blob/master/btoandav20/sizers/oandav20sizer.py

booboothefool commented 4 years ago

Yes, I understand. Backtest seems too optimistic and inaccurate so unreliable, however I've noticed paper trade is close enough to the real result. I watched it trade all day today and was very happy with the performance so far, so it shows promise and I think it will continue. It is very consistent and manages risk well, so could also easily lose 10x trades in a row and still be safe. I am hopeful, thanks!

And thanks for the tips about the pips and sizers! Yes I was wondering about that cause EUR/USD USD/JPY, etc. Very happy!

Don't risk more than 1%!!! 🥇

happydasch commented 4 years ago

I like this attitude. Wish you a lot of luck. Will check the trailpercent and close this issue.

booboothefool commented 4 years ago

Oh, actually I made mistake earlier. The picture I showed of all "Stop" orders was actually "Stop". When "StopTrail" is completed successfully, it should show up under "Trailing Stop" column like this: Screenshot 2020-05-19 14 18 40 So it looks like it works! 👍

Had questions though after seeing the implementation at https://github.com/ftomassetti/backtrader-oandav20/commit/ef0fc2c746af2236a52fb2eff0c2dd70321b042f#diff-fbbb1d2f4d23dc3fcd8e4fcb14b21e17R378 Does thetrailpercent here update to new price difference whenever it moves, or does it just take the first price and use that only similar to how ATR stop can't update?

So if trail by 1%, which one? A: (99) 100 -> (108.9) 110 or B: (99) 100 -> (109) 110

I would like it to do A, but it looks like it might be doing B.

https://www.backtrader.com/docu/order-creation-execution/trail/stoptrail/ Also it looks like backtrader uses decimals e.g. 0.01. (100 price / 100) * 0.01 = 0.01 trailamount = 100 - 0.01 = 99.99 seems too small

So it looks like in your code you use percentage like 1%: (100 price / 100) * 1 = 1 trailamount = 100 - 1 = 99

Is this intentional or did you mean to match the backtrader and use 0.01 format? Thanks!

happydasch commented 4 years ago

I changed the calculation to just do price * trailpercent, so it will use 0.01 for 1 percent. also it will never change the amount, when the order is sent, it won't be changed anymore.

booboothefool commented 4 years ago

Just a comment. I noticed even though self.data.contractdetails['minimumTrailingStopDistance'] = 0.0005, 'trailamount': 0.0003 still works sometimes. I am not sure why.

happydasch commented 4 years ago

Will close this issue, since it seems to be working