Skip to content

Commit e73b42b

Browse files
committed
Fix private strategy lineactions
1 parent cdf1ccf commit e73b42b

4 files changed

Lines changed: 694 additions & 46 deletions

File tree

backtrader/broker.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,13 @@ def _commission_lookup_keys(data):
139139
seen = set()
140140
for attr in ("name", "_name", "_dataname", "symbol"):
141141
value = getattr(data, attr, None)
142-
if value in (None, ""):
142+
if value is None:
143+
continue
144+
if isinstance(value, str) and value == "":
145+
continue
146+
try:
147+
hash(value)
148+
except TypeError:
143149
continue
144150
text = str(value)
145151
for key in (value, text, text.upper(), text.lower()):

backtrader/strategy.py

Lines changed: 39 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ def visit(value):
587587

588588
result = []
589589
for attr_name, attr_value in attrs.items():
590-
if attr_name.startswith("_"):
590+
if attr_name in {"_strategy_lineactions_cache", "_strategy_next_lineactions_cache"}:
591591
continue
592592
result.extend(visit(attr_value))
593593

@@ -904,52 +904,43 @@ def _feed_of(node, _seen=None):
904904
# Indicator minimum periods
905905
minperiods = [x._minperiod for x in all_indicators]
906906

907-
# CRITICAL FIX: Also scan strategy attributes for LineActions objects
908-
# (like LinesOperation from sma - sma(-10)) that aren't registered as indicators
909-
# but still need their minperiod considered
910-
from .linebuffer import LineActions
911-
912-
for attr_name in dir(self):
913-
if attr_name.startswith("_"):
914-
continue
915-
try:
916-
attr = getattr(self, attr_name)
917-
# Check if it's a LineActions but not already in _lineiterators
918-
if isinstance(attr, LineActions) and hasattr(attr, "_minperiod"):
919-
if attr not in self._lineiterators[LineIterator.IndType]:
920-
minperiods.append(attr._minperiod)
921-
except (AttributeError, TypeError):
922-
# Attribute access/typecheck failed; skip this attribute.
923-
pass
907+
# Strategy-owned LineActions are not necessarily registered indicators.
908+
# Original backtrader's metaclass machinery advanced them regardless of
909+
# whether users kept them in public or private attributes. Reuse the
910+
# recursive strategy-attribute scan here so private containers such as
911+
# self._Type / self._OptionType participate in minperiod and execution.
912+
strategy_lineactions = tuple(self._get_strategy_lineactions())
913+
for attr in strategy_lineactions:
914+
if (
915+
hasattr(attr, "_minperiod")
916+
and attr not in self._lineiterators[LineIterator.IndType]
917+
):
918+
minperiods.append(attr._minperiod)
924919

925920
# Set strategy minimum period to max of indicator and data minperiods
926921
self._minperiod = max(minperiods or [self._minperiod])
927922

928-
# CRITICAL FIX: Update _minperiods for LineActions, but only for their associated data
929-
# For single-data strategies, apply LineActions minperiod to data[0]
930-
# For multi-data strategies, LineActions minperiod should only affect its source data
931-
from .linebuffer import LineActions
932-
923+
# Update _minperiods for strategy-owned LineActions, but only for their
924+
# associated data. For multi-data strategies, LineActions minperiod
925+
# should only affect the source data that clocks the expression.
933926
if self._minperiods:
934-
for attr_name in dir(self):
935-
if attr_name.startswith("_"):
936-
continue
927+
for attr in strategy_lineactions:
937928
try:
938-
attr = getattr(self, attr_name)
939-
if isinstance(attr, LineActions) and hasattr(attr, "_minperiod"):
940-
# Try to determine which data this LineActions is associated with
941-
# by checking its _clock or data sources
942-
data_idx = 0 # Default to data[0]
943-
if hasattr(attr, "_clock") and attr._clock is not None:
944-
for i, d in enumerate(self.datas):
945-
if attr._clock is d or attr._clock in d.lines:
946-
data_idx = i
947-
break
948-
# Only update minperiod for the specific data
949-
if data_idx < len(self._minperiods):
950-
self._minperiods[data_idx] = max(
951-
self._minperiods[data_idx], attr._minperiod
952-
)
929+
if not hasattr(attr, "_minperiod"):
930+
continue
931+
# Try to determine which data this LineActions is associated
932+
# with by checking its _clock or data sources.
933+
data_idx = 0 # Default to data[0]
934+
if hasattr(attr, "_clock") and attr._clock is not None:
935+
for i, d in enumerate(self.datas):
936+
if attr._clock is d or attr._clock in d.lines:
937+
data_idx = i
938+
break
939+
# Only update minperiod for the specific data.
940+
if data_idx < len(self._minperiods):
941+
self._minperiods[data_idx] = max(
942+
self._minperiods[data_idx], attr._minperiod
943+
)
953944
except (AttributeError, TypeError):
954945
# Attribute access/typecheck failed; skip this attribute.
955946
pass
@@ -1502,7 +1493,9 @@ def _next(self):
15021493
forward_line._idx += 1
15031494
forward_line.lencount += 1
15041495
forward_line.array.append(
1505-
dt_value if valid_dt and dt_value >= 1.0 else forward_line._default_value
1496+
dt_value
1497+
if valid_dt and dt_value >= 1.0
1498+
else forward_line._default_value
15061499
)
15071500
elif valid_dt:
15081501
idx = datetime_line._idx
@@ -1726,8 +1719,7 @@ def _start(self):
17261719
and type(self).notify_cashvalue is Strategy.notify_cashvalue
17271720
)
17281721
self._notify_fund_default = (
1729-
"notify_fund" not in self.__dict__
1730-
and type(self).notify_fund is Strategy.notify_fund
1722+
"notify_fund" not in self.__dict__ and type(self).notify_fund is Strategy.notify_fund
17311723
)
17321724
self._skip_empty_notify = (
17331725
not self._quicknotify
@@ -1736,7 +1728,9 @@ def _start(self):
17361728
and self._notify_fund_default
17371729
)
17381730
self._fast_simple_next = (
1739-
not self._lineiterators[LineIterator.IndType]
1731+
len(self.datas) == 1
1732+
and len(self._minperiods) == 1
1733+
and not self._lineiterators[LineIterator.IndType]
17401734
and not self._lineaction_datas
17411735
and not self._has_strategy_next_lineactions
17421736
and self._skip_empty_notify

0 commit comments

Comments
 (0)