lumibot 4.1.3__py3-none-any.whl → 4.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lumibot might be problematic. Click here for more details.
- lumibot/backtesting/__init__.py +19 -5
- lumibot/backtesting/backtesting_broker.py +98 -18
- lumibot/backtesting/databento_backtesting.py +5 -686
- lumibot/backtesting/databento_backtesting_pandas.py +738 -0
- lumibot/backtesting/databento_backtesting_polars.py +860 -546
- lumibot/backtesting/fix_debug.py +37 -0
- lumibot/backtesting/thetadata_backtesting.py +9 -355
- lumibot/backtesting/thetadata_backtesting_pandas.py +1167 -0
- lumibot/brokers/alpaca.py +8 -1
- lumibot/brokers/schwab.py +12 -2
- lumibot/credentials.py +13 -0
- lumibot/data_sources/__init__.py +5 -8
- lumibot/data_sources/data_source.py +6 -2
- lumibot/data_sources/data_source_backtesting.py +30 -0
- lumibot/data_sources/databento_data.py +5 -390
- lumibot/data_sources/databento_data_pandas.py +440 -0
- lumibot/data_sources/databento_data_polars.py +15 -9
- lumibot/data_sources/pandas_data.py +30 -17
- lumibot/data_sources/polars_data.py +986 -0
- lumibot/data_sources/polars_mixin.py +472 -96
- lumibot/data_sources/polygon_data_polars.py +5 -0
- lumibot/data_sources/yahoo_data.py +9 -2
- lumibot/data_sources/yahoo_data_polars.py +5 -0
- lumibot/entities/__init__.py +15 -0
- lumibot/entities/asset.py +5 -28
- lumibot/entities/bars.py +89 -20
- lumibot/entities/data.py +29 -6
- lumibot/entities/data_polars.py +668 -0
- lumibot/entities/position.py +38 -4
- lumibot/strategies/_strategy.py +2 -1
- lumibot/strategies/strategy.py +61 -49
- lumibot/tools/backtest_cache.py +284 -0
- lumibot/tools/databento_helper.py +35 -35
- lumibot/tools/databento_helper_polars.py +738 -775
- lumibot/tools/futures_roll.py +251 -0
- lumibot/tools/indicators.py +135 -104
- lumibot/tools/polars_utils.py +142 -0
- lumibot/tools/thetadata_helper.py +1068 -134
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/METADATA +9 -1
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/RECORD +71 -147
- tests/backtest/test_databento.py +37 -6
- tests/backtest/test_databento_comprehensive_trading.py +8 -4
- tests/backtest/test_databento_parity.py +4 -2
- tests/backtest/test_debug_avg_fill_price.py +1 -1
- tests/backtest/test_example_strategies.py +11 -1
- tests/backtest/test_futures_edge_cases.py +3 -3
- tests/backtest/test_futures_single_trade.py +2 -2
- tests/backtest/test_futures_ultra_simple.py +2 -2
- tests/backtest/test_polars_lru_eviction.py +470 -0
- tests/backtest/test_yahoo.py +42 -0
- tests/test_asset.py +4 -4
- tests/test_backtest_cache_manager.py +149 -0
- tests/test_backtesting_data_source_env.py +6 -0
- tests/test_continuous_futures_resolution.py +60 -48
- tests/test_data_polars_parity.py +160 -0
- tests/test_databento_asset_validation.py +23 -5
- tests/test_databento_backtesting.py +1 -1
- tests/test_databento_backtesting_polars.py +312 -192
- tests/test_databento_data.py +220 -463
- tests/test_databento_live.py +10 -10
- tests/test_futures_roll.py +38 -0
- tests/test_indicator_subplots.py +101 -0
- tests/test_market_infinite_loop_bug.py +77 -3
- tests/test_polars_resample.py +67 -0
- tests/test_polygon_helper.py +46 -0
- tests/test_thetadata_backwards_compat.py +97 -0
- tests/test_thetadata_helper.py +222 -23
- tests/test_thetadata_pandas_verification.py +186 -0
- lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/__pycache__/constants.cpython-312.pyc +0 -0
- lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
- lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
- lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/WHEEL +0 -0
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/top_level.txt +0 -0
lumibot/backtesting/__init__.py
CHANGED
|
@@ -6,11 +6,25 @@ from .interactive_brokers_rest_backtesting import InteractiveBrokersRESTBacktest
|
|
|
6
6
|
from .pandas_backtesting import PandasDataBacktesting
|
|
7
7
|
from .polygon_backtesting import PolygonDataBacktesting
|
|
8
8
|
from .thetadata_backtesting import ThetaDataBacktesting
|
|
9
|
+
from .thetadata_backtesting_pandas import ThetaDataBacktestingPandas
|
|
9
10
|
from .yahoo_backtesting import YahooDataBacktesting
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from
|
|
12
|
+
from .databento_backtesting import DataBentoDataBacktesting
|
|
13
|
+
from .databento_backtesting_pandas import DataBentoDataBacktestingPandas
|
|
14
|
+
from .databento_backtesting_polars import DataBentoDataBacktestingPolars
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AlpacaBacktesting",
|
|
18
|
+
"AlphaVantageBacktesting",
|
|
19
|
+
"BacktestingBroker",
|
|
20
|
+
"CcxtBacktesting",
|
|
21
|
+
"InteractiveBrokersRESTBacktesting",
|
|
22
|
+
"PandasDataBacktesting",
|
|
23
|
+
"PolygonDataBacktesting",
|
|
24
|
+
"ThetaDataBacktesting",
|
|
25
|
+
"ThetaDataBacktestingPandas",
|
|
26
|
+
"YahooDataBacktesting",
|
|
27
|
+
"DataBentoDataBacktesting",
|
|
28
|
+
"DataBentoDataBacktestingPandas",
|
|
29
|
+
"DataBentoDataBacktestingPolars",
|
|
30
|
+
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import math
|
|
1
2
|
import traceback
|
|
2
3
|
import threading
|
|
3
4
|
from collections import OrderedDict
|
|
@@ -1353,17 +1354,21 @@ class BacktestingBroker(Broker):
|
|
|
1353
1354
|
|
|
1354
1355
|
# Get the OHLCV data for the asset if we're using the YAHOO, CCXT data source
|
|
1355
1356
|
data_source_name = self.data_source.SOURCE.upper()
|
|
1356
|
-
if data_source_name in ["CCXT", "YAHOO", "ALPACA", "DATABENTO"]:
|
|
1357
|
-
#
|
|
1357
|
+
if data_source_name in ["CCXT", "YAHOO", "ALPACA", "DATABENTO", "DATABENTO_POLARS"]:
|
|
1358
|
+
# Negative deltas here are intentional: _pull_source_symbol_bars subtracts the offset, so
|
|
1359
|
+
# passing -1 minute yields an effective +1 minute guard that keeps us on the previously
|
|
1360
|
+
# completed bar. See tests/*_lookahead for regression coverage.
|
|
1358
1361
|
timeshift = timedelta(minutes=-1)
|
|
1359
|
-
if data_source_name
|
|
1360
|
-
# DataBento
|
|
1362
|
+
if data_source_name in {"DATABENTO", "DATABENTO_POLARS"}:
|
|
1363
|
+
# DataBento feeds can skip minutes around maintenance windows. Giving it a two-minute
|
|
1364
|
+
# cushion mirrors the legacy Polygon behaviour and avoids falling through gaps.
|
|
1361
1365
|
timeshift = timedelta(minutes=-2)
|
|
1362
1366
|
elif data_source_name == "YAHOO":
|
|
1363
|
-
# Yahoo
|
|
1367
|
+
# Yahoo daily bars are stamped at the close (16:00). A one-day backstep keeps fills on
|
|
1368
|
+
# the previous session so we never peek at the in-progress bar.
|
|
1364
1369
|
timeshift = timedelta(days=-1)
|
|
1365
1370
|
elif data_source_name == "ALPACA":
|
|
1366
|
-
# Alpaca minute bars
|
|
1371
|
+
# Alpaca minute bars line up with our clock already; no offset needed.
|
|
1367
1372
|
timeshift = None
|
|
1368
1373
|
|
|
1369
1374
|
ohlc = self.data_source.get_historical_prices(
|
|
@@ -1373,6 +1378,23 @@ class BacktestingBroker(Broker):
|
|
|
1373
1378
|
timeshift=timeshift,
|
|
1374
1379
|
)
|
|
1375
1380
|
|
|
1381
|
+
if (
|
|
1382
|
+
ohlc is None
|
|
1383
|
+
or getattr(ohlc, "df", None) is None
|
|
1384
|
+
or (hasattr(ohlc.df, "empty") and ohlc.df.empty)
|
|
1385
|
+
):
|
|
1386
|
+
if strategy is not None:
|
|
1387
|
+
display_symbol = getattr(order.asset, "symbol", order.asset)
|
|
1388
|
+
order_identifier = getattr(order, "identifier", None)
|
|
1389
|
+
if order_identifier is None:
|
|
1390
|
+
order_identifier = getattr(order, "id", "<unknown>")
|
|
1391
|
+
strategy.log_message(
|
|
1392
|
+
f"[DIAG] No historical bars returned for {display_symbol} at {self.datetime}; "
|
|
1393
|
+
f"pending {order.order_type} id={order_identifier}",
|
|
1394
|
+
color="yellow",
|
|
1395
|
+
)
|
|
1396
|
+
continue
|
|
1397
|
+
|
|
1376
1398
|
# Handle both pandas and polars DataFrames
|
|
1377
1399
|
if hasattr(ohlc.df, 'index'): # pandas
|
|
1378
1400
|
dt = ohlc.df.index[-1]
|
|
@@ -1406,6 +1428,16 @@ class BacktestingBroker(Broker):
|
|
|
1406
1428
|
)
|
|
1407
1429
|
# Check if we got any ohlc data
|
|
1408
1430
|
if ohlc is None or ohlc.empty:
|
|
1431
|
+
if strategy is not None:
|
|
1432
|
+
display_symbol = getattr(order.asset, "symbol", order.asset)
|
|
1433
|
+
order_identifier = getattr(order, "identifier", None)
|
|
1434
|
+
if order_identifier is None:
|
|
1435
|
+
order_identifier = getattr(order, "id", "<unknown>")
|
|
1436
|
+
strategy.log_message(
|
|
1437
|
+
f"[DIAG] No pandas bars for {display_symbol} at {self.datetime}; "
|
|
1438
|
+
f"canceling {order.order_type} id={order_identifier}",
|
|
1439
|
+
color="yellow",
|
|
1440
|
+
)
|
|
1409
1441
|
self.cancel_order(order)
|
|
1410
1442
|
continue
|
|
1411
1443
|
|
|
@@ -1502,41 +1534,89 @@ class BacktestingBroker(Broker):
|
|
|
1502
1534
|
strategy=strategy,
|
|
1503
1535
|
)
|
|
1504
1536
|
else:
|
|
1537
|
+
if strategy is not None:
|
|
1538
|
+
display_symbol = getattr(order.asset, "symbol", order.asset)
|
|
1539
|
+
order_identifier = getattr(order, "identifier", None)
|
|
1540
|
+
if order_identifier is None:
|
|
1541
|
+
order_identifier = getattr(order, "id", "<unknown>")
|
|
1542
|
+
detail = (
|
|
1543
|
+
f"limit={order.limit_price}, high={high}, low={low}"
|
|
1544
|
+
if order.order_type == Order.OrderType.LIMIT
|
|
1545
|
+
else f"type={order.order_type}, high={high}, low={low}, stop={getattr(order, 'stop_price', None)}"
|
|
1546
|
+
)
|
|
1547
|
+
strategy.log_message(
|
|
1548
|
+
f"[DIAG] Order remained open for {display_symbol} ({detail}) "
|
|
1549
|
+
f"id={order_identifier} at {self.datetime}",
|
|
1550
|
+
color="yellow",
|
|
1551
|
+
)
|
|
1505
1552
|
continue
|
|
1506
1553
|
|
|
1507
1554
|
# After handling all pending orders, cash settle any residual expired contracts.
|
|
1508
1555
|
self.process_expired_option_contracts(strategy)
|
|
1509
1556
|
|
|
1557
|
+
def _coerce_price(self, value):
|
|
1558
|
+
"""Convert numeric inputs to float when possible for safe comparisons."""
|
|
1559
|
+
if value is None:
|
|
1560
|
+
return None
|
|
1561
|
+
try:
|
|
1562
|
+
return float(value)
|
|
1563
|
+
except (TypeError, ValueError):
|
|
1564
|
+
return value
|
|
1565
|
+
|
|
1566
|
+
def _is_invalid_price(self, value):
|
|
1567
|
+
"""Determine whether a price is unusable (None or NaN)."""
|
|
1568
|
+
if value is None:
|
|
1569
|
+
return True
|
|
1570
|
+
if isinstance(value, float) and math.isnan(value):
|
|
1571
|
+
return True
|
|
1572
|
+
return False
|
|
1573
|
+
|
|
1510
1574
|
def limit_order(self, limit_price, side, open_, high, low):
|
|
1511
1575
|
"""Limit order logic."""
|
|
1576
|
+
open_val = self._coerce_price(open_)
|
|
1577
|
+
high_val = self._coerce_price(high)
|
|
1578
|
+
low_val = self._coerce_price(low)
|
|
1579
|
+
limit_val = self._coerce_price(limit_price)
|
|
1580
|
+
|
|
1581
|
+
if any(self._is_invalid_price(val) for val in (open_val, high_val, low_val, limit_val)):
|
|
1582
|
+
return None
|
|
1583
|
+
|
|
1512
1584
|
# Gap Up case: Limit wasn't triggered by previous candle but current candle opens higher, fill it now
|
|
1513
|
-
if side == "sell" and
|
|
1514
|
-
return
|
|
1585
|
+
if side == "sell" and limit_val <= open_val:
|
|
1586
|
+
return open_val
|
|
1515
1587
|
|
|
1516
1588
|
# Gap Down case: Limit wasn't triggered by previous candle but current candle opens lower, fill it now
|
|
1517
|
-
if side == "buy" and
|
|
1518
|
-
return
|
|
1589
|
+
if side == "buy" and limit_val >= open_val:
|
|
1590
|
+
return open_val
|
|
1519
1591
|
|
|
1520
1592
|
# Current candle triggered limit normally
|
|
1521
|
-
if
|
|
1522
|
-
return
|
|
1593
|
+
if low_val <= limit_val <= high_val:
|
|
1594
|
+
return limit_val
|
|
1523
1595
|
|
|
1524
1596
|
# Limit has not been met
|
|
1525
1597
|
return None
|
|
1526
1598
|
|
|
1527
1599
|
def stop_order(self, stop_price, side, open_, high, low):
|
|
1528
1600
|
"""Stop order logic."""
|
|
1601
|
+
open_val = self._coerce_price(open_)
|
|
1602
|
+
high_val = self._coerce_price(high)
|
|
1603
|
+
low_val = self._coerce_price(low)
|
|
1604
|
+
stop_val = self._coerce_price(stop_price)
|
|
1605
|
+
|
|
1606
|
+
if any(self._is_invalid_price(val) for val in (open_val, high_val, low_val, stop_val)):
|
|
1607
|
+
return None
|
|
1608
|
+
|
|
1529
1609
|
# Gap Down case: Stop wasn't triggered by previous candle but current candle opens lower, fill it now
|
|
1530
|
-
if side == "sell" and
|
|
1531
|
-
return
|
|
1610
|
+
if side == "sell" and stop_val >= open_val:
|
|
1611
|
+
return open_val
|
|
1532
1612
|
|
|
1533
1613
|
# Gap Up case: Stop wasn't triggered by previous candle but current candle opens higher, fill it now
|
|
1534
|
-
if side == "buy" and
|
|
1535
|
-
return
|
|
1614
|
+
if side == "buy" and stop_val <= open_val:
|
|
1615
|
+
return open_val
|
|
1536
1616
|
|
|
1537
1617
|
# Current candle triggered stop normally
|
|
1538
|
-
if
|
|
1539
|
-
return
|
|
1618
|
+
if low_val <= stop_val <= high_val:
|
|
1619
|
+
return stop_val
|
|
1540
1620
|
|
|
1541
1621
|
# Stop has not been met
|
|
1542
1622
|
return None
|