Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Currencies in backtest #164

Merged
merged 42 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2538007
First draft of currency parameter in backtest
eirik-thorp Jun 3, 2024
e4cd1a5
Fix trailing whitespace
eirik-thorp Jun 3, 2024
9a8e7a4
Update currency implementation for backtest
eirik-thorp Jun 4, 2024
c4365b3
Add exchange rate ticker class
eirik-thorp Jun 4, 2024
b33ffe7
Fix Binance ticker
eirik-thorp Jun 4, 2024
b2a67db
Merge branch 'master' of github.com:quarkfin/qf-lib into backtest-cur…
eirik-thorp Jul 8, 2024
5c7c500
Removed ColorBar and ValueAnnotations from HeatmapChart. Added defaul…
Jul 9, 2024
0bab80d
Updated heatmap chart
Jul 10, 2024
066818f
Merge branch 'heatmap_fix' of github.com:quarkfin/qf-lib into backtes…
eirik-thorp Jul 10, 2024
76cb301
Remove currency enum and refactor currencies in backtest
eirik-thorp Jul 12, 2024
67a7416
Merge branch 'master' of github.com:quarkfin/qf-lib into backtest-cur…
eirik-thorp Jul 12, 2024
2557623
Remove currency enum file
eirik-thorp Jul 12, 2024
9652658
Fix target division error causing tests to fail
eirik-thorp Jul 12, 2024
e99779d
Merge branch 'master' of github.com:quarkfin/qf-lib into backtest-cur…
eirik-thorp Aug 16, 2024
a0c6b45
Add tests for portfolio with currency
eirik-thorp Sep 3, 2024
0fa4238
Add dummy tickers for currency exchange
eirik-thorp Sep 3, 2024
8bee4d9
Remove unused import
eirik-thorp Sep 3, 2024
a6e40e9
Reformat tests
eirik-thorp Sep 3, 2024
280db2d
Add tests for portfolio with currency
eirik-thorp Sep 10, 2024
f5c7bae
change naming in currency ticker
eirik-thorp Nov 5, 2024
8e9cda2
Fix tests and remove dummy exchange ticker
eirik-thorp Nov 5, 2024
b0c3e21
Fix trailing whitespace
eirik-thorp Nov 5, 2024
35c6cb6
Refactor currency exhanges
eirik-thorp Nov 6, 2024
16d827a
Remove abstract method from base data provider class
eirik-thorp Nov 6, 2024
127bc4a
Add currencies to data handler
eirik-thorp Nov 7, 2024
a8ba3b8
Fix exchange rate ticker in backtest trading session
eirik-thorp Nov 7, 2024
4ac00d2
Add support for currencies in prefetching dataprovider
eirik-thorp Nov 11, 2024
4079b20
Remove unused import
eirik-thorp Nov 11, 2024
18104ea
Avoid currency conversion between identical currencies
eirik-thorp Nov 11, 2024
7edfdb5
Refactor currency exchange
eirik-thorp Nov 14, 2024
c36c2c2
Add default frequency to mock dataprovider in portfolio tests
eirik-thorp Nov 14, 2024
3af8400
Change default currency in backtest trading session builder to None
eirik-thorp Nov 14, 2024
1ca67e6
Add pass to abstract class method
eirik-thorp Nov 15, 2024
2fa4b7c
Refactor data handler
eirik-thorp Nov 26, 2024
4031aad
Fix portfolio.py
eirik-thorp Nov 26, 2024
4b2fc72
Remove data handler from tests
eirik-thorp Nov 26, 2024
bb53f48
Refactor data_handler
eirik-thorp Nov 28, 2024
d783714
Fix flake8
eirik-thorp Nov 28, 2024
aa04e56
Add info in NotImplementedError
eirik-thorp Nov 29, 2024
5f56d97
Add test to assert NotImplementedError
eirik-thorp Nov 29, 2024
3367253
Remove hardcoded values
eirik-thorp Nov 29, 2024
6626c0d
Minor changes
eirik-thorp Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions qf_lib/backtesting/broker/backtest_broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@
self.portfolio = portfolio
self.execution_handler = execution_handler

def get_portfolio_value(self) -> Optional[float]:
return self.portfolio.net_liquidation
def get_portfolio_value(self, currency: str = None) -> Optional[float]:
if currency:
return self.portfolio.net_liquidation_in_currency(currency)

