Skip to content

Commit

Permalink
Working new universe setup.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sinbad-The-Sailor committed Feb 24, 2024
1 parent c7e4ec3 commit 23f3715
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 102 deletions.
85 changes: 23 additions & 62 deletions backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,81 +3,42 @@
import numpy as np
import pandas as pd

from datetime import datetime as dt
from matplotlib import pyplot
from tests.test_config import TEST_YAHOO_STOCK_UNIVERSE_16, TEST_YAHOO_STOCK_UNIVERSE_8, TEST_YAHOO_STOCK_UNIVERSE_4
from src.abacus.utils.instrument import Instrument
from src.abacus.utils.portfolio import Portfolio
from src.abacus.utils.universe import Universe
from src.abacus.simulator.simulator import Simulator
from src.abacus.assessor.risk_assessor import RiskAssessor
from src.abacus.optimizer.optimizer import SPMaximumUtility, MPCMaximumUtility, MPCMaximumReturn
from src.abacus.optimizer.optimizer import MPCMaximumReturn


"""
Starting 2020, monthly updates to 2023.
Record portfolio weights
Record portfolio returns
Record 1/n returns
"""

time_series_data = {}
instruments = []
initial_weights = {}
inital_holdings = {}
inital_cash = 10_000

# TODO: Should be in a universe class maybe...
instrument_mapping = {}
for id, ticker in enumerate(sorted(TEST_YAHOO_STOCK_UNIVERSE_8)):
instrument_specification = {}
inital_weights = {}
wealth = []
number_of_start_assets = 5
for i, ticker in enumerate(sorted(TEST_YAHOO_STOCK_UNIVERSE_8)):
file = f"tests/data/{ticker}.csv"
time_series = pd.read_csv(file, index_col='Date')
time_series_data[ticker] = time_series
ins = Instrument(id, ticker, "Stock", None)
instruments.append(ins)
initial_weights[ins] = 1 / len(TEST_YAHOO_STOCK_UNIVERSE_8)
inital_holdings[ins] = 10
instrument_mapping[ticker] = ins
instrument_specification[ticker] = time_series
if i < number_of_start_assets: inital_weights[ticker] = 1 / number_of_start_assets

universe = Universe(instrument_specifications=instrument_specification)
portfolio = Portfolio(weights=inital_weights)
simulator = Simulator(universe)
simulator.calibrate()
simulator.run_simulation(time_steps=5, number_of_simulations=100)

ts = time_series_data["XOM"]
# Date range for backtesting.
start_date = "2020-01-02"
end_date = "2020-01-03" # "2023-05-31"
us_bd = CustomBusinessDay(calendar=USFederalHolidayCalendar())
pr = pd.date_range(start=start_date, end=end_date, freq='B')

portfolio1 = Portfolio(weights=initial_weights)
portfolio2 = Portfolio(weights=initial_weights)
portfolio3 = Portfolio(holdings=inital_holdings, cash=inital_cash)


wealth = np.zeros(len(pr))
for i, date in enumerate(pr):

date_range = pd.date_range(start=start_date, end=end_date, freq='B')

# Build universe.
for ins in instruments:
ins.price_history = time_series_data[ins.identifier].loc[:str(date)]

# Build simulator.
simulator = Simulator(instruments)
for date in date_range:
universe.date_today = date
simulator = Simulator(universe)
simulator.calibrate()
simulator.run_simulation(time_steps=5, number_of_simulations=25)

# Run optimizer on portfolio.
optimizer = MPCMaximumReturn(portfolio1, simulator.return_tensor, gamma=10, l1_penalty=0, l2_penalty=1, covariance_matrix=simulator.covariance_matrix)
optimizer = MPCMaximumReturn(universe, portfolio, simulator.return_tensor, gamma=1, l1_penalty=0, l2_penalty=0.05, covariance_matrix=simulator.covariance_matrix)
optimizer.solve()
solution = optimizer.solution
solution = {instrument_mapping[ticker]: weight for ticker, weight in solution.items()}
print(solution)
# optimizer = MPCMaximumUtility(portfolio2, simulator.return_tensor, gamma=1)
# optimizer.solve()

# optimizer = SPMaximumUtility(portfolio3, simulator.price_tensor, simulator._inital_prices, gamma=-3)
# optimizer.solve()

exit()


# Update portfolio weights.

# Record portfolio wealth.
wealth[i] = 0
print(optimizer.solution)
break
93 changes: 56 additions & 37 deletions src/abacus/optimizer/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import ClassVar

from src.abacus.utils.portfolio import Portfolio
from src.abacus.utils.universe import Universe
from src.abacus.config import DEFAULT_SOLVER
from src.abacus.utils.enumerations import OptimizationSpecifications

