Skip to content

Commit

Permalink
Moved the get_price responsibility from DataProvider to AbstractPrice…
Browse files Browse the repository at this point in the history
…DataProvider (#191)
  • Loading branch information
myrmarachne authored Nov 26, 2024
1 parent df4bb7a commit db26e2a
Show file tree
Hide file tree
Showing 89 changed files with 1,632 additions and 1,771 deletions.
1 change: 1 addition & 0 deletions .coverargerc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ exclude_lines =
# Don't complain about missing debug-only code:
def __repr__
def __str__
pass

# Don't complain if abstractmethods are skipped
@abstractmethod
Expand Down
2 changes: 1 addition & 1 deletion demo_scripts/backtester/run_alpha_model_backtest_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def main():

# ----- build models ----- #
model = MovingAverageAlphaModel(fast_time_period=5, slow_time_period=20, risk_estimation_factor=1.25,
data_provider=ts.data_handler)
data_provider=ts.data_provider)
model_tickers = [DummyTicker('AAA'), DummyTicker('BBB')]
model_tickers_dict = {model: model_tickers}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def main():

# ----- build models ----- #
model = MovingAverageAlphaModel(fast_time_period=5, slow_time_period=20, risk_estimation_factor=1.25,
data_provider=ts.data_handler)
data_provider=ts.data_provider)
model_tickers = [DummyTicker('AAA'), DummyTicker('BBB'), DummyTicker('CCC'),
DummyTicker('DDD'), DummyTicker('EEE'), DummyTicker('FFF')]
model_tickers_dict = {model: model_tickers}
Expand Down
9 changes: 5 additions & 4 deletions demo_scripts/strategies/intraday_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@ class IntradayMAStrategy(AbstractStrategy):
10:00 and 13:00, and creates a buy order in case if the short moving average is greater or equal to the long moving
average.
"""

def __init__(self, ts: BacktestTradingSession, ticker: Ticker):
super().__init__(ts)
self.broker = ts.broker
self.order_factory = ts.order_factory
self.data_handler = ts.data_handler
self.data_provider = ts.data_provider
self.position_sizer = ts.position_sizer
self.timer = ts.timer
self.timer = ts.data_provider.timer
self.ticker = ticker

self.logger = qf_logger.getChild(self.__class__.__name__)
Expand All @@ -64,8 +65,8 @@ def calculate_and_place_orders(self):
short_ma_len = 5

# Use data handler to download last 20 daily close prices and use them to compute the moving averages
long_ma_series = self.data_handler.historical_price(self.ticker, PriceField.Close, long_ma_len,
frequency=Frequency.MIN_1)
long_ma_series = self.data_provider.historical_price(self.ticker, PriceField.Close, long_ma_len,
frequency=Frequency.MIN_1)
long_ma_price = long_ma_series.mean()

short_ma_series = long_ma_series.tail(short_ma_len)
Expand Down
5 changes: 3 additions & 2 deletions demo_scripts/strategies/simple_ma_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ class SimpleMAStrategy(AbstractStrategy):
short - 5 days) and creates a buy order in case if the short moving average is greater or equal to the long moving
average.
"""

def __init__(self, ts: BacktestTradingSession, ticker: Ticker):
super().__init__(ts)
self.broker = ts.broker
self.order_factory = ts.order_factory
self.data_handler = ts.data_handler
self.data_provider = ts.data_provider
self.ticker = ticker

def calculate_and_place_orders(self):
Expand All @@ -54,7 +55,7 @@ def calculate_and_place_orders(self):
short_ma_len = 5

# Use data handler to download last 20 daily close prices and use them to compute the moving averages
long_ma_series = self.data_handler.historical_price(self.ticker, PriceField.Close, long_ma_len)
long_ma_series = self.data_provider.historical_price(self.ticker, PriceField.Close, long_ma_len)
long_ma_price = long_ma_series.mean()

short_ma_series = long_ma_series.tail(short_ma_len)
Expand Down
14 changes: 0 additions & 14 deletions docs/source/backtesting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,6 @@ contract
contract_to_ticker_conversion.simulated_contract_ticker_mapper.SimulatedContractTickerMapper
contract_to_ticker_conversion.ib_contract_ticker_mapper.IBContractTickerMapper


data_handler
==================
.. currentmodule:: qf_lib.backtesting.data_handler

.. autosummary::
:nosignatures:
:toctree: _autosummary
:template: short_class.rst

data_handler.DataHandler
daily_data_handler.DailyDataHandler
intraday_data_handler.IntradayDataHandler

events
========
.. currentmodule:: qf_lib.backtesting.events.time_event
Expand Down
1 change: 1 addition & 0 deletions docs/source/data_providers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ data_providers

