backtrader2 / backtrader

Python Backtesting library for trading strategies
https://www.backtrader.com
GNU General Public License v3.0
221 stars 52 forks source link

Strategy's self.positions throws AttributeError when cerebro.broker set to IBBroker #61

Closed vladisld closed 2 years ago

vladisld commented 3 years ago

Community Forum Discussion: https://community.backtrader.com/topic/3637/self-positions-throws-attributeerror-when-cerebro-broker-set-to-ibbroker

Description

When trying to access the self.positions attribute from within the next method of a strategy, the following exception is thrown in case IBBroker is setup as a broker:

Traceback (most recent call last):
  File "/test/support_3637.py", line 30, in <module>
    run()
  File "/test/support_3637.py", line 27, in run
    cerebro.run()
  File "W:\backtrader\backtrader\cerebro.py", line 1177, in run
    runstrat = self.runstrategies(iterstrat)
  File "W:\backtrader\backtrader\cerebro.py", line 1351, in runstrategies
    self._runnext(runstrats)
  File "W:\backtrader\backtrader\cerebro.py", line 1687, in _runnext
    strat._next()
  File "W:\backtrader\backtrader\strategy.py", line 347, in _next
    super(Strategy, self)._next()
  File "W:\backtrader\backtrader\lineiterator.py", line 273, in _next
    self.nextstart()  # only called for the 1st value
  File "W:\backtrader\backtrader\lineiterator.py", line 347, in nextstart
    self.next()
  File "/test/support_3637.py", line 9, in next
    print(self.positions)
  File "\backtrader\backtrader\lineseries.py", line 461, in __getattr__
    return getattr(self.lines, name)
AttributeError: 'Lines_LineSeries_LineIterator_DataAccessor_Strateg' object has no attribute 'positions'

Process finished with exit code 1

Analysis

It seems the property 'positions' is not defined in IBBroker class, while defined in all other broker implementations. This properly is defined in IBStore class instead. The fix seems to be easy: just add the properly to IBBrokerclass which will just return the IBStore.positions property

Test case

import backtrader as bt

class TestPositions(bt.Strategy):
    def next(self):
        print(self.positions)

def run(args=None):
    cerebro = bt.Cerebro(live=True)
    store = bt.stores.IBStore(host='127.0.0.1', port=7496)
    cerebro.setbroker(store.getbroker())
    data = store.getdata(dataname='EUR.USD-CASH-IDEALPRO',
                           historical=False,
                           backfill=False,
                           backfill_start=False)
    cerebro.adddata(data)
    cerebro.addstrategy(TestPositions)
    cerebro.run()

if __name__ == '__main__':
    run()
neilsmurphy commented 3 years ago

Are we sure we are not referring to self.position? Your code above is working with 'self.position'.

vladisld commented 3 years ago

The code works ok if BackBroker is used IIRC.

neilsmurphy commented 3 years ago

I feel like I'm missing something. in both ib and backbrokers, self.position works fine. self.positions returns object in both. Am I looking at the wrong thing? What are we hoping to get back from self.positions vs. self.position?

vladisld commented 3 years ago

"self.positions returns object in both" Are you positive about it? The test I've attached shows otherwise

neilsmurphy commented 3 years ago

Hey @vladisld Here's the code I'm running:

class TestPositions(bt.Strategy):
    params = (("pos", None,),)
    def __init__(self):
        self.traded = False
        self.pos = self.position if self.p.pos == "position" else self.positions
    def next(self):
        if not self.traded:
            self.buy()

        print(f"{datetime.datetime.now()} position: {self.pos}")
        self.traded = True

def run(broker=None, pos=None):

    cerebro = bt.Cerebro()

    if broker == "backbroker":
        data = bt.feeds.GenericCSVData(
            dataname="data/2006-day-001.txt",
            dtformat=("%Y-%m-%d"),
            timeframe=bt.TimeFrame.Days,
            compression=1,
        )
        cerebro.adddata(data)

    elif broker == 'ib':
        cerebro = bt.Cerebro(live=True)
        store = IBStore(host='127.0.0.1', port=7497, clientId=***)
        cerebro.setbroker(store.getbroker())
        data = store.getdata(dataname='EUR.USD-CASH-IDEALPRO',
                               historical=False,
                               backfill=False,
                               backfill_start=False)
        cerebro.adddata(data)

    cerebro.addstrategy(TestPositions, pos=pos)
    cerebro.run()

if __name__ == '__main__':
    run(broker="ib", pos="positions")

For both ib and backbroker outputs are: for self.position

2021-03-23 18:17:15.258129 position: --- Position Begin
- Size: 3
- Price: 3.68721665
- Price orig: 3.68721665
- Closed: 0
- Opened: 0
- Adjbase: None
--- Position End

for self.positions in ib:

Server Version: 76
TWS Time at connection:20210323 18:18:47 EST
Traceback (most recent call last):
  File "20210323 ib_broker position error.py", line 48, in <module>
    run(broker="ib", pos="positions")
  File "20210323 ib_broker position error.py", line 45, in run
    cerebro.run()
  File "/home/runout/.local/lib/python3.8/site-packages/backtrader/cerebro.py", line 1127, in run
    runstrat = self.runstrategies(iterstrat)
  File "/home/runout/.local/lib/python3.8/site-packages/backtrader/cerebro.py", line 1217, in runstrategies
    strat = stratcls(*sargs, **skwargs)
  File "/home/runout/.local/lib/python3.8/site-packages/backtrader/metabase.py", line 88, in __call__
    _obj, args, kwargs = cls.doinit(_obj, *args, **kwargs)
  File "/home/runout/.local/lib/python3.8/site-packages/backtrader/metabase.py", line 78, in doinit
    _obj.__init__(*args, **kwargs)
  File "20210323 ib_broker position error.py", line 10, in __init__
    self.pos = self.position if self.p.pos == "position" else self.positions
  File "/home/runout/.local/lib/python3.8/site-packages/backtrader/lineseries.py", line 461, in __getattr__
    return getattr(self.lines, name)
AttributeError: 'Lines_LineSeries_LineIterator_DataAccessor_Strateg' object has no attribute 'positions'

For self.positions in backbroker.

2021-03-23 18:20:27.807886 position: defaultdict(<class 'backtrader.position.Position'>, {<backtrader.feeds.csvgeneric.GenericCSVData object at 0x7f4b37f54b80>: <backtrader.position.Position object at 0x7f4b37f54bb0>})

Is there a case where self.positions is an attribute?

vladisld commented 3 years ago

Not sure I understand your question, but in all other brokers (including BackBrocker) the positions attribute is defined as:

self.positions = collections.defaultdict(Position)

neilsmurphy commented 3 years ago

Not sure I understand your question, but in all other brokers (including BackBrocker) the positions attribute is defined as:

self.positions = collections.defaultdict(Position)

My misunderstanding. thanks.

neilsmurphy commented 3 years ago

Did we ever figure anything out on this?

vladisld commented 3 years ago

Probably I'm not following. What is it that needs to be figured out?

The problem seems to be clear, as well as a fix. Just need to do it, no?

neilsmurphy commented 3 years ago

I added in the offending line to fix the error, I even pushed. But I realized while this stops the error, we are not really getting results for the call. I'm not familiar with IBPy will try to find some time on the weekend to include the methods necessary to return positions.

I reverted the commit.

p595285902 commented 3 years ago

Also having the same issue. I personally use this to check if I hold any position in all stocks. I cant use positionbecause it only returns the position of the data0.