Skip to content

Commit 7617d6e

Browse files
authored
Merge branch 'master' into speed-improvements2
2 parents 28eca40 + 431ec32 commit 7617d6e

File tree

4 files changed

+25
-31
lines changed

4 files changed

+25
-31
lines changed

β€Žbacktesting/__init__.pyβ€Ž

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,17 @@
6767

6868
from . import lib # noqa: F401
6969
from ._plotting import set_bokeh_output # noqa: F401
70+
from ._util import try_
7071
from .backtesting import Backtest, Strategy # noqa: F401
7172

7273

7374
# Add overridable backtesting.Pool used for parallel optimization
7475
def Pool(processes=None, initializer=None, initargs=()):
7576
import multiprocessing as mp
77+
import sys
78+
# Revert performance related change in Python>=3.14
79+
if sys.platform.startswith('linux') and mp.get_start_method(allow_none=True) != 'fork':
80+
try_(lambda: mp.set_start_method('fork'))
7681
if mp.get_start_method() == 'spawn':
7782
import warnings
7883
warnings.warn(

β€Žbacktesting/_stats.pyβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def _round_timedelta(value, _period=_data_period(index)):
108108
])
109109

110110
have_position = np.repeat(0, len(index))
111-
for t in trades_df.itertuples(index=False):
111+
for t in trades_df[['EntryBar', 'ExitBar']].itertuples(index=False):
112112
have_position[t.EntryBar:t.ExitBar + 1] = 1
113113

114114
exposure_time_pct = have_position.mean() * 100 # In "n bars" time, not index time

β€Žbacktesting/backtesting.pyβ€Ž

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import warnings
1313
from abc import ABCMeta, abstractmethod
1414
from copy import copy
15+
from difflib import get_close_matches
1516
from functools import cached_property, lru_cache, partial
1617
from itertools import chain, product, repeat
1718
from math import copysign
@@ -64,10 +65,12 @@ def __str__(self):
6465
def _check_params(self, params):
6566
for k, v in params.items():
6667
if not hasattr(self, k):
68+
suggestions = get_close_matches(k, (attr for attr in dir(self) if not attr.startswith('_')))
69+
hint = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
6770
raise AttributeError(
68-
f"Strategy '{self.__class__.__name__}' is missing parameter '{k}'."
71+
f"Strategy '{self.__class__.__name__}' is missing parameter '{k}'. "
6972
"Strategy class should define parameters as class variables before they "
70-
"can be optimized or run with.")
73+
"can be optimized or run with." + hint)
7174
setattr(self, k, v)
7275
return params
7376

@@ -309,7 +312,7 @@ def position(self) -> 'Position':
309312
@property
310313
def orders(self) -> 'Tuple[Order, ...]':
311314
"""List of orders (see `Order`) waiting for execution."""
312-
return _Orders(self._broker.orders)
315+
return tuple(self._broker.orders)
313316

314317
@property
315318
def trades(self) -> 'Tuple[Trade, ...]':
@@ -322,27 +325,6 @@ def closed_trades(self) -> 'Tuple[Trade, ...]':
322325
return tuple(self._broker.closed_trades)
323326

324327

325-
class _Orders(tuple):
326-
"""
327-
TODO: remove this class. Only for deprecation.
328-
"""
329-
def cancel(self):
330-
"""Cancel all non-contingent (i.e. SL/TP) orders."""
331-
for order in self:
332-
if not order.is_contingent:
333-
order.cancel()
334-
335-
def __getattr__(self, item):
336-
# TODO: Warn on deprecations from the previous version. Remove in the next.
337-
removed_attrs = ('entry', 'set_entry', 'is_long', 'is_short',
338-
'sl', 'tp', 'set_sl', 'set_tp')
339-
if item in removed_attrs:
340-
raise AttributeError(f'Strategy.orders.{"/.".join(removed_attrs)} were removed in'
341-
'Backtesting 0.2.0. '
342-
'Use `Order` API instead. See docs.')
343-
raise AttributeError(f"'tuple' object has no attribute {item!r}")
344-
345-
346328
class Position:
347329
"""
348330
Currently held asset position, available as
@@ -998,8 +980,9 @@ def _process_orders(self):
998980
# Not enough cash/margin even for a single unit
999981
if not size:
1000982
warnings.warn(
1001-
f'time={self._i}: Broker canceled the relative-sized '
1002-
f'order due to insufficient margin.', category=UserWarning)
983+
f'time={self._i}: Broker canceled the relative-sized order due to insufficient margin '
984+
f'(equity={self.equity:.2f}, margin_available={self.margin_available:.2f}).',
985+
category=UserWarning)
1003986
# XXX: The order is canceled by the broker?
1004987
self.orders.remove(order)
1005988
continue
@@ -1032,6 +1015,10 @@ def _process_orders(self):
10321015
# If we don't have enough liquidity to cover for the order, the broker CANCELS it
10331016
if abs(need_size) * adjusted_price_plus_commission > \
10341017
self.margin_available * self._leverage:
1018+
warnings.warn(
1019+
f'time={self._i}: Broker canceled the order due to insufficient margin '
1020+
f'(equity={self.equity:.2f}, margin_available={self.margin_available:.2f}).',
1021+
category=UserWarning)
10351022
self.orders.remove(order)
10361023
continue
10371024

@@ -1174,7 +1161,7 @@ class Backtest:
11741161
11751162
`cash` is the initial cash to start with.
11761163
1177-
`spread` is the the constant bid-ask spread rate (relative to the price).
1164+
`spread` is the constant bid-ask spread rate (relative to the price).
11781165
E.g. set it to `0.0002` for commission-less forex
11791166
trading where the average spread is roughly 0.2‰ of the asking price.
11801167
@@ -1204,7 +1191,7 @@ class Backtest:
12041191
12051192
`margin` is the required margin (ratio) of a leveraged account.
12061193
No difference is made between initial and maintenance margins.
1207-
To run the backtest using e.g. 50:1 leverge that your broker allows,
1194+
To run the backtest using e.g. 50:1 leverage that your broker allows,
12081195
set margin to `0.02` (1 / leverage).
12091196
12101197
If `trade_on_close` is `True`, market orders will be filled

β€Žbacktesting/test/_test.pyβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@ def next(self, _FEW_DAYS=pd.Timedelta('3 days')): # noqa: N803
173173
self.position.is_long
174174

175175
if crossover(self.sma, self.data.Close):
176-
self.orders.cancel() # cancels only non-contingent
177176
price = self.data.Close[-1]
178177
sl, tp = 1.05 * price, .9 * price
179178

@@ -1039,10 +1038,13 @@ class TestDocs(TestCase):
10391038
DOCS_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'doc')
10401039

10411040
@unittest.skipUnless(os.path.isdir(DOCS_DIR), "docs dir doesn't exist")
1041+
@unittest.skipUnless(sys.platform.startswith('linux'), "test_examples requires mp.start_method=fork")
10421042
def test_examples(self):
1043+
import backtesting
10431044
examples = glob(os.path.join(self.DOCS_DIR, 'examples', '*.py'))
10441045
self.assertGreaterEqual(len(examples), 4)
1045-
with chdir(gettempdir()):
1046+
with chdir(gettempdir()), \
1047+
patch(backtesting, 'Pool', mp.get_context('fork').Pool):
10461048
for file in examples:
10471049
with self.subTest(example=os.path.basename(file)):
10481050
run_path(file)

0 commit comments

Comments
Β (0)