|
8 | 8 |
|
9 | 9 | from __future__ import annotations
|
10 | 10 |
|
11 |
| -import multiprocessing as mp |
12 |
| -import os |
13 | 11 | import sys
|
14 | 12 | import warnings
|
15 | 13 | from abc import ABCMeta, abstractmethod
|
16 |
| -from concurrent.futures import ProcessPoolExecutor, as_completed |
17 | 14 | from copy import copy
|
18 | 15 | from functools import lru_cache, partial
|
19 | 16 | from itertools import chain, product, repeat
|
|
23 | 20 |
|
24 | 21 | import numpy as np
|
25 | 22 | import pandas as pd
|
| 23 | +from joblib import Parallel, delayed |
26 | 24 | from numpy.random import default_rng
|
27 | 25 |
|
28 | 26 | try:
|
@@ -1497,41 +1495,15 @@ def _optimize_grid() -> Union[pd.Series, Tuple[pd.Series, pd.Series]]:
|
1497 | 1495 | [p.values() for p in param_combos],
|
1498 | 1496 | names=next(iter(param_combos)).keys()))
|
1499 | 1497 |
|
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] |
| 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 |
1535 | 1507 |
|
1536 | 1508 | if pd.isnull(heatmap).all():
|
1537 | 1509 | # No trade was made in any of the runs. Just make a random
|
@@ -1580,7 +1552,7 @@ def memoized_run(tup):
|
1580 | 1552 | stats = self.run(**dict(tup))
|
1581 | 1553 | return -maximize(stats)
|
1582 | 1554 |
|
1583 |
| - progress = iter(_tqdm(repeat(None), total=max_tries, leave=False, desc='Backtest.optimize')) |
| 1555 | + progress = iter(_tqdm(repeat(None), total=max_tries, desc='Backtest.optimize')) |
1584 | 1556 | _names = tuple(kwargs.keys())
|
1585 | 1557 |
|
1586 | 1558 | def objective_function(x):
|
@@ -1625,11 +1597,9 @@ def cons(x):
|
1625 | 1597 | return output
|
1626 | 1598 |
|
1627 | 1599 | @staticmethod
|
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])] |
| 1600 | + def _mp_task(bt, params, *, maximize): |
| 1601 | + stats = bt.run(**params) |
| 1602 | + return maximize(stats) if stats['# Trades'] else np.nan |
1633 | 1603 |
|
1634 | 1604 | _mp_backtests: Dict[float, Tuple['Backtest', List, Callable]] = {}
|
1635 | 1605 |
|
|
0 commit comments