lumibot 4.2.9__py3-none-any.whl → 4.2.10__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.

@@ -24,8 +24,13 @@ except ImportError: # pragma: no cover - optional dependency
24
24
  from .data_source import DataSource
25
25
  from .polars_mixin import PolarsMixin
26
26
  from lumibot.entities import Asset, Bars, Quote
27
- from lumibot.tools import databento_helper_polars
28
- from lumibot.tools.databento_helper_polars import _ensure_polars_datetime_timezone as _ensure_polars_tz
27
+ from lumibot.tools import databento_helper_polars, futures_roll
28
+ from lumibot.tools.databento_helper_polars import (
29
+ _ensure_polars_datetime_timezone as _ensure_polars_tz,
30
+ _ensure_polars_datetime_precision as _ensure_polars_precision,
31
+ _format_futures_symbol_for_databento,
32
+ _generate_databento_symbol_alternatives,
33
+ )
29
34
  from lumibot.tools.lumibot_logger import get_logger
30
35
 
31
36
  logger = get_logger(__name__)
@@ -455,6 +460,7 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
455
460
 
456
461
  df = pl.DataFrame(tail_bars).sort('datetime')
457
462
  df = _ensure_polars_tz(df)
463
+ df = _ensure_polars_precision(df)
458
464
  logger.debug(f"[DATABENTO][LIVE] Collected {len(df)} tail bars after {after_dt}")
459
465
  return df
460
466
 
@@ -512,38 +518,30 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
512
518
 
513
519
  def _resolve_futures_symbol(self, asset: Asset, reference_date: datetime = None) -> str:
514
520
  """Resolve asset to specific futures contract symbol"""
515
- if asset.asset_type in [Asset.AssetType.FUTURE, Asset.AssetType.CONT_FUTURE]:
516
- # For continuous futures, resolve to specific contract
517
- if asset.asset_type == Asset.AssetType.CONT_FUTURE:
518
- if hasattr(asset, 'resolve_continuous_futures_contract'):
519
- return asset.resolve_continuous_futures_contract(
520
- reference_date=reference_date,
521
- year_digits=1,
522
- )
523
-
524
- # Manual resolution for common futures
525
- symbol = asset.symbol.upper()
526
- month = reference_date.month if reference_date else datetime.now().month
527
- year = reference_date.year if reference_date else datetime.now().year
528
-
529
- # Quarterly contracts
530
- if month <= 3:
531
- month_code = 'H'
532
- elif month <= 6:
533
- month_code = 'M'
534
- elif month <= 9:
535
- month_code = 'U'
536
- else:
537
- month_code = 'Z'
538
-
539
- year_digit = year % 10
540
-
541
- if symbol in ["ES", "NQ", "RTY", "YM", "MES", "MNQ", "MYM", "M2K", "CL", "GC", "SI"]:
542
- return f"{symbol}{month_code}{year_digit}"
543
-
521
+ if asset.asset_type not in [Asset.AssetType.FUTURE, Asset.AssetType.CONT_FUTURE]:
544
522
  return asset.symbol
545
-
546
- return asset.symbol
523
+
524
+ ref_dt = reference_date or datetime.now(timezone.utc)
525
+
526
+ if asset.asset_type == Asset.AssetType.FUTURE and asset.expiration:
527
+ return _format_futures_symbol_for_databento(asset, reference_date=reference_date)
528
+
529
+ if asset.asset_type == Asset.AssetType.CONT_FUTURE:
530
+ resolved_contract = futures_roll.resolve_symbol_for_datetime(
531
+ asset,
532
+ ref_dt,
533
+ year_digits=2,
534
+ )
535
+ else:
536
+ temp_asset = Asset(asset.symbol, Asset.AssetType.CONT_FUTURE)
537
+ resolved_contract = futures_roll.resolve_symbol_for_datetime(
538
+ temp_asset,
539
+ ref_dt,
540
+ year_digits=2,
541
+ )
542
+
543
+ databento_symbol = _generate_databento_symbol_alternatives(asset.symbol, resolved_contract)
544
+ return databento_symbol[0] if databento_symbol else resolved_contract
547
545
 