Expand All @@ -17,9 +18,11 @@ class OptimizationModel(ABC):

_model_specification: ClassVar[str]

def __init__(self, portfolio: Portfolio, simulation_tensor: torch.Tensor, solver: str=DEFAULT_SOLVER):
def __init__(self, universe: Universe, portfolio: Portfolio, simulation_tensor: torch.Tensor, solver: str=DEFAULT_SOLVER):
self._portfolio = portfolio
self._simulation_tensor = simulation_tensor
self._universe = universe
self._asset_identifiers = universe.instrument_identifiers
self._solver = solver
self._solved = False
self._ampl = None
Expand Down Expand Up @@ -59,8 +62,8 @@ class SPMaximumUtility(OptimizationModel):

_model_specification = OptimizationSpecifications.SP_MAXIMIZE_UTILITY

def __init__(self, portfolio: Portfolio, price_tensor: torch.Tensor, inital_prices: torch.Tensor, gamma: float):
super().__init__(portfolio, price_tensor)
def __init__(self, universe: Universe, portfolio: Portfolio, price_tensor: torch.Tensor, inital_prices: torch.Tensor, gamma: float):
super().__init__(universe, portfolio, price_tensor)
self._inital_prices = inital_prices
self._gamma = gamma

Expand Down Expand Up @@ -99,8 +102,8 @@ class MPCMaximumUtility(OptimizationModel):

_model_specification = OptimizationSpecifications.MPC_MAXIMIZE_UTILITY

def __init__(self, portfolio: Portfolio, return_tensor: torch.Tensor, gamma: float):
super().__init__(portfolio, return_tensor)
def __init__(self, universe: Universe, portfolio: Portfolio, return_tensor: torch.Tensor, gamma: float):
super().__init__(universe, portfolio, return_tensor)
self._gamma = gamma

@property
Expand All @@ -116,14 +119,13 @@ def _set_ampl_data(self):
# TODO: Add these as properties in superclass.
assets = self._portfolio.instruments
inital_weights = self._portfolio.weights
asset_identifiers = [instrument.identifier for instrument in assets]
inital_weights = dict(zip(asset_identifiers, inital_weights.values()))
inital_weights = dict(zip(self._asset_identifiers, inital_weights.values()))
expected_return_tensor = np.array(self._return_expectation_tensor)
tensor_size = expected_return_tensor.shape
number_of_time_steps = tensor_size[1]
return_dict = {(j+1, asset.identifier): expected_return_tensor[asset.id][j] for asset in assets for j in range(number_of_time_steps)}

self._ampl.get_set("assets").set_values(asset_identifiers)
self._ampl.get_set("assets").set_values(self._asset_identifiers)
self._ampl.param["gamma"] = self._gamma
self._ampl.param["number_of_time_steps"] = number_of_time_steps
self._ampl.param["inital_weights"] = inital_weights
Expand All @@ -134,50 +136,67 @@ class MPCMaximumReturn(OptimizationModel):

_model_specification = OptimizationSpecifications.MPC_MAXIMIZE_RETURN

def __init__(self, portfolio: Portfolio, simulation_tensor: torch.Tensor, gamma: float, l1_penalty: float, l2_penalty: float,
covariance_matrix: torch.Tensor):
super().__init__(portfolio, simulation_tensor)
def __init__(self, universe: Universe, portfolio: Portfolio, simulation_tensor: torch.Tensor, gamma: float,
l1_penalty: float, l2_penalty: float, covariance_matrix: torch.Tensor):
super().__init__(universe, portfolio, simulation_tensor)
self._gamma = gamma
self._l1_penalty = l1_penalty
self._l2_penalty = l2_penalty
self._covariance_matrix = covariance_matrix

@property
def _return_expectation_tensor(self):
return torch.mean(self._simulation_tensor, dim=2)
def solve(self):
super().solve()

@property
def solution(self):
self._check_solved()
# TODO: Should be general?
ampl_output = self._ampl.get_variable("weights").to_pandas().loc[1].to_dict()["weights.val"]
# print(self._ampl.eval("display OBJECTIVE;"))
return ampl_output

def solve(self):
# TODO: Should weights be a configuration variable?
super().solve()
#print(self._ampl.eval("display OBJECTIVE;"))
@property
def _return_expectation_tensor(self):
return torch.mean(self._simulation_tensor, dim=2)

@property
def _assets(self):
return self._universe.instruments