data_provider.DataProvider
abstract_price_data_provider.AbstractPriceDataProvider
futures_data_provider.FuturesDataProvider
bloomberg.bloomberg_data_provider.BloombergDataProvider
bloomberg_beap_hapi.bloomberg_beap_hapi_data_provider.BloombergBeapHapiDataProvider
preset_data_provider.PresetDataProvider
Expand Down
32 changes: 15 additions & 17 deletions qf_lib/analysis/signals_analysis/signals_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from qf_lib.analysis.common.abstract_document import AbstractDocument
from qf_lib.backtesting.alpha_model.alpha_model import AlphaModel
from qf_lib.backtesting.alpha_model.exposure_enum import Exposure
from qf_lib.backtesting.data_handler.data_handler import DataHandler
from qf_lib.backtesting.events.time_event.regular_time_event.market_close_event import MarketCloseEvent
from qf_lib.backtesting.events.time_event.regular_time_event.market_open_event import MarketOpenEvent
from qf_lib.common.enums.frequency import Frequency
Expand All @@ -35,6 +34,7 @@
from qf_lib.containers.futures.futures_adjustment_method import FuturesAdjustmentMethod
from qf_lib.containers.futures.futures_chain import FuturesChain
from qf_lib.containers.series.qf_series import QFSeries
from qf_lib.data_providers.data_provider import DataProvider
from qf_lib.documents_utils.document_exporting.element.chart import ChartElement
from qf_lib.documents_utils.document_exporting.element.custom import CustomElement
from qf_lib.documents_utils.document_exporting.element.heading import HeadingElement
Expand Down Expand Up @@ -69,8 +69,8 @@ class SignalsPlotter(AbstractDocument):
starting date of the prices data frame
end_date: datetime
last date of the prices data frame
data_handler: DataHandler
data handler used to download prices
data_provider: DataProvider
data provider used to download prices
alpha_models: AlphaModel, Sequence[AlphaModel]
instances of alpha models which signals will be evaluated. Each plot in the document is described using the
alpha_models __str__ function.
Expand All @@ -87,7 +87,7 @@ class SignalsPlotter(AbstractDocument):
"""

def __init__(self, tickers: Union[Ticker, Sequence[Ticker]], start_date: datetime, end_date: datetime,
data_handler: DataHandler, alpha_models: Union[AlphaModel, Sequence[AlphaModel]], settings: Settings,
data_provider: DataProvider, alpha_models: Union[AlphaModel, Sequence[AlphaModel]], settings: Settings,
pdf_exporter: PDFExporter, title: str = "Signals Plotter",
signal_frequency: Frequency = Frequency.DAILY, data_frequency: Frequency = Frequency.DAILY):

Expand All @@ -99,18 +99,16 @@ def __init__(self, tickers: Union[Ticker, Sequence[Ticker]], start_date: datetim
self.start_date = start_date
self.end_date = end_date

self.data_handler = data_handler

assert isinstance(self.data_handler.timer, SettableTimer)
self.timer: SettableTimer = self.data_handler.timer
self.data_provider = data_provider
self.data_provider.set_timer(SettableTimer(end_date))

self.signal_frequency = signal_frequency
self.data_frequency = data_frequency

for ticker in self.tickers:
if isinstance(ticker, FutureTicker):
# use a new timer that allows to look until the end date
ticker.initialize_data_provider(SettableTimer(end_date), self.data_handler.data_provider)
ticker.initialize_data_provider(self.data_provider)

def build_document(self):
self._add_header()
Expand Down Expand Up @@ -139,8 +137,8 @@ def create_tickers_analysis(self, ticker: Ticker):
prev_exposure = Exposure.OUT
for date in self._get_signals_dates():
try:
self.timer.set_current_time(date)
new_exposure = alpha_model.get_signal(ticker, prev_exposure, date, self.data_frequency)\
self.data_provider.set_current_time(date)
new_exposure = alpha_model.get_signal(ticker, prev_exposure, date, self.data_frequency) \
.suggested_exposure
exposures.append(new_exposure.value)
dates.append(date)
Expand Down Expand Up @@ -174,17 +172,17 @@ def get_prices(self, ticker: Ticker):
Here we can use data provider as we do not worry about look-ahead
"""
if isinstance(ticker, FutureTicker):
futures_chain = FuturesChain(ticker, self.data_handler.data_provider, FuturesAdjustmentMethod.NTH_NEAREST)
futures_chain = FuturesChain(ticker, self.data_provider, FuturesAdjustmentMethod.NTH_NEAREST)
prices_df = futures_chain.get_price(PriceField.ohlc(),
start_date=self.start_date,
end_date=self.end_date,
frequency=self.data_frequency)
else:
prices_df = self.data_handler.data_provider.get_price(ticker,
PriceField.ohlc(),
start_date=self.start_date,
end_date=self.end_date,
frequency=self.data_frequency)
prices_df = self.data_provider.get_price(ticker,
PriceField.ohlc(),
start_date=self.start_date,
end_date=self.end_date,
frequency=self.data_frequency)
return prices_df

