From e8b71b7aec5231f333e994245db1f46755606eeb Mon Sep 17 00:00:00 2001 From: Rodion Lim Date: Mon, 26 Jan 2026 13:29:53 +0800 Subject: [PATCH] feat(futures): add minimal example to help with easier implementation --- src/quantlib_st/sysdata/examples/README.md | 72 ++++++++ src/quantlib_st/sysdata/examples/__init__.py | 0 .../sysdata/examples/futures_simplified.py | 155 ++++++++++++++++++ .../examples/futures_simplified_with_roll.py | 139 ++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 src/quantlib_st/sysdata/examples/README.md create mode 100644 src/quantlib_st/sysdata/examples/__init__.py create mode 100644 src/quantlib_st/sysdata/examples/futures_simplified.py create mode 100644 src/quantlib_st/sysdata/examples/futures_simplified_with_roll.py diff --git a/src/quantlib_st/sysdata/examples/README.md b/src/quantlib_st/sysdata/examples/README.md new file mode 100644 index 0000000..f2b5b36 --- /dev/null +++ b/src/quantlib_st/sysdata/examples/README.md @@ -0,0 +1,72 @@ +# Futures Example: External API Stub + +This folder contains a minimal, working example that shows how to implement a futures data source for carry and backadjustment. + +## Files + +- [futures_simplified.py](futures_simplified.py) — A mock implementation of `futuresSimData` that returns synthetic prices for one instrument (`BRT`) with pre-built `PRICE/CARRY/FORWARD` series. +- [futures_simplified_with_roll.py](futures_simplified_with_roll.py) — A mock implementation that derives `PRICE/CARRY/FORWARD` from per-contract prices using roll calendars and roll parameters. + +## What you need to implement + +The example class `MockApiFuturesSimData` in [futures_simplified.py](futures_simplified.py) shows how to implement the abstract methods from `futuresSimData`: + +1. **Instrument catalog** + - `get_instrument_list()` + - `get_instrument_asset_classes()` + +2. **Costs + metadata** + - `get_spread_cost()` + - `get_instrument_meta_data()` + - `get_instrument_object_with_meta_data()` + +3. **Roll setup** + - `get_roll_parameters()` + +4. **Core price methods** + - `get_multiple_prices_from_start_date()` + - `get_backadjusted_futures_price()` (usually calls the helper `_get_backadjusted_futures_price_from_multiple_prices`) + +## What to replace with API calls + +In the examples, the following are mock implementations: + +- `get_multiple_prices_from_start_date()` in [futures_simplified.py](futures_simplified.py) → replace with your API call to fetch: + - price series for the traded contract (PRICE) + - price series for the carry contract (CARRY) + - price series for the forward contract (FORWARD) + - the corresponding contract IDs (`PRICE_CONTRACT`, `CARRY_CONTRACT`, `FORWARD_CONTRACT`) + +- `_mock_contract_prices()` in [futures_simplified_with_roll.py](futures_simplified_with_roll.py) → replace with your API call to fetch per-contract final prices: + - one price series per contract (keyed by `YYYYMM`) + - these are used to build a roll calendar and derive `PRICE/CARRY/FORWARD` + +The example uses `BRT` with contract IDs `202512`, `202511`, `202510`, etc., purely as placeholders. + +## Data shape required for futuresMultiplePrices + +Your API output must be converted into a `futuresMultiplePrices` object with a `DatetimeIndex` and these columns: + +- `PRICE` +- `CARRY` +- `FORWARD` +- `PRICE_CONTRACT` +- `CARRY_CONTRACT` +- `FORWARD_CONTRACT` + +Once you have this, `futuresSimData` can compute: + +- **Carry metrics** (roll, annualised roll) +- **Backadjusted futures prices** using `_get_backadjusted_futures_price_from_multiple_prices` + +## When you might need per-contract data + +If your API provides raw per-contract data (one contract per ticker), you can also implement a data source that inherits from `futuresContractPriceData`. + +See: + +- [sysdata/futures/README.md](../futures/README.md) +- [sysdata/futures/futures_per_contract_prices.py](../futures/futures_per_contract_prices.py) +- [objects/dict_of_futures_per_contract_prices.py](../../objects/dict_of_futures_per_contract_prices.py) + +These per-contract classes are **optional** for carry/backadjustment if you already have continuous `PRICE`, `CARRY`, and `FORWARD` series. diff --git a/src/quantlib_st/sysdata/examples/__init__.py b/src/quantlib_st/sysdata/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/quantlib_st/sysdata/examples/futures_simplified.py b/src/quantlib_st/sysdata/examples/futures_simplified.py new file mode 100644 index 0000000..be3c09d --- /dev/null +++ b/src/quantlib_st/sysdata/examples/futures_simplified.py @@ -0,0 +1,155 @@ +""" +Example: Minimal futuresSimData implementation with mock data. + +This module is intended as a template for plugging in an external API that returns +per-contract futures prices. Replace the mock data generators with API calls. +""" + +from __future__ import annotations + +import datetime as dt +import numpy as np +import pandas as pd + +from quantlib_st.objects.instruments import ( + assetClassesAndInstruments, + futuresInstrument, + futuresInstrumentWithMetaData, + instrumentMetaData, +) +from quantlib_st.objects.multiple_prices import futuresMultiplePrices +from quantlib_st.objects.dict_of_named_futures_per_contract_prices import ( + price_name, + carry_name, + forward_name, + contract_name_from_column_name, +) +from quantlib_st.objects.adjusted_prices import futuresAdjustedPrices +from quantlib_st.objects.rolls import rollParameters +from quantlib_st.sysdata.sim.futures_sim_data import futuresSimData + +price_contract_name = contract_name_from_column_name(price_name) +carry_contract_name = contract_name_from_column_name(carry_name) +forward_contract_name = contract_name_from_column_name(forward_name) + + +class MockApiFuturesSimData(futuresSimData): + """ + Minimal, end-to-end futuresSimData example. + + Replace the mock methods with your external API calls. + """ + + def __init__(self, start_date: dt.datetime | None = None): + super().__init__() + self._start_date_for_data_from_config = start_date or dt.datetime(2024, 1, 2) + + def get_instrument_list(self) -> list[str]: + return ["BRT"] + + def get_instrument_asset_classes(self) -> assetClassesAndInstruments: + series = pd.Series({"BRT": "Energy"}) + return assetClassesAndInstruments.from_pd_series(series) + + def get_spread_cost(self, instrument_code: str) -> float: + return 0.01 + + def get_roll_parameters(self, instrument_code: str) -> rollParameters: + return rollParameters( + hold_rollcycle="FGHJKMNQUVXZ", + priced_rollcycle="FGHJKMNQUVXZ", + roll_offset_day=0, + carry_offset=-1, + approx_expiry_offset=0, + ) + + def get_instrument_meta_data( + self, instrument_code: str + ) -> futuresInstrumentWithMetaData: + instrument = futuresInstrument(instrument_code) + meta = instrumentMetaData( + Description="Brent Crude Oil (Mock)", + Pointsize=1000.0, + Currency="USD", + AssetClass="Energy", + PerBlock=0.0, + Percentage=0.0, + PerTrade=0.0, + Region="Global", + ) + return futuresInstrumentWithMetaData(instrument, meta) + + def get_instrument_object_with_meta_data( + self, instrument_code: str + ) -> futuresInstrumentWithMetaData: + return self.get_instrument_meta_data(instrument_code) + + def get_backadjusted_futures_price( + self, instrument_code: str + ) -> futuresAdjustedPrices: + return self._get_backadjusted_futures_price_from_multiple_prices( + instrument_code, + backadjust_methodology="diff_adjusted", + ) + + def get_multiple_prices_from_start_date( + self, instrument_code: str, start_date: dt.datetime + ) -> futuresMultiplePrices: + """ + Mock implementation using synthetic prices and a rolling contract schedule. + + Replace `_mock_multiple_prices` with API calls returning the raw contract + prices and contract IDs. + """ + if instrument_code != "BRT": + return futuresMultiplePrices.create_empty() + + return self._mock_multiple_prices(start_date) + + def _mock_multiple_prices( + self, start_date: dt.datetime, days: int = 15 + ) -> futuresMultiplePrices: + index = pd.date_range(start=start_date, periods=days, freq="B") + + # Synthetic price series + base_price = 75.0 + np.linspace(0, 1.4, num=days) + price_series = pd.Series(base_price, index=index) + carry_series = price_series + 0.2 + forward_series = price_series + 0.4 + + # Mock contract IDs (YYYYMM) that change over time + contract_schedule = ["202512", "202511", "202510"] + chunk = int(np.ceil(days / len(contract_schedule))) + price_contracts = ( + [contract for contract in contract_schedule for _ in range(chunk)] + )[:days] + + contract_map = { + "202512": ("202511", "202510"), + "202511": ("202510", "202509"), + "202510": ("202509", "202508"), + } + carry_contracts = [contract_map[c][0] for c in price_contracts] + forward_contracts = [contract_map[c][1] for c in price_contracts] + + data = pd.DataFrame( + { + price_name: price_series, + carry_name: carry_series, + forward_name: forward_series, + price_contract_name: price_contracts, + carry_contract_name: carry_contracts, + forward_contract_name: forward_contracts, + }, + index=index, + ) + + return futuresMultiplePrices(data) + + +if __name__ == "__main__": + data = MockApiFuturesSimData() + multiple_prices = data.get_multiple_prices("BRT") + backadjusted = data.get_backadjusted_futures_price("BRT") + print(multiple_prices.tail(3)) + print(backadjusted.tail(3)) diff --git a/src/quantlib_st/sysdata/examples/futures_simplified_with_roll.py b/src/quantlib_st/sysdata/examples/futures_simplified_with_roll.py new file mode 100644 index 0000000..2c824f7 --- /dev/null +++ b/src/quantlib_st/sysdata/examples/futures_simplified_with_roll.py @@ -0,0 +1,139 @@ +""" +Example: futuresSimData implementation using roll calendars derived from per-contract prices. + +This shows how to: +1) Provide per-contract final prices (dictFuturesContractFinalPrices) +2) Build a roll calendar from roll parameters +3) Build multiple prices (PRICE/CARRY/FORWARD) from that calendar + +Replace the mock price generation with API calls. +""" + +from __future__ import annotations + +import datetime as dt + +import numpy as np +import pandas as pd + +from quantlib_st.objects.instruments import ( + assetClassesAndInstruments, + futuresInstrument, + futuresInstrumentWithMetaData, + instrumentMetaData, +) +from quantlib_st.objects.dict_of_futures_per_contract_prices import ( + dictFuturesContractFinalPrices, +) +from quantlib_st.objects.multiple_prices import futuresMultiplePrices +from quantlib_st.objects.adjusted_prices import futuresAdjustedPrices +from quantlib_st.objects.rolls import rollParameters +from quantlib_st.objects.roll_calendars import rollCalendar +from quantlib_st.sysdata.sim.futures_sim_data import futuresSimData + + +class MockApiFuturesSimDataWithRoll(futuresSimData): + """ + Example implementation that derives PRICE/CARRY/FORWARD from per-contract prices. + + Replace `_mock_contract_prices` with calls to your external API. + """ + + def __init__(self, start_date: dt.datetime | None = None): + super().__init__() + self._start_date_for_data_from_config = start_date or dt.datetime(2024, 1, 2) + + def get_instrument_list(self) -> list[str]: + return ["BRT"] + + def get_instrument_asset_classes(self) -> assetClassesAndInstruments: + series = pd.Series({"BRT": "Energy"}) + return assetClassesAndInstruments.from_pd_series(series) + + def get_spread_cost(self, instrument_code: str) -> float: + return 0.01 + + def get_roll_parameters(self, instrument_code: str) -> rollParameters: + return rollParameters( + hold_rollcycle="FGHJKMNQUVXZ", + priced_rollcycle="FGHJKMNQUVXZ", + roll_offset_day=0, + carry_offset=-1, + approx_expiry_offset=0, + ) + + def get_instrument_meta_data( + self, instrument_code: str + ) -> futuresInstrumentWithMetaData: + instrument = futuresInstrument(instrument_code) + meta = instrumentMetaData( + Description="Brent Crude Oil (Mock)", + Pointsize=1000.0, + Currency="USD", + AssetClass="Energy", + PerBlock=0.0, + Percentage=0.0, + PerTrade=0.0, + Region="Global", + ) + return futuresInstrumentWithMetaData(instrument, meta) + + def get_instrument_object_with_meta_data( + self, instrument_code: str + ) -> futuresInstrumentWithMetaData: + return self.get_instrument_meta_data(instrument_code) + + def get_backadjusted_futures_price( + self, instrument_code: str + ) -> futuresAdjustedPrices: + return self._get_backadjusted_futures_price_from_multiple_prices( + instrument_code, + backadjust_methodology="diff_adjusted", + ) + + def get_multiple_prices_from_start_date( + self, instrument_code: str, start_date: dt.datetime + ) -> futuresMultiplePrices: + if instrument_code != "BRT": + return futuresMultiplePrices.create_empty() + + # 1) Build per-contract final prices (mocked here) + contract_prices = self._mock_contract_prices(start_date) + + # 2) Build roll calendar based on roll parameters + available contracts + roll_params = self.get_roll_parameters(instrument_code) + calendar = rollCalendar.create_from_prices(contract_prices, roll_params) + + # 3) Convert per-contract data into multiple prices (PRICE/CARRY/FORWARD) + multiple_prices = futuresMultiplePrices.create_from_raw_data( + calendar, contract_prices + ) + + # Ensure we respect the requested start date + filtered = pd.DataFrame(multiple_prices).loc[start_date:] + return futuresMultiplePrices(filtered) + + def _mock_contract_prices( + self, start_date: dt.datetime, days: int = 30 + ) -> dictFuturesContractFinalPrices: + index = pd.date_range(start=start_date, periods=days, freq="B") + + # Mock contract list (YYYYMM) + contract_ids = ["202510", "202511", "202512", "202601"] + + price_dict = {} + for i, contract_id in enumerate(contract_ids): + # Each contract series is a smooth trend with a small offset + base = 75.0 + (i * 0.5) + series = pd.Series(base + np.linspace(0, 1.0, num=len(index)), index=index) + price_dict[contract_id] = series + + return dictFuturesContractFinalPrices(price_dict) + + +if __name__ == "__main__": + data = MockApiFuturesSimDataWithRoll() + mp = data.get_multiple_prices("BRT") + backadj = data.get_backadjusted_futures_price("BRT") + print(mp.tail(3)) + print(backadj.tail(3))