def _set_ampl_data(self):
inital_weights = self._portfolio.weights
assets = self._portfolio.instruments
asset_identifiers = [instrument.identifier for instrument in assets]
inital_weights = dict(zip(asset_identifiers, inital_weights.values()))
expected_return_tensor = np.array(self._return_expectation_tensor)
tensor_size = expected_return_tensor.shape
number_of_time_steps = tensor_size[1]
return_dict = {(j+1, asset.identifier): expected_return_tensor[asset.id][j] for asset in assets for j in range(number_of_time_steps)}
covariance_matrix = np.array(self._covariance_matrix)
@property
def _inital_weights(self):
portfolio_weights = self._portfolio.weights
universe_instruments = self._universe.instrument_identifiers
return {identifier: portfolio_weights.get(identifier, 0) for identifier in universe_instruments}

l1_penalty_vector = self._l1_penalty * np.ones(len(assets))
l2_penalty_vector = self._l2_penalty * np.ones(len(assets))
@property
def _instrument_returns(self):
expected_return_array = np.array(self._return_expectation_tensor)
return {(j+1, asset.identifier): expected_return_array[asset.id][j] for asset in self._assets
for j in range(self._number_of_time_steps)}

self._ampl.get_set("assets").set_values(asset_identifiers)
@property
def _number_of_time_steps(self):
return self._return_expectation_tensor.shape[1]

@property
def _number_of_assets(self):
return len(self._universe.instruments)

@property
def _l1_penalty_array(self):
return self._l1_penalty * np.ones(self._number_of_assets)

@property
def _l2_penalty_array(self):
return self._l2_penalty * np.ones(self._number_of_assets)

def _set_ampl_data(self):
self._ampl.get_set("assets").set_values(self._asset_identifiers)
self._ampl.param["gamma"] = self._gamma
self._ampl.param["number_of_time_steps"] = number_of_time_steps
self._ampl.param["inital_weights"] = inital_weights
self._ampl.param["returns"] = return_dict
self._ampl.param["covariance"] = covariance_matrix
self._ampl.param["l1_penalty"] = l1_penalty_vector
self._ampl.param["l2_penalty"] = l2_penalty_vector
self._ampl.param["number_of_time_steps"] = self._number_of_time_steps
self._ampl.param["inital_weights"] = self._inital_weights
self._ampl.param["returns"] = self._instrument_returns
self._ampl.param["covariance"] = np.array(self._covariance_matrix)
self._ampl.param["l1_penalty"] = self._l1_penalty_array
self._ampl.param["l2_penalty"] = self._l2_penalty_array
7 changes: 4 additions & 3 deletions src/abacus/simulator/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
import numpy as np
import pyvinecopulib as pv

from src.abacus.utils.instrument import Instrument
from src.abacus.utils.exceptions import ParameterError
from src.abacus.utils.enumerations import DataTypes
from src.abacus.utils.universe import Universe
from src.abacus.simulator.model_selector import ModelSelector
from src.abacus.config import VINE_COPULA_FAMILIES, VINE_COPULA_NUMBER_OF_THREADS



class Simulator:
"""
TODO: Add description of resulting tensor.
"""

def __init__(self, instruments: list[Instrument]):
def __init__(self, universe: Universe):
self._model_selector = ModelSelector()
self._instruments = sorted(instruments, key=lambda x: x.identifier)
self._instruments = universe.instruments
self._calibrated = False
self._return_tensor = None
self._price_tensor = None
Expand Down
46 changes: 46 additions & 0 deletions src/abacus/utils/universe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-

from pandas import DataFrame
from datetime import date
from src.abacus.utils.instrument import Instrument



class Universe:

def __init__(self, instrument_specifications: dict[str:DataFrame], date_today: str=str(date.today())):
self._instrument_specifications = instrument_specifications
self._date_today = date_today
self._instruments = None
self._instrument_build_date = None

@property
def date_today(self):
return self._date_today

@date_today.setter
def date_today(self, new):
self._date_today = new

@property
def instrument_identifiers(self):
return sorted(self._instrument_specifications.keys())

@property
def instruments(self) -> list[Instrument]:
if self.has_updated_cache():
return self._instruments

built_instruments = []
for id, identifier in enumerate(self.instrument_identifiers):
time_series = self._instrument_specifications[identifier].loc[:str(self.date_today)]
ins = Instrument(id, identifier, "Stock", time_series)
built_instruments.append(ins)
return built_instruments

def has_updated_cache(self) -> bool:
has_cache = self._instruments is not None
has_last_build = self._instrument_build_date is not None
updated_date = self._date_today == self._instrument_build_date
conditions = has_cache, has_last_build, updated_date
return any(conditions)

0 comments on commit 23f3715

Please sign in to comment.