548
546
  def get_historical_prices(
549
547
  self,
@@ -633,6 +631,8 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
633
631
 
634
632
  df = _ensure_polars_tz(df)
635
633
  tail_df = _ensure_polars_tz(tail_df)
634
+ df = _ensure_polars_precision(df)
635
+ tail_df = _ensure_polars_precision(tail_df)
636
636
 
637
637
  # Only keep columns that exist in both dataframes
638
638
  common_columns = [col for col in df.columns if col in tail_df.columns]
@@ -678,9 +678,10 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
678
678
  except Exception as e:
679
679
  logger.warning(f"[DATABENTO][MERGE] Failed to merge live tail: {e}")
680
680
 
681
- # Trim to requested length
681
+ # Trim to requested length and normalize datetime metadata
682
682
  df = df.tail(length)
683
683
  df = _ensure_polars_tz(df)
684
+ df = _ensure_polars_precision(df)
684
685
  return Bars(
685
686
  df=df,
686
687
  source=self.SOURCE,
lumibot/entities/asset.py CHANGED
@@ -702,6 +702,17 @@ class Asset:
702
702
  if c not in seen:
703
703
  seen.add(c)
704
704
  unique.append(c)
705
+
706
+ # Ensure the resolved primary contract appears first so downstream
707
+ # consumers try the preferred contract before fallbacks.
708
+ try:
709
+ primary = self.resolve_continuous_futures_contract(reference_date)
710
+ except ValueError:
711
+ primary = None
712
+ if primary and primary in unique:
713
+ unique.remove(primary)
714
+ unique.insert(0, primary)
715
+
705
716
  return unique
706
717
 
707
718
  def _determine_continuous_contract_components(
@@ -1830,6 +1830,21 @@ class Strategy(_Strategy):
1830
1830
  >>> self.cancel_open_orders()
1831
1831
 
1832
1832
  """
1833
+ try:
1834
+ tracked_orders = self.broker.get_tracked_orders(self.name)
1835
+ active_orders = [order for order in tracked_orders if order.is_active()]
1836
+ order_ids = [
1837
+ getattr(order, "identifier", None)
1838
+ or getattr(order, "id", None)
1839
+ or getattr(order, "order_id", None)
1840
+ for order in active_orders
1841
+ ]
1842
+ self.log_message(
1843
+ f"cancel_open_orders -> active={len(active_orders)} ids={order_ids}",
1844
+ color="yellow",
1845
+ )
1846
+ except Exception as exc: # pragma: no cover - defensive logging
1847
+ self.logger.exception("Failed to enumerate open orders before cancellation: %s", exc)
1833
1848
  return self.broker.cancel_open_orders(self.name)
1834
1849
 
1835
1850
  def modify_order(self, order: Order, limit_price: Union[float, None] = None, stop_price: Union[float, None] = None):
@@ -1917,7 +1932,37 @@ class Strategy(_Strategy):
1917
1932
  - If no open position exists, this method does nothing.
1918
1933
  """
1919
1934
  asset_obj = self._sanitize_user_asset(asset)
1920
- result = self.broker.close_position(self.name, asset_obj, fraction)
1935
+
1936
+ try:
1937
+ position = self.get_position(asset_obj)
1938
+ qty = getattr(position, "quantity", 0) if position else 0
1939
+ self.log_message(
1940
+ f"close_position -> asset={asset_obj.symbol if hasattr(asset_obj, 'symbol') else asset_obj}, "
1941
+ f"fraction={fraction}, current_qty={qty}",
1942
+ color="yellow",
1943
+ )
1944
+ except Exception as exc: # pragma: no cover - defensive logging
1945
+ self.logger.exception("Unable to inspect position prior to close_position: %s", exc)
1946
+
1947
+ target_asset = getattr(position, "asset", asset_obj) if position is not None else asset_obj
1948
+ if target_asset is not asset_obj:
1949
+ try:
1950
+ self.log_message(
1951
+ f"close_position -> resolved continuous asset {asset_obj.symbol if hasattr(asset_obj, 'symbol') else asset_obj} "
1952
+ f"to contract {target_asset.symbol if hasattr(target_asset, 'symbol') else target_asset}",
1953
+ color="yellow",
1954
+ )
1955
+ except Exception: # pragma: no cover - best effort logging
1956
+ pass
1957
+ result = self.broker.close_position(self.name, target_asset, fraction)
1958
+ if result is None:
1959
+ self.log_message("close_position -> broker returned None (no order submitted)", color="yellow")
1960
+ else:
1961
+ order_id = getattr(result, "identifier", None) or getattr(result, "id", None) or getattr(result, "order_id", None)
1962
+ self.log_message(
1963
+ f"close_position -> broker submitted order {order_id} (type={type(result).__name__})",
1964
+ color="yellow",
1965
+ )
1921
1966
  if result is not None:
1922
1967
  return result
1923
1968
 
@@ -784,6 +784,22 @@ def _fetch_and_update_futures_multiplier(
784
784
  logger.info(f"[MULTIPLIER] AFTER update: asset.multiplier = {asset.multiplier}")
785
785
  else:
786
786
  logger.error(f"[MULTIPLIER] ✗ Definition missing unit_of_measure_qty field! Fields: {list(definition.keys())}")
787
+
788
+ if (
789
+ asset.asset_type == Asset.AssetType.FUTURE
790
+ and getattr(asset, "expiration", None) in (None, "")
791
+ ):
792
+ expiration_value = definition.get('expiration')
793
+ if expiration_value:
794
+ try:
795
+ expiration_ts = pd.to_datetime(expiration_value, utc=True, errors='coerce')
796
+ except Exception as exc:
797
+ logger.debug(f"[MULTIPLIER] Unable to parse expiration '{expiration_value}' for {asset.symbol}: {exc}")
798
+ expiration_ts = None
799
+
800
+ if expiration_ts is not None and not pd.isna(expiration_ts):
801
+ asset.expiration = expiration_ts.date()
802
+ logger.debug(f"[MULTIPLIER] ✓ Captured expiration for {asset.symbol}: {asset.expiration}")
787
803
  else:
788
804
  logger.error(f"[MULTIPLIER] ✗ Failed to get definition from DataBento for {resolved_symbol}")
789
805
 
@@ -863,6 +863,22 @@ def _fetch_and_update_futures_multiplier(
863
863
  logger.debug(f"[MULTIPLIER] AFTER update: asset.multiplier = {asset.multiplier}")
864
864
  else:
865
865
  logger.error(f"[MULTIPLIER] ✗ Definition missing unit_of_measure_qty field! Fields: {list(definition.keys())}")
866
+
867
+ if (
868
+ asset.asset_type == Asset.AssetType.FUTURE
869
+ and getattr(asset, "expiration", None) in (None, "")
870
+ ):
871
+ expiration_value = definition.get('expiration')
872
+ if expiration_value:
873
+ try:
874
+ expiration_ts = pd.to_datetime(expiration_value, utc=True, errors='coerce')
875
+ except Exception as exc:
876
+ logger.debug(f"[MULTIPLIER] Unable to parse expiration '{expiration_value}' for {asset.symbol}: {exc}")
877
+ expiration_ts = None
878
+
879
+ if expiration_ts is not None and not pd.isna(expiration_ts):
880
+ asset.expiration = expiration_ts.date()
881
+ logger.debug(f"[MULTIPLIER] ✓ Captured expiration for {asset.symbol}: {asset.expiration}")
866
882
  else:
867
883
  logger.error(f"[MULTIPLIER] ✗ Failed to get definition from DataBento for {resolved_symbol}")
868
884
 
@@ -1046,8 +1062,10 @@ def get_price_data_from_databento(
1046
1062
  logger.warning(f"No datetime column found after reset_index, using first column: {first_col}")
1047
1063
  combined_reset = combined_reset.rename(columns={first_col: 'datetime'})
1048
1064
 
1049
- # Convert to polars
1065
+ # Convert to polars and normalize datetime metadata
1050
1066
  combined_polars = pl.from_pandas(combined_reset)
1067
+ combined_polars = _ensure_polars_datetime_timezone(combined_polars)
1068
+ combined_polars = _ensure_polars_datetime_precision(combined_polars)
1051
1069
 
1052
1070
  return combined_polars
1053
1071
 
@@ -1239,6 +1257,8 @@ def _generate_databento_symbol_alternatives(base_symbol: str, resolved_contract:
1239
1257
  # Fallback for unexpected contract format - use original contract
1240
1258
  logger.warning(f"Unexpected contract format: {resolved_contract}, using as-is")
1241
1259
  return [resolved_contract]
1260
+
1261
+
1242
1262
  def _ensure_polars_datetime_timezone(df: pl.DataFrame, column: str = "datetime") -> pl.DataFrame:
1243
1263
  """Ensure the specified datetime column is timezone-aware (defaults to UTC)."""
1244
1264
  if column not in df.columns:
@@ -1251,6 +1271,26 @@ def _ensure_polars_datetime_timezone(df: pl.DataFrame, column: str = "datetime")
1251
1271
  return df
1252
1272
 
1253
1273
 
1274
+ def _ensure_polars_datetime_precision(
1275
+ df: pl.DataFrame,
1276
+ column: str = "datetime",
1277
+ time_unit: str = "ns",
1278
+ ) -> pl.DataFrame:
1279
+ """Normalize the precision of a datetime column (default nanosecond)."""
1280
+ if column not in df.columns:
1281
+ return df
1282
+ col_dtype = df.schema.get(column)
1283
+ if not isinstance(col_dtype, pl.Datetime):
1284
+ return df
1285
+ target_dtype = pl.Datetime(
1286
+ time_unit=time_unit,
1287
+ time_zone=col_dtype.time_zone or "UTC",
1288
+ )
1289
+ if col_dtype == target_dtype:
1290
+ return df
1291
+ return df.with_columns(pl.col(column).cast(target_dtype))
1292
+
1293
+
1254
1294
  def get_price_data_from_databento_polars(*args, **kwargs):
1255
1295
  """Compatibility helper that forces polars return type."""
1256
1296
  kwargs.setdefault("return_polars", True)
@@ -30,13 +30,32 @@ _FUTURES_MONTH_CODES: Dict[int, str] = {
30
30
  class RollRule:
31
31
  offset_business_days: int
32
32
  anchor: str
33
+ contract_months: Optional[Tuple[int, ...]] = None
34
+
35
+
36
+ _DEFAULT_CONTRACT_MONTHS: Tuple[int, ...] = (3, 6, 9, 12)
33
37
 
34
38
 
35
39
  ROLL_RULES: Dict[str, RollRule] = {
36
- symbol: RollRule(offset_business_days=8, anchor="third_friday")
40
+ symbol: RollRule(offset_business_days=8, anchor="third_friday", contract_months=_DEFAULT_CONTRACT_MONTHS)
37
41
  for symbol in {"ES", "MES", "NQ", "MNQ", "YM", "MYM"}
38
42
  }
39
43
 
44
+ ROLL_RULES.update(
45
+ {
46
+ "GC": RollRule(
47
+ offset_business_days=7,
48
+ anchor="third_last_business_day",
49
+ contract_months=(2, 4, 6, 8, 10, 12),
50
+ ),
51
+ "SI": RollRule(
52
+ offset_business_days=7,
53
+ anchor="third_last_business_day",
54
+ contract_months=(1, 3, 5, 7, 9, 12),
55
+ ),
56
+ }
57
+ )
58
+
40
59
  YearMonth = Tuple[int, int]
41
60
 
42
61
 
@@ -72,9 +91,32 @@ def _subtract_business_days(dt: datetime, days: int) -> datetime:
72
91
  return result
73
92
 
74
93
 
94
+ def _third_last_business_day(year: int, month: int) -> datetime:
95
+ if month == 12:
96
+ next_month = 1
97
+ next_year = year + 1
98
+ else:
99
+ next_month = month + 1
100
+ next_year = year
101
+
102
+ last_day = _to_timezone(datetime(next_year, next_month, 1)) - timedelta(days=1)
103
+
104
+ remaining = 3
105
+ cursor = last_day
106
+ while remaining > 0:
107
+ if cursor.weekday() < 5:
108
+ remaining -= 1
109
+ if remaining == 0:
110
+ break
111
+ cursor -= timedelta(days=1)
112
+ return cursor.replace(hour=0, minute=0, second=0, microsecond=0)
113
+
114
+
75
115
  def _calculate_roll_trigger(year: int, month: int, rule: RollRule) -> datetime:
76
116
  if rule.anchor == "third_friday":
77
117
  anchor = _third_friday(year, month)
118
+ elif rule.anchor == "third_last_business_day":
119
+ anchor = _third_last_business_day(year, month)
78
120
  else:
79
121
  anchor = _to_timezone(datetime(year, month, 15))
80
122
  if rule.offset_business_days <= 0:
@@ -82,15 +124,28 @@ def _calculate_roll_trigger(year: int, month: int, rule: RollRule) -> datetime:
82
124
  return _subtract_business_days(anchor, rule.offset_business_days)
83
125
 
84
126
 
85
- def _advance_quarter(current_month: int, current_year: int) -> YearMonth:
86
- quarter_months = [3, 6, 9, 12]
87
- idx = quarter_months.index(current_month)
88
- next_idx = (idx + 1) % len(quarter_months)
89
- next_month = quarter_months[next_idx]
90
- next_year = current_year + (1 if next_idx == 0 else 0)
127
+ def _get_contract_months(rule: Optional[RollRule]) -> Tuple[int, ...]:
128
+ if rule and rule.contract_months:
129
+ return tuple(sorted(rule.contract_months))
130
+ return _DEFAULT_CONTRACT_MONTHS
131
+
132
+
133
+ def _advance_contract(current_month: int, current_year: int, months: Tuple[int, ...]) -> YearMonth:
134
+ months_sorted = tuple(sorted(months))
135
+ idx = months_sorted.index(current_month)
136
+ next_idx = (idx + 1) % len(months_sorted)
137
+ next_month = months_sorted[next_idx]
138
+ next_year = current_year + (1 if next_idx <= idx else 0)
91
139
  return next_year, next_month
92
140
 
93
141
 
142
+ def _select_contract(year: int, month: int, months: Tuple[int, ...]) -> YearMonth:
143
+ for candidate in sorted(months):
144
+ if month <= candidate:
145
+ return year, candidate
146
+ return year + 1, sorted(months)[0]
147
+
148
+
94
149
  def _legacy_mid_month(reference_date: datetime) -> YearMonth:
95
150
  quarter_months = [3, 6, 9, 12]
96
151
  year = reference_date.year
@@ -118,27 +173,22 @@ def determine_contract_year_month(symbol: str, reference_date: Optional[datetime
118
173
  ref = _normalize_reference_date(reference_date)
119
174
  symbol_upper = symbol.upper()
120
175
  rule = ROLL_RULES.get(symbol_upper)
121
-
122
- quarter_months = [3, 6, 9, 12]
123
176
  year = ref.year
124
177
  month = ref.month
125
178
 
126
179
  if rule is None:
127
180
  return _legacy_mid_month(ref)
128
181
 
129
- if month in quarter_months:
182
+ contract_months = _get_contract_months(rule)
183
+
184
+ if month in contract_months:
130
185
  target_year, target_month = year, month
131
- roll_point = _calculate_roll_trigger(target_year, target_month, rule)
132
- if ref >= roll_point:
133
- target_year, target_month = _advance_quarter(target_month, target_year)
134
186
  else:
135
- candidates = [m for m in quarter_months if m > month]
136
- if candidates:
137
- target_month = candidates[0]
138
- target_year = year
139
- else:
140
- target_month = quarter_months[0]
141
- target_year = year + 1
187
+ target_year, target_month = _select_contract(year, month, contract_months)
188
+
189
+ roll_point = _calculate_roll_trigger(target_year, target_month, rule)
190
+ if ref >= roll_point:
191
+ target_year, target_month = _advance_contract(target_month, target_year, contract_months)
142
192
 
143
193
  return target_year, target_month
144
194
 
@@ -201,6 +251,7 @@ def build_roll_schedule(asset, start: datetime, end: datetime, year_digits: int
201
251
 
202
252
  symbol_upper = asset.symbol.upper()
203
253
  rule = ROLL_RULES.get(symbol_upper)
254
+ contract_months = _get_contract_months(rule)
204
255
 
205
256
  schedule = []
206
257
  cursor = start
@@ -1605,6 +1605,9 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
1605
1605
  except ThetaDataConnectionError as exc:
1606
1606
  logger.error("Theta Data connection failed after supervised restarts: %s", exc)
1607
1607
  raise
1608
+ except ValueError:
1609
+ # Preserve deliberate ValueError signals (e.g., ThetaData error_type responses)
1610
+ raise
1608
1611
  except Exception as e:
1609
1612
  logger.warning(f"Exception during request (attempt {counter + 1}): {e}")
1610
1613
  check_connection(username=username, password=password, wait_for_connection=True)
@@ -1614,7 +1617,7 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
1614
1617
 
1615
1618
  counter += 1
1616
1619
  if counter > 1:
1617
- raise ThetaDataConnectionError("Unable to connect to Theta Data after repeated retries.")
1620
+ raise ValueError("Cannot connect to Theta Data!")
1618
1621
 
1619
1622
  # Store this page's response data
1620
1623
  page_count += 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumibot
3
- Version: 4.2.9
3
+ Version: 4.2.10
4
4
  Summary: Backtesting and Trading Library, Made by Lumiwealth
5
5
  Home-page: https://github.com/Lumiwealth/lumibot
6
6
  Author: Robert Grzesik
@@ -19,7 +19,7 @@ lumibot/backtesting/yahoo_backtesting.py,sha256=LT2524mGlrUSq1YSRnUqGW4-Xcq4USgR
19
19
  lumibot/brokers/__init__.py,sha256=MGWKHeH3mqseYRL7u-KX1Jp2x9EaFO4Ol8sfNSxzu1M,404
20
20
  lumibot/brokers/alpaca.py,sha256=VQ17idfqiEFb2JCqqdMGmbvF789L7_PpsCbudiFRzmg,61595
21
21
  lumibot/brokers/bitunix.py,sha256=hwcYC4goXsukSHSevb9W5irJz2lJt5tSgK2X5S0VyUs,34555
22
- lumibot/brokers/broker.py,sha256=01cBrBjeiQGT9LMdZ2EgrSYT-xyoKz9JhVZkQQr1QxU,71951
22
+ lumibot/brokers/broker.py,sha256=yCKQluRh52SGLWT3-41YQ8uO4GFS3M-AE1PzgWPfYpk,73249
23
23
  lumibot/brokers/ccxt.py,sha256=9F8YeEF9HBRGgcwJ9WTSb2pKRXlh_zUj-CeA1j4K77w,31434
24
24
  lumibot/brokers/example_broker.py,sha256=mjfBaPU8kJvLwigMKczSeFcmPYQIB5L1CkqvNGnvat4,8661
25
25
  lumibot/brokers/interactive_brokers.py,sha256=qOTvOLOk01_LnF7B-t_5gtmuDtXitqV_WUkAZYFdRLw,60526
@@ -28,7 +28,7 @@ lumibot/brokers/projectx.py,sha256=JT7ysIQ4ek-yZdNrmuZYA6aKZMinSh1GnraetDrsjh0,7
28
28
  lumibot/brokers/schwab.py,sha256=eiBEm-WXtzMZQhY1eyErNazkso8ONqJFtGNIcxdDOHE,91486
29
29
  lumibot/brokers/tradeovate.py,sha256=NBGw79aWWL0JlNF34EAJQ5dfB3HkiGuWhuSVQ5yg1ZI,22091
30
30
  lumibot/brokers/tradier.py,sha256=E45lj4LV-lrF3mKgtZtoYDXoetgbnFlsmYb5HI7bgbM,50863
31
- lumibot/brokers/tradovate.py,sha256=8YaadpvoJyA-IkK6BCENLvCRfYSMnG-uTxkvDB2Sd94,38668
31
+ lumibot/brokers/tradovate.py,sha256=88T0578ALiWfE7kB2axJj9Uy2vHX95OS4k89n4m-lbo,61998
32
32
  lumibot/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  lumibot/components/configs_helper.py,sha256=e4-2jBmjsIDeMQ9fbQ9j2U6WlHCe2z9bIQtJNlP_iEs,3066
34
34
  lumibot/components/drift_rebalancer_logic.py,sha256=KL1S9Su5gxibGzl2fw_DH8StzXvjFfFl23sOFGHgIZI,28653
@@ -48,7 +48,7 @@ lumibot/data_sources/data_source.py,sha256=TrOlzYGrwc8PGru0c0DFK5DkoAc9Q0GrIzWce
48
48
  lumibot/data_sources/data_source_backtesting.py,sha256=PPlfhuYzaqjs9IkS8CXMSBwgB4oEHoi6u3hWqjSiqCc,9619
49
49
  lumibot/data_sources/databento_data.py,sha256=PIv0SWB6WJbu9sfxfuifXXKjDFuuiSVJMzgORal8K0Y,332
50
50
  lumibot/data_sources/databento_data_pandas.py,sha256=H4T53OGZi1ymzMKW0F-BES9DIGFG7BLDW5iV_zeUaYA,16293
51
- lumibot/data_sources/databento_data_polars.py,sha256=uPzhHHKdF-tavo6mzgdab75n4ixPpNkPfA6e8AVgbvE,36670
51
+ lumibot/data_sources/databento_data_polars.py,sha256=9PqJ3OG9z_dKxRnCUzjWpvit5XPSJGhPSxExvVx-9L0,36790
52
52
  lumibot/data_sources/databento_data_polars_backtesting.py,sha256=0YA8O7qe8aTxdFcc9ALhUZSHUJH70PnNG0eK1ZpLZt0,25864
53
53
  lumibot/data_sources/databento_data_polars_live.py,sha256=NJc1nHU6P94uUZQWtQ0z5-bi1SRKCkW4-yCFmwZ5FDI,36479
54
54
  lumibot/data_sources/example_broker_data.py,sha256=WkCc3rLyOOI4GzLptUwhqeFRkEWQsM4y3tdzUbHPfik,2066
@@ -67,7 +67,7 @@ lumibot/data_sources/tradovate_data.py,sha256=TY1MCXVTqiJTAYKN_5oinFHuvn4q6P5Re1
67
67
  lumibot/data_sources/yahoo_data.py,sha256=dFgsUNEhLDY_Erimeczuz2LLrIB3abPPFJay6E9Czig,19132
68
68
  lumibot/data_sources/yahoo_data_polars.py,sha256=0JB7RFoaoVS4JKLegT_kXxbVOLP3MlXRgeqrqCoiw2o,14244
69
69
  lumibot/entities/__init__.py,sha256=-zmHEFGc5TfOl0CqHKOGUrSfnL_UZArW94r7HpJrc2Q,637
70
- lumibot/entities/asset.py,sha256=BwpgKjXpx5NGOb_aF34ZSvxZVN11SUQ0MW-GYAGozC4,28643
70
+ lumibot/entities/asset.py,sha256=VlW4iNW0ab6zCEIjx-SOWeNMh2ud1vVkOWDgfRiyuzY,29046
71
71
  lumibot/entities/bar.py,sha256=_2gFtBzdbNPassLf1SU9J1mYKCZDner2h52qHl8e0oA,6003
72
72
  lumibot/entities/bars.py,sha256=N5N6ouQdX8-w2h1sstXMczNUNdM6d12YukmqUgmxTQ4,24506
73
73
  lumibot/entities/chains.py,sha256=4P42AUtoPrRQHnPVj-g6mJ5W2nVf17ju3gXdavZcVyk,6281
@@ -106,7 +106,7 @@ lumibot/resources/conf.yaml,sha256=rjB9-10JP7saZ_edjX5bQDGfuc3amOQTUUUr-UiMpNA,5
106
106
  lumibot/strategies/__init__.py,sha256=jEZ95K5hG0f595EXYKWwL2_UsnWWk5Pug361PK2My2E,79
107
107
  lumibot/strategies/_strategy.py,sha256=3Z2MPz3jYgJdAphaFyoEI5OUs_6ClAJgRCrLWy-b2sg,111718
108
108
  lumibot/strategies/session_manager.py,sha256=Nze6UYNSPlCsf-tyHvtFqUeL44WSNHjwsKrIepvsyCY,12956
109
- lumibot/strategies/strategy.py,sha256=toPeL5oIVWmCxBNcfXqIuTCF_EeCfIVj425PrSYImCo,170021
109
+ lumibot/strategies/strategy.py,sha256=UKZdwiHKEKwRL7SOefskGUt6UZ9wcLAuSixKSnJUmCc,172351
110
110
  lumibot/strategies/strategy_executor.py,sha256=AnmXlKD2eMgKXs3TrD1u8T_Zsn_8GnG5KRcM_Pq-JBQ,70749
111
111
  lumibot/tools/__init__.py,sha256=oRRoK2NBkfnc0kueAfY0HrWVKgzRBO1hlglVMR4jr5M,1501
112
112
  lumibot/tools/alpaca_helpers.py,sha256=nhBS-sv28lZfIQ85szC9El8VHLrCw5a5KbsGOOEjm6w,3147
@@ -114,12 +114,12 @@ lumibot/tools/backtest_cache.py,sha256=A-Juzu0swZI_FP4U7cd7ruYTgJYgV8BPf_WJDI-Nt
114
114
  lumibot/tools/bitunix_helpers.py,sha256=-UzrN3w_Y-Ckvhl7ZBoAcx7sgb6tH0KcpVph1Ovm3gw,25780
115
115
  lumibot/tools/black_scholes.py,sha256=TBjJuDTudvqsbwqSb7-zb4gXsJBCStQFaym8xvePAjw,25428
116
116
  lumibot/tools/ccxt_data_store.py,sha256=PlP3MHPHZP7GisEZsk1OUxeWijoPXwiQbsOBTr7jkQI,21227
117
- lumibot/tools/databento_helper.py,sha256=WBuQCm9Z7WFpde8qdsJEabtpU0ThGsjjPeK7BI2pUVA,43681
118
- lumibot/tools/databento_helper_polars.py,sha256=FXvvES_Y-E-IzAmVBtquh1UtQ-eN6i6BEoflcP7y8s0,48674
117
+ lumibot/tools/databento_helper.py,sha256=Tvljv-IZ9xZFFTcr91qkegLiYv8SFxCXrQV2MJMABy8,44493
118
+ lumibot/tools/databento_helper_polars.py,sha256=mr8gaUwtGu9uPh7v9syXkSbodEEsqBxzIMPoPAHtfR0,50272
119
119
  lumibot/tools/databento_roll.py,sha256=48HAw3h6OngCK4UTl9ifpjo-ki8qmB6OoJUrHp0gRmE,6767
120
120
  lumibot/tools/debugers.py,sha256=ga6npFsS9cpKtTXaygh9t2_txCElg3bfzfeqDBvSL8k,485
121
121
  lumibot/tools/decorators.py,sha256=gokLv6s37C1cnbnFSVOUc4RaVJ5aMTU2C344Vvi3ycs,2275
122
- lumibot/tools/futures_roll.py,sha256=k_c7X5bYuBX7_0vBdix8-RXjkvvC6RX9CJOFf6vweCE,7307
122
+ lumibot/tools/futures_roll.py,sha256=97xSI32Yko8jqOcTWl-XG-jdSMAgJCd02c7chjfas_w,8870
123
123
  lumibot/tools/futures_symbols.py,sha256=hFV02dk9cKucdaFOQAiQrlS15AJzdZ0qCuzVn7PfoPg,7851
124
124
  lumibot/tools/helpers.py,sha256=Q459K0aQGUME2CfwBCXmKbUQwiGR9FKSjUN2yLbBMIE,17873
125
125
  lumibot/tools/indicators.py,sha256=sihuiQTJ92igCBBMBQcyhpJFc-AWyj94vLQlTp6fu6Q,38465
@@ -132,7 +132,7 @@ lumibot/tools/polygon_helper_async.py,sha256=YHDXa9kmkkn8jh7hToY6GP5etyXS9Tj-uky
132
132
  lumibot/tools/polygon_helper_polars_optimized.py,sha256=NaIZ-5Av-G2McPEKHyJ-x65W72W_Agnz4lRgvXfQp8c,30415
133
133
  lumibot/tools/projectx_helpers.py,sha256=EIemLfbG923T_RBV_i6s6A9xgs7dt0et0oCnhFwdWfA,58299
134
134
  lumibot/tools/schwab_helper.py,sha256=CXnYhgsXOIb5MgmIYOp86aLxsBF9oeVrMGrjwl_GEv0,11768
135
- lumibot/tools/thetadata_helper.py,sha256=F5CdaWZkfJYFrParvN8KhowRWc5cRmPL4TnI9SH-VpI,90111
135
+ lumibot/tools/thetadata_helper.py,sha256=DAKQCQkItgzQ2MvPsAd2HcJYEOuuPc3NkWb1hsXmFhE,90220
136
136
  lumibot/tools/types.py,sha256=x-aQBeC6ZTN2-pUyxyo69Q0j5e0c_swdfe06kfrWSVc,1978
137
137
  lumibot/tools/yahoo_helper.py,sha256=htcKKkuktatIckVKfLc_ms0X75mXColysQhrZW244z8,19497
138
138
  lumibot/tools/yahoo_helper_polars_optimized.py,sha256=g9xBN-ReHSW4Aj9EMU_OncBXVS1HpfL8LTHit9ZxFY4,7417
@@ -142,7 +142,7 @@ lumibot/traders/trader.py,sha256=KMif3WoZtnSxA0BzoK3kvkTITNELrDFIortx1BYBv8s,967
142
142
  lumibot/trading_builtins/__init__.py,sha256=vH2QL5zLjL3slfEV1YW-BvQHtEYLCFkIWTZDfh3y8LE,87
143
143
  lumibot/trading_builtins/custom_stream.py,sha256=8_XiPT0JzyXrgnXCXoovGGUrWEfnG4ohIYMPfB_Nook,5264
144
144
  lumibot/trading_builtins/safe_list.py,sha256=IIjZOHSiZYK25A4WBts0oJaZNOJDsjZL65MOSHhE3Ig,1975
145
- lumibot-4.2.9.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
145
+ lumibot-4.2.10.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
146
146
  tests/__init__.py,sha256=3-VoT-nAuqMfwufd4ceN6fXaHl_zCfDCSXJOTp1ywYQ,393
147
147
  tests/conftest.py,sha256=UBw_2fx7r6TZPKus2b1Qxrzmd4bg8EEBnX1vCHUuSVA,3311
148
148
  tests/fixtures.py,sha256=wOHQsh1SGHnXe_PGi6kDWI30CS_Righi7Ig7vwSEKT4,9082
@@ -195,7 +195,7 @@ tests/test_databento_live.py,sha256=cydmUDbBruhVd-cZxN5uKjWRgXWt-gy4ZlajUfy0QSs,
195
195
  tests/test_databento_timezone_fixes.py,sha256=NsfND7yTdKH2ddiYYhO6kU3m41V7se7C4_zTvqKOGv0,11562
196
196
  tests/test_drift_rebalancer.py,sha256=AUuEd3WIunfx3gwVdLVtq8jOHlz65UeqpO4adY1xfcs,105289
197
197
  tests/test_futures_integration.py,sha256=3Ut0M8d5xPwHd4WcTSmP4HLC7VG_xSUXeJPX0-c0Fe8,9179
198
- tests/test_futures_roll.py,sha256=hg0_QIjBeN2-DHyGZmL3NAnSUJYprAgXX727sXFZjcM,1262
198
+ tests/test_futures_roll.py,sha256=ykx67tWAb-fDoO-UQ6SsnvttjIPAnFOD3293vDngs1U,2036
199
199
  tests/test_get_historical_prices.py,sha256=0JGlFuDyQFltKRpaFp2CLKEyleD2xMmgINVzLjrTdl8,15540
200
200
  tests/test_helpers.py,sha256=8Ay1B6I8yn3trZKrYjOs6Kbda7jmM20-TFh8LfIWpmY,11659
201
201
  tests/test_indicator_subplots.py,sha256=-nqUq1jNhTtSb0xH0WU16xfC-DLD-Ak9qzWo6Yx2bnE,13245
@@ -233,15 +233,16 @@ tests/test_quiet_logs_comprehensive.py,sha256=QVkZWLAUnPEb04Ec8qKXvLzdDUkp8alez2
233
233
  tests/test_quiet_logs_functionality.py,sha256=MlOBUICuTy1OXCDifOW05uD7hjnZRsQ2xxQlcLkGebQ,3811
234
234
  tests/test_quiet_logs_requirements.py,sha256=YoUooSVLrFL8TlWPfxEiqxvSj4d8z6-qg58ja4dtOc0,7856
235
235
  tests/test_session_manager.py,sha256=1qygN3aQ2Xe2uh4BMPm0E3V8KXLFNGq5qdL8KkZjef4,11632
236
+ tests/test_strategy_close_position.py,sha256=88_A137S1dtfjmGcA5zcFpa0koef3B9Az1y9Lx226yA,2747
236
237
  tests/test_strategy_methods.py,sha256=j9Mhr6nnG1fkiVQXnx7gLjzGbeQmwt0UbJr_4plD36o,12539
237
238
  tests/test_strategy_price_guard.py,sha256=3GJdlfROwx6-adsSi8ZBrWaLOy9e-0N6V1eqpikj8e4,1540
238
239
  tests/test_thetadata_backwards_compat.py,sha256=RzNLhNZNJZ2hPkEDyG-T_4mRRXh5XqavK6r-OjfRASQ,3306
239
- tests/test_thetadata_helper.py,sha256=4EdFHoCTTKELU216ZNYGjfo6Uvq-29kK-TVpFLPWT3M,67263
240
+ tests/test_thetadata_helper.py,sha256=TlbmZXGUclJKcBbrRbbLpbhILbYdtz7N_2yml-gyHXQ,69090
240
241
  tests/test_thetadata_pandas_verification.py,sha256=MWUecqBY6FGFslWLRo_C5blGbom_unmXCZikAfZXLks,6553
241
242
  tests/test_tradier.py,sha256=iCEM2FTxJSzJ2oLNaRqSx05XaX_DCiMzLx1aEYPANko,33280
242
243
  tests/test_tradier_data.py,sha256=1jTxDzQtzaC42CQJVXMRMElBwExy1mVci3NFfKjjVH0,13363
243
244
  tests/test_tradingfee.py,sha256=2CBJgdU-73Ae4xuys-QkbCtpDTL9hwOUkRnCgLm4OmE,163
244
- tests/test_tradovate.py,sha256=XW0ZyiMbRYr16hqGJIa8C1Wg5O0V0tpiUMHvejIAnEg,37436
245
+ tests/test_tradovate.py,sha256=U5mR61yh_6OkO_NJQLQ1XTGcAcsjIS5UiCNlXCJROZ0,48730
245
246
  tests/test_unified_logger.py,sha256=Y2rhLk6GoUs9Vj-qRvGThRUTdNohxmH2yFbb3j8Yq3g,10849
246
247
  tests/test_vix_helper.py,sha256=jE6TZ4ufVU_0W4Jx3zJ295srsy4Xjw9qU3KwfujjZ_s,8476
247
248
  tests/test_yahoo_data.py,sha256=84R2jCl9z2U5qKZhR68tFJou2Rfwno0Qomc8yxPfvAs,4578
@@ -282,7 +283,7 @@ tests/backtest/test_thetadata.py,sha256=xWYfC9C4EhbMDb29qyZWHO3sSWaLIPzzvcMbHCt5
282
283
  tests/backtest/test_thetadata_comprehensive.py,sha256=-gN3xLJcJtlB-k4vlaK82DCZDGDmr0LNZZDzn-aN3l4,26120
283
284
  tests/backtest/test_thetadata_vs_polygon.py,sha256=dZqsrOx3u3cz-1onIO6o5BDRjI1ey7U9vIkZupfXoig,22831
284
285
  tests/backtest/test_yahoo.py,sha256=2FguUTUMC9_A20eqxnZ17rN3tT9n6hyvJHaL98QKpqY,3443
285
- lumibot-4.2.9.dist-info/METADATA,sha256=Bis7WU6RY3Wp7YXHsNa7-xklQzQcXRi5zcJzZhC2j0w,12093
286
- lumibot-4.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
287
- lumibot-4.2.9.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
288
- lumibot-4.2.9.dist-info/RECORD,,
286
+ lumibot-4.2.10.dist-info/METADATA,sha256=IVrYPlPQOaa67eTLhVKhGSWaJpLaGOISsHL_UN1wZc0,12094
287
+ lumibot-4.2.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
288
+ lumibot-4.2.10.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
289
+ lumibot-4.2.10.dist-info/RECORD,,
@@ -36,3 +36,23 @@ def test_resolve_symbols_for_range_produces_sequential_contracts():
36
36
 
37
37
  symbols = futures_roll.resolve_symbols_for_range(asset, start, end, year_digits=1)
38
38
  assert symbols == ["MESU5", "MESZ5", "MESH6"], symbols
39
+
40
+
41
+ def test_comex_gold_rolls_on_third_last_business_day_offset():
42
+ asset_symbol = "GC"
43
+
44
+ year, month = futures_roll.determine_contract_year_month(asset_symbol, _dt(2025, 2, 14))
45
+ assert (year, month) == (2025, 2)
46
+
47
+ # Seven business days before the third last business day of February 2025 is Feb 17
48
+ year, month = futures_roll.determine_contract_year_month(asset_symbol, _dt(2025, 2, 17))
49
+ assert (year, month) == (2025, 4)
50
+
51
+
52
+ def test_comex_gold_symbol_sequence_uses_even_month_cycle():
53
+ asset = Asset("GC", asset_type=Asset.AssetType.CONT_FUTURE)
54
+ start = _dt(2025, 1, 1)
55
+ end = _dt(2025, 8, 1)
56
+
57
+ symbols = futures_roll.resolve_symbols_for_range(asset, start, end, year_digits=1)
58
+ assert symbols == ["GCG5", "GCJ5", "GCM5", "GCQ5"], symbols