Check warning on line 34 in qf_lib/backtesting/broker/backtest_broker.py

View check run for this annotation

Codecov / codecov/patch

qf_lib/backtesting/broker/backtest_broker.py#L34

Added line #L34 was not covered by tests
eirik-thorp marked this conversation as resolved.
Show resolved Hide resolved
else:
return self.portfolio.net_liquidation

def get_positions(self) -> List[Position]:
return list(self.portfolio.open_positions_dict.values())
Expand Down
2 changes: 1 addition & 1 deletion qf_lib/backtesting/broker/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self, contract_ticker_mapper: ContractTickerMapper):
self.contract_ticker_mapper = contract_ticker_mapper

@abstractmethod
def get_portfolio_value(self) -> float:
def get_portfolio_value(self, currency: Optional[str] = None) -> float:
pass

@abstractmethod
Expand Down
6 changes: 2 additions & 4 deletions qf_lib/backtesting/order/order_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,7 @@ def percent_orders(self, percentages: Mapping[Ticker, float], execution_style: E
"""
self._log_function_call(vars())
self._check_tickers_type(list(percentages.keys()))
portfolio_value = self.broker.get_portfolio_value()
values = {ticker: portfolio_value * fraction for ticker, fraction in percentages.items()}
values = {ticker: self.broker.get_portfolio_value(ticker.currency) * fraction for ticker, fraction in percentages.items()}

return self.value_orders(values, execution_style, time_in_force, frequency)

Expand Down Expand Up @@ -291,9 +290,8 @@ def target_percent_orders(self, target_percentages: Mapping[Ticker, float], exec
assert 0.0 <= tolerance_percentage < 1.0, "The tolerance_percentage should belong to [0, 1) interval"
self._check_tickers_type(list(target_percentages.keys()))

portfolio_value = self.broker.get_portfolio_value()
target_values = {
ticker: portfolio_value * target_percent for ticker, target_percent in target_percentages.items()
ticker: self.broker.get_portfolio_value(ticker.currency) * target_percent for ticker, target_percent in target_percentages.items()
}
return self.target_value_orders(target_values, execution_style, time_in_force, tolerance_percentage, frequency)

Expand Down
39 changes: 34 additions & 5 deletions qf_lib/backtesting/portfolio/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import datetime
from typing import List, Dict
from typing import List, Dict, Optional

from qf_lib.backtesting.portfolio.backtest_position import BacktestPosition, BacktestPositionSummary
from qf_lib.backtesting.portfolio.position_factory import BacktestPositionFactory
Expand All @@ -24,16 +24,18 @@
from qf_lib.containers.series.prices_series import PricesSeries
from qf_lib.containers.series.qf_series import QFSeries
from qf_lib.data_providers.abstract_price_data_provider import AbstractPriceDataProvider
from qf_lib.data_providers.exchange_rate_provider import ExchangeRateProvider


class Portfolio:
def __init__(self, data_provider: AbstractPriceDataProvider, initial_cash: float):
def __init__(self, data_provider: AbstractPriceDataProvider, initial_cash: float, currency: Optional[str] = None):
"""
On creation, the Portfolio object contains no positions and all values are "reset" to the initial
cash, with no PnL.
"""
self.initial_cash = initial_cash
self.data_provider = data_provider
self.currency = currency

self.net_liquidation = initial_cash
""" Cash value includes futures P&L + stock value + securities options value + bond value + fund value. """
Expand Down Expand Up @@ -62,6 +64,27 @@

self.logger = qf_logger.getChild(self.__class__.__name__)

def _current_exchange_rate(self, currency: str) -> float:
"""Last available exchange rate from the specified currency to the portfolio currency."""
if currency == self.currency:
return 1.

Check warning on line 70 in qf_lib/backtesting/portfolio/portfolio.py

View check run for this annotation

Codecov / codecov/patch

qf_lib/backtesting/portfolio/portfolio.py#L70

Added line #L70 was not covered by tests

if isinstance(self.data_provider, ExchangeRateProvider):
return self.data_provider.get_last_available_exchange_rate(currency, self.currency,
frequency=self.data_provider.frequency)
else:
raise NotImplementedError(f"Portfolio currency is set to {self.currency} but {type(self.data_provider)} "
"does not extend ExchangeRateProvider.")

def net_liquidation_in_currency(self, currency: str = None) -> float:
"""Converts the current net liquidation from the portfolio currency into the specified currency"""
if currency == self.currency:
return self.net_liquidation
elif self.currency is not None:
return self.net_liquidation/self._current_exchange_rate(currency)
else:
raise ValueError("Portfolio currency is not specified.")

def transact_transaction(self, transaction: Transaction):
"""
Adjusts positions to account for a transaction.
Expand All @@ -88,7 +111,10 @@
new_position = self._create_new_position(remaining_transaction)
transaction_cost += new_position.transact_transaction(remaining_transaction)

self.current_cash += transaction_cost
if self.currency is not None:
self.current_cash += transaction_cost*self._current_exchange_rate(transaction.ticker.currency)
else:
self.current_cash += transaction_cost

def update(self, record=False):
"""
Expand All @@ -109,8 +135,11 @@
position.update_price(bid_price=security_price, ask_price=security_price)
position_value = position.market_value()
position_exposure = position.total_exposure()
self.net_liquidation += position_value
self.gross_exposure_of_positions += abs(position_exposure)

current_exchange_rate = self._current_exchange_rate(ticker.currency) if self.currency is not None else 1.
self.net_liquidation += position_value*current_exchange_rate
self.gross_exposure_of_positions += abs(position_exposure)*current_exchange_rate

if record:
current_positions[ticker] = BacktestPositionSummary(position)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def _compute_target_value(self, signal: Signal, frequency=Frequency.DAILY) -> fl
"""
ticker: Ticker = signal.ticker

portfolio_value = self._broker.get_portfolio_value()
portfolio_value = self._broker.get_portfolio_value(ticker.currency)
target_percentage = self._compute_target_percentage(signal)
target_value = portfolio_value * target_percentage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from qf_lib.common.utils.miscellaneous.to_list_conversion import convert_to_list
from qf_lib.containers.helpers import compute_container_hash
from qf_lib.data_providers.abstract_price_data_provider import AbstractPriceDataProvider
from qf_lib.data_providers.exchange_rate_provider import ExchangeRateProvider
from qf_lib.data_providers.prefetching_data_provider import PrefetchingDataProvider


Expand Down Expand Up @@ -80,6 +81,14 @@ def use_data_preloading(self, tickers: Union[Ticker, Sequence[Ticker]], time_del
# the same set of tickers and fields
tickers, _ = convert_to_list(tickers, Ticker)

if self.portfolio.currency is not None and isinstance(self.data_provider, ExchangeRateProvider):
currencies = set(ticker.currency for ticker in tickers)
currencies.discard(self.portfolio.currency)
if currencies:
tickers = tickers + [
self.data_provider.create_exchange_rate_ticker(currency, self.portfolio.currency) for currency in currencies
]

self.data_provider = PrefetchingDataProvider(self.data_provider, sorted(tickers), sorted(PriceField.ohlcv()),
data_start, self.end_date, self.frequency,
timer=self.data_provider.timer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def __init__(self, settings: Settings, pdf_exporter: PDFExporter, excel_exporter

self._backtest_name = "Backtest Results"
self._initial_cash = 10000000
self._currency = None
self._initial_risk = None
self._benchmark_tms = None
self._monitor_settings = None
Expand Down Expand Up @@ -195,6 +196,17 @@ def set_initial_cash(self, initial_cash: int):
assert type(initial_cash) is int and initial_cash > 0
self._initial_cash = initial_cash

@ConfigExporter.update_config
def set_portfolio_currency(self, currency: str):
"""Sets the portfolio currency.

Parameters
-----------
currency: str
ISO code of the portfolio currency (Ex. 'EUR' for Euro)
"""
self._currency = currency

@ConfigExporter.update_config
def set_initial_risk(self, initial_risk: float):
"""Sets the initial risk value.
Expand Down Expand Up @@ -392,7 +404,7 @@ def build(self, start_date: datetime, end_date: datetime) -> BacktestTradingSess

signals_register = self._signals_register if self._signals_register else BacktestSignalsRegister()

self._portfolio = Portfolio(self._data_provider, self._initial_cash)
self._portfolio = Portfolio(self._data_provider, self._initial_cash, self._currency)

self._backtest_result = BacktestResult(self._portfolio, signals_register, self._backtest_name, start_date,
end_date, self._initial_risk)
Expand Down
Loading