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.
- lumibot/brokers/broker.py +33 -0
- lumibot/brokers/tradovate.py +556 -10
- lumibot/data_sources/databento_data_polars.py +35 -34
- lumibot/entities/asset.py +11 -0
- lumibot/strategies/strategy.py +46 -1
- lumibot/tools/databento_helper.py +16 -0
- lumibot/tools/databento_helper_polars.py +41 -1
- lumibot/tools/futures_roll.py +71 -20
- lumibot/tools/thetadata_helper.py +4 -1
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/METADATA +1 -1
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/RECORD +18 -17
- tests/test_futures_roll.py +20 -0
- tests/test_strategy_close_position.py +83 -0
- tests/test_thetadata_helper.py +45 -1
- tests/test_tradovate.py +293 -0
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/WHEEL +0 -0
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
-
|
|
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(
|
lumibot/strategies/strategy.py
CHANGED
|
@@ -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
|
-
|
|
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)
|
lumibot/tools/futures_roll.py
CHANGED
|
@@ -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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
1620
|
+
raise ValueError("Cannot connect to Theta Data!")
|
|
1618
1621
|
|
|
1619
1622
|
# Store this page's response data
|
|
1620
1623
|
page_count += 1
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
118
|
-
lumibot/tools/databento_helper_polars.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
286
|
-
lumibot-4.2.
|
|
287
|
-
lumibot-4.2.
|
|
288
|
-
lumibot-4.2.
|
|
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,,
|
tests/test_futures_roll.py
CHANGED
|
@@ -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
|