def add_models_implementation(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from qf_lib.containers.series.prices_series import PricesSeries
from qf_lib.containers.series.qf_series import QFSeries
from qf_lib.containers.series.simple_returns_series import SimpleReturnsSeries
from qf_lib.data_providers.data_provider import DataProvider
from qf_lib.data_providers.abstract_price_data_provider import AbstractPriceDataProvider
from qf_lib.documents_utils.document_exporting.element.df_table import DFTable
from qf_lib.documents_utils.document_exporting.element.heading import HeadingElement
from qf_lib.documents_utils.document_exporting.element.new_page import NewPageElement
Expand Down Expand Up @@ -70,7 +70,7 @@ class AssetPerfAndDrawdownSheet(AbstractDocument):
start_date: datetime
end_date: datetime
Dates to used as start and end date for the statistics
data_provider: DataProvider
data_provider: AbstractPriceDataProvider
Data provider used to download the prices and future contracts information, necessary to compute Buy and Hold
benchmark performance
settings: Settings
Expand All @@ -87,7 +87,7 @@ class AssetPerfAndDrawdownSheet(AbstractDocument):
"""

def __init__(self, category_to_model_tickers: Dict[str, List[Ticker]], transactions: Union[List[Transaction], str],
start_date: datetime, end_date: datetime, data_provider: DataProvider, settings: Settings,
start_date: datetime, end_date: datetime, data_provider: AbstractPriceDataProvider, settings: Settings,
pdf_exporter: PDFExporter, title: str = "Assets Monitoring Sheet", initial_cash: int = 10000000,
frequency: Frequency = Frequency.YEARLY):

Expand All @@ -104,6 +104,7 @@ def __init__(self, category_to_model_tickers: Dict[str, List[Ticker]], transacti
self._start_date = start_date
self._end_date = end_date
self._data_provider = data_provider
self._data_provider.set_timer(SettableTimer(self._end_date))
self._initial_cash = initial_cash

if frequency not in (Frequency.MONTHLY, Frequency.YEARLY):
Expand Down Expand Up @@ -190,7 +191,7 @@ def _generate_buy_and_hold_returns(self, ticker: Ticker) -> SimpleReturnsSeries:
""" Computes series of simple returns, which would be returned by the Buy and Hold strategy. """
if isinstance(ticker, FutureTicker):
try:
ticker.initialize_data_provider(SettableTimer(self._end_date), self._data_provider)
ticker.initialize_data_provider(self._data_provider)
futures_chain = FuturesChain(ticker, self._data_provider, FuturesAdjustmentMethod.BACK_ADJUSTED)
prices_series = futures_chain.get_price(PriceField.Close, self._start_date, self._end_date)
except NoValidTickerException:
Expand Down
11 changes: 7 additions & 4 deletions qf_lib/analysis/strategy_monitoring/pnl_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@
from qf_lib.containers.futures.future_tickers.future_ticker import FutureTicker
from qf_lib.containers.series.prices_series import PricesSeries
from qf_lib.containers.series.qf_series import QFSeries
from qf_lib.data_providers.data_provider import DataProvider
from qf_lib.data_providers.abstract_price_data_provider import AbstractPriceDataProvider


class PnLCalculator:
def __init__(self, data_provider: DataProvider):
def __init__(self, data_provider: AbstractPriceDataProvider):
"""
The purpose of this class is the computation of Profit and Loss for a given ticker, based on a list of
Transaction objects. The PnL is computed every day, at the AfterMarketCloseEvent time. The calculation requires
Expand All @@ -45,10 +45,11 @@ def __init__(self, data_provider: DataProvider):
Parameters
-----------
data_provider: DataProvider
data_provider: AbstractPriceDataProvider
data provider used to download prices data
"""
self._data_provider = data_provider
self._data_provider.set_timer(SettableTimer())

def compute_pnl(self, ticker: Ticker, transactions: List[Transaction], start_date: datetime, end_date: datetime) \
-> PricesSeries:
Expand Down Expand Up @@ -83,7 +84,9 @@ def compute_pnl(self, ticker: Ticker, transactions: List[Transaction], start_dat
def _get_prices_df(self, ticker: Ticker, start_date: datetime, end_date: datetime) -> PricesDataFrame:
""" Returns non-adjusted open and close prices, indexed with the Market Open and Market Close time."""
if isinstance(ticker, FutureTicker):
ticker.initialize_data_provider(SettableTimer(end_date), self._data_provider)
assert isinstance(self._data_provider.timer, SettableTimer)
self._data_provider.timer.set_current_time(end_date)
ticker.initialize_data_provider(self._data_provider)
tickers_chain = ticker.get_expiration_dates()

if start_date >= tickers_chain.index[-1] or end_date <= tickers_chain.index[0]:
Expand Down
2 changes: 1 addition & 1 deletion qf_lib/analysis/tearsheets/portfolio_analysis_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ def _add_avg_time_in_the_market_per_ticker(self):
self.document.add_element(HeadingElement(level=2, text="Average time in the market per asset"))

start_time = self.backtest_result.start_date
end_time = self.backtest_result.portfolio.timer.now()
end_time = self.backtest_result.portfolio.data_provider.timer.now()
backtest_duration = pd.Timedelta(end_time - start_time) / pd.Timedelta(minutes=1) # backtest duration in min
positions_list = self.backtest_result.portfolio.closed_positions() + \
list(self.backtest_result.portfolio.open_positions_dict.values())
Expand Down
11 changes: 5 additions & 6 deletions qf_lib/backtesting/alpha_model/alpha_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from qf_lib.common.tickers.tickers import Ticker
from qf_lib.common.utils.logging.qf_parent_logger import qf_logger
from qf_lib.common.utils.miscellaneous.average_true_range import average_true_range
from qf_lib.data_providers.data_provider import DataProvider
from qf_lib.data_providers.abstract_price_data_provider import AbstractPriceDataProvider


class AlphaModel(metaclass=ABCMeta):
Expand All @@ -36,12 +36,11 @@ class AlphaModel(metaclass=ABCMeta):
risk_estimation_factor
float value which estimates the risk level of the specific AlphaModel. Corresponds to the level at which
the stop-loss should be placed.
data_provider: DataProvider
DataProvider which provides data for the ticker. For the backtesting purposes, in order to avoid looking into
the future, use DataHandler wrapper.
data_provider: AbstractPriceDataProvider
DataProvider which provides data for the ticker.
"""

def __init__(self, risk_estimation_factor: float, data_provider: DataProvider):
def __init__(self, risk_estimation_factor: float, data_provider: AbstractPriceDataProvider):
self.risk_estimation_factor = risk_estimation_factor
self.data_provider = data_provider
self.logger = qf_logger.getChild(self.__class__.__name__)
Expand Down Expand Up @@ -83,7 +82,7 @@ def calculate_exposure(self, ticker: Ticker, current_exposure: Exposure, current
"""
Returns the expected Exposure, which is the key part of a generated Signal. Exposure suggests the trend
direction for managing the trading position.
Uses DataHandler passed when the AlphaModel (child) is initialized - all required data is provided in the child
Uses DataProvider passed when the AlphaModel (child) is initialized - all required data is provided in the child
class.
Parameters
Expand Down
8 changes: 4 additions & 4 deletions qf_lib/backtesting/alpha_model/futures_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from qf_lib.containers.futures.futures_adjustment_method import FuturesAdjustmentMethod
from qf_lib.containers.futures.futures_chain import FuturesChain
from qf_lib.containers.series.qf_series import QFSeries
from qf_lib.data_providers.data_provider import DataProvider
from qf_lib.data_providers.abstract_price_data_provider import AbstractPriceDataProvider


class FuturesModel(AlphaModel, metaclass=abc.ABCMeta):
Expand All @@ -51,14 +51,14 @@ class FuturesModel(AlphaModel, metaclass=abc.ABCMeta):
risk_estimation_factor: float
float value which estimates the risk level of the specific AlphaModel. Corresponds to the level at which
the stop-loss should be placed.
data_provider: DataProvider
DataProvider which provides data for the ticker. For backtesting purposes the Data Handler should be used.
data_provider: AbstractPriceDataProvider
DataProvider which provides data for the ticker
cache_path: Optional[str]
path to a directory, which could be used by the model for caching purposes. If provided, the model will cache
the outputs of get_data function.
"""

def __init__(self, num_of_bars_needed: int, risk_estimation_factor: float, data_provider: DataProvider,
def __init__(self, num_of_bars_needed: int, risk_estimation_factor: float, data_provider: AbstractPriceDataProvider,
cache_path: Optional[str] = None):

super().__init__(risk_estimation_factor, data_provider)
Expand Down
Loading

0 comments on commit db26e2a

Please sign in to comment.