|
8 | 8 |
|
9 | 9 | from __future__ import annotations
|
10 | 10 |
|
| 11 | +import multiprocessing as mp |
| 12 | +import os |
11 | 13 | import sys
|
12 | 14 | import warnings
|
13 | 15 | from abc import ABCMeta, abstractmethod
|
| 16 | +from concurrent.futures import ProcessPoolExecutor, as_completed |
14 | 17 | from copy import copy
|
15 | 18 | from functools import lru_cache, partial
|
16 | 19 | from itertools import chain, product, repeat
|
|
20 | 23 |
|
21 | 24 | import numpy as np
|
22 | 25 | import pandas as pd
|
23 |
| -from joblib import Parallel, delayed |
24 | 26 | from numpy.random import default_rng
|
25 | 27 |
|
26 | 28 | try:
|
@@ -1495,15 +1497,41 @@ def _optimize_grid() -> Union[pd.Series, Tuple[pd.Series, pd.Series]]:
|
1495 | 1497 | [p.values() for p in param_combos],
|
1496 | 1498 | names=next(iter(param_combos)).keys()))
|
1497 | 1499 |
|
1498 |
| - with Parallel(prefer='threads', require='sharedmem', max_nbytes='50M', |
1499 |
| - n_jobs=-2, return_as='generator') as parallel: |
1500 |
| - results = _tqdm( |
1501 |
| - parallel(delayed(self._mp_task)(self, params, maximize=maximize) |
1502 |
| - for params in param_combos), |
1503 |
| - total=len(param_combos), |
1504 |
| - desc='Backtest.optimize') |
1505 |
| - for value, params in zip(results, param_combos): |
1506 |
| - heatmap[tuple(params.values())] = value |
| 1500 | + def _batch(seq): |
| 1501 | + n = np.clip(int(len(seq) // (os.cpu_count() or 1)), 1, 300) |
| 1502 | + for i in range(0, len(seq), n): |
| 1503 | + yield seq[i:i + n] |
| 1504 | + |
| 1505 | + # Save necessary objects into "global" state; pass into concurrent executor |
| 1506 | + # (and thus pickle) nothing but two numbers; receive nothing but numbers. |
| 1507 | + # With start method "fork", children processes will inherit parent address space |
| 1508 | + # in a copy-on-write manner, achieving better performance/RAM benefit. |
| 1509 | + backtest_uuid = np.random.random() |
| 1510 | + param_batches = list(_batch(param_combos)) |
| 1511 | + Backtest._mp_backtests[backtest_uuid] = (self, param_batches, maximize) |
| 1512 | + try: |
| 1513 | + # If multiprocessing start method is 'fork' (i.e. on POSIX), use |
| 1514 | + # a pool of processes to compute results in parallel. |
| 1515 | + # Otherwise (i.e. on Windos), sequential computation will be "faster". |
| 1516 | + if mp.get_start_method(allow_none=False) == 'fork': |
| 1517 | + with ProcessPoolExecutor() as executor: |
| 1518 | + futures = [executor.submit(Backtest._mp_task, backtest_uuid, i) |
| 1519 | + for i in range(len(param_batches))] |
| 1520 | + for future in _tqdm(as_completed(futures), total=len(futures), |
| 1521 | + desc='Backtest.optimize'): |
| 1522 | + batch_index, values = future.result() |
| 1523 | + for value, params in zip(values, param_batches[batch_index]): |
| 1524 | + heatmap[tuple(params.values())] = value |
| 1525 | + else: |
| 1526 | + if os.name == 'posix': |
| 1527 | + warnings.warn("For multiprocessing support in `Backtest.optimize()` " |
| 1528 | + "set multiprocessing start method to 'fork'.") |
| 1529 | + for batch_index in _tqdm(range(len(param_batches))): |
| 1530 | + _, values = Backtest._mp_task(backtest_uuid, batch_index) |
| 1531 | + for value, params in zip(values, param_batches[batch_index]): |
| 1532 | + heatmap[tuple(params.values())] = value |
| 1533 | + finally: |
| 1534 | + del Backtest._mp_backtests[backtest_uuid] |
1507 | 1535 |
|
1508 | 1536 | if pd.isnull(heatmap).all():
|
1509 | 1537 | # No trade was made in any of the runs. Just make a random
|
@@ -1552,7 +1580,7 @@ def memoized_run(tup):
|
1552 | 1580 | stats = self.run(**dict(tup))
|
1553 | 1581 | return -maximize(stats)
|
1554 | 1582 |
|
1555 |
| - progress = iter(_tqdm(repeat(None), total=max_tries, desc='Backtest.optimize')) |
| 1583 | + progress = iter(_tqdm(repeat(None), total=max_tries, leave=False, desc='Backtest.optimize')) |
1556 | 1584 | _names = tuple(kwargs.keys())
|
1557 | 1585 |
|
1558 | 1586 | def objective_function(x):
|
@@ -1597,9 +1625,11 @@ def cons(x):
|
1597 | 1625 | return output
|
1598 | 1626 |
|
1599 | 1627 | @staticmethod
|
1600 |
| - def _mp_task(bt, params, *, maximize): |
1601 |
| - stats = bt.run(**params) |
1602 |
| - return maximize(stats) if stats['# Trades'] else np.nan |
| 1628 | + def _mp_task(backtest_uuid, batch_index): |
| 1629 | + bt, param_batches, maximize_func = Backtest._mp_backtests[backtest_uuid] |
| 1630 | + return batch_index, [maximize_func(stats) if stats['# Trades'] else np.nan |
| 1631 | + for stats in (bt.run(**params) |
| 1632 | + for params in param_batches[batch_index])] |
1603 | 1633 |
|
1604 | 1634 | _mp_backtests: Dict[float, Tuple['Backtest', List, Callable]] = {}
|
1605 | 1635 |
|
|
0 commit comments