diff --git a/makefile b/makefile index d6805d4..cdbd832 100644 --- a/makefile +++ b/makefile @@ -11,7 +11,7 @@ venv: . venv/bin/activate && exec zsh env: - @source .env + @source .env; lint: @black src/. diff --git a/run.py b/run.py index 4a97c82..893bb1f 100644 --- a/run.py +++ b/run.py @@ -2,27 +2,50 @@ import pandas as pd from matplotlib import pyplot -from tests.test_config import TEST_YAHOO_STOCK_UNIVERSE_16 +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.simulator.simulator import Simulator +from src.abacus.assessor.risk_assessor import RiskAssessor +from src.abacus.optimizer.optimizer import SPMaximumUtility + + +# Mock instrument creation... instruments = [] -for ticker in TEST_YAHOO_STOCK_UNIVERSE_16[0:3]: +for id, ticker in enumerate(sorted(TEST_YAHOO_STOCK_UNIVERSE_4)): file = f"tests/data/{ticker}.csv" time_series = pd.read_csv(file, index_col='Date') - ins = Instrument(ticker, "Stock", time_series) + ins = Instrument(id, ticker, "Stock", time_series) instruments.append(ins) +# Simulation ... simulator = Simulator(instruments) simulator.calibrate() -simulator.run_simulation(500, 50) +simulator.run_simulation(time_steps=10, number_of_simulations=1000) + +# Portfolio creation... +holdings = 500, 200, 100, 125 +holdings = dict(zip(instruments[0:4], holdings)) +print(holdings) +cash = 100_000 +portfolio = Portfolio(holdings, cash) +# Risk assessor creation... +assessor = RiskAssessor(portfolio=portfolio, return_tensor=simulator.return_tensor, time_step=5) +assessor.summary() -for i in range(50): +# Display reasonableness of simulations... +for i in range(25): y = simulator.price_tensor[0,:,i] x = [i for i in range(len(y))] pyplot.plot(x, y) pyplot.show() +# Create optimizer with different optimization models... +optimizer = SPMaximumUtility(portfolio, simulator.price_tensor) +optimizer.solve() + + print("OK!") diff --git a/src/abacus/__init__.py b/src/abacus/__init__.py index d914c9f..5e42474 100644 --- a/src/abacus/__init__.py +++ b/src/abacus/__init__.py @@ -2,5 +2,7 @@ import logging import logging.config + + logging.config.fileConfig("src/abacus/logging.cfg") logger = logging.getLogger(__name__) diff --git a/src/abacus/assessor/__init__.py b/src/abacus/assessor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/abacus/utils/risk_assessor.py b/src/abacus/assessor/risk_assessor.py similarity index 90% rename from src/abacus/utils/risk_assessor.py rename to src/abacus/assessor/risk_assessor.py index 898dfe8..79cbadc 100644 --- a/src/abacus/utils/risk_assessor.py +++ b/src/abacus/assessor/risk_assessor.py @@ -4,8 +4,8 @@ from scipy.optimize import minimize -from utils.config import EVT_THRESHOLD, GEV_INITIAL_SOLUTION -from utils.portfolio import Portfolio +from src.abacus.config import EVT_THRESHOLD, GEV_INITIAL_SOLUTION +from src.abacus.utils.portfolio import Portfolio @@ -25,9 +25,16 @@ def __init__(self, portfolio: Portfolio, return_tensor: torch.Tensor, time_step: self._return_tensor = return_tensor self._time_step = time_step self._calibrated = False - self._check_time_step() + def summary(self): + headers = "VaR", "Extreme VaR", "ES", "Extreme ES" + confidence_99 = self.value_at_risk(0.99), self.extreme_value_at_risk(0.99), self.expected_shortfall(0.99), self.extreme_expected_shortfall(0.99) + confidence_999 = self.value_at_risk(0.999), self.extreme_value_at_risk(0.999), self.expected_shortfall(0.999), self.extreme_expected_shortfall(0.999) + print(*headers) + print(*confidence_99) + print(*confidence_999) + @property def _evt_threshold(self) -> torch.Tensor: return torch.quantile(self._portfolio_loss_scenarios, torch.tensor(EVT_THRESHOLD)) @@ -44,8 +51,7 @@ def _return_matrix(self) -> torch.Tensor: @property def _reduced_return_matrix(self) -> torch.Tensor: - instrument_ids = [instrument.id for instrument in self._portfolio.weights] - sorted_ids = torch.tensor(sorted(instrument_ids)) + sorted_ids = torch.tensor(self._portfolio.indices) return torch.index_select(self._return_matrix, dim=0, index=sorted_ids) @property @@ -55,8 +61,8 @@ def _maxmimum_time_step(self): @property def _weights(self) -> torch.Tensor: portfolio_weights = self._portfolio.weights + sorted_keys = sorted(list(portfolio_weights.keys()), key=lambda x: x.id) weights = torch.empty(len(portfolio_weights)) - sorted_keys = sorted(list(portfolio_weights), key=lambda x: x.id) for i, key in enumerate(sorted_keys): weights[i] = portfolio_weights[key] return weights @@ -73,7 +79,6 @@ def _portfolio_loss_scenarios(self) -> torch.Tensor: def _constraints(self) -> list[dict]: constraints = [] observations = self._excess_portfolio_losses - constraints.append({"type": "ineq", "fun": lambda x: x[1]}) for observation in observations: @@ -158,9 +163,6 @@ def extreme_expected_shortfall(self, confidence_level: float) -> float: threshold = self._evt_threshold parameters = torch.tensor(self._parameters) xi, beta = parameters[0], parameters[1] - - print(xi, beta) - extreme_es = extreme_var / (1 - xi) + (beta - threshold * xi) /(1 - xi) return extreme_es.item() diff --git a/src/abacus/utils/header.txt b/src/abacus/assets/header.txt similarity index 100% rename from src/abacus/utils/header.txt rename to src/abacus/assets/header.txt diff --git a/src/abacus/config.py b/src/abacus/config.py index a41fc25..7514015 100644 --- a/src/abacus/config.py +++ b/src/abacus/config.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import pyvinecopulib as pv + + # Processes MAXIMUM_AR_ORDER = 5 INITIAL_VARIANCE_GARCH_OBSERVATIONS = 20 diff --git a/src/abacus/utils/main.py b/src/abacus/legacy/leg_main.py similarity index 100% rename from src/abacus/utils/main.py rename to src/abacus/legacy/leg_main.py diff --git a/src/abacus/legacy/leg_model.py b/src/abacus/legacy/leg_model.py deleted file mode 100644 index ed717e9..0000000 --- a/src/abacus/legacy/leg_model.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -import numpy as np - -from abc import ABC, abstractmethod - - -class Model(ABC): - """ - Abstract class representing a model for an instrument. - - Attributes: - data (np.array): Dataset used for parameter estimation. - solution (np.array): optimal parameters of the model. - """ - - def __init__(self, data: np.array) -> None: - self.data = data - self.solution = None - - @property - def initial_solution(self) -> np.array: - pass - - @property - def mse(self) -> float: - pass - - @abstractmethod - def fit_model(self) -> np.array: - pass - - @abstractmethod - def _cost_function(self) -> float: - pass - - @abstractmethod - def run_simulation(self, number_of_steps: int) -> np.array: - pass - - @abstractmethod - def transform_to_true(self) -> np.array: - pass - - @abstractmethod - def transform_to_uniform(self) -> np.array: - pass - - -class StationarityError(ValueError): - pass - - -class NoParametersError(ValueError): - pass diff --git a/src/abacus/legacy/leg_model_factory.py b/src/abacus/legacy/leg_model_factory.py deleted file mode 100644 index 5789155..0000000 --- a/src/abacus/legacy/leg_model_factory.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -import numpy as np -from abacus.config import ADMISSIBLE_EQUTIY_MODELS, ADMISSIBLE_FX_MODELS - -from abacus.instruments import FX, Equity, Instrument -from abacus.simulator.ar import AR -from abacus.simulator.garch import GARCH -from abacus.simulator.gjr_grach import GJRGARCH -from abacus.simulator.ma import MA -from abacus.simulator.model import Model -from abacus.simulator.nnar import NNAR - - -class ModelFactory: - """ - Model factory for instruments. Picks the most appropriate model for each - instrument using an error estimate. - - Currently using MSE (Mean Squared Error). - """ - - def __init__(self, instruments: list[Instrument]) -> None: - self.instruments = instruments - - @staticmethod - def build_model(data, model_name, *args) -> Model: - if model_name == "AR": - return [AR(data, p=arg) for arg in args] - elif model_name == "MA": - return [MA(data, q=arg) for arg in args] - elif model_name == "NNAR": - return [NNAR(data, p=arg) for arg in args] - elif model_name == "GARCH": - return [GARCH(data)] - elif model_name == "GJRGARCH": - return [GJRGARCH(data)] - else: - raise ValueError(f"Model {model_name} not available.") - - def select_model(self, instrument: Instrument) -> None: - current_MSE = np.Inf - current_model = None - - if type(instrument) is Equity: - for model_name, hyperparameters in ADMISSIBLE_EQUTIY_MODELS.items(): - potential_models = self.build_model( - np.array(instrument.log_return_history), - model_name, - *hyperparameters, - ) - for potential_model in potential_models: - potential_model.fit_model() - if potential_model.mse < current_MSE: - current_MSE = potential_model.mse - current_model = potential_model - - elif type(instrument) is FX: - for model_name, hyperparameters in ADMISSIBLE_EQUTIY_MODELS.items(): - potential_models = self.build_model( - np.array(instrument.log_return_history), - model_name, - *hyperparameters, - ) - for potential_model in potential_models: - potential_model.fit_model() - if potential_model.mse < current_MSE: - current_MSE = potential_model.mse - current_model = potential_model - - instrument.set_model(current_model) - - def build_all(self) -> None: - """ - Applies a model builder for every instrument given. - """ - for instrument in self.instruments: - self.select_model(instrument) diff --git a/src/abacus/optimizer/leg_optimizer.py b/src/abacus/optimizer/leg_optimizer.py new file mode 100644 index 0000000..7ed976e --- /dev/null +++ b/src/abacus/optimizer/leg_optimizer.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +import os +import torch +import numpy as np + +from amplpy import AMPL, Environment + +from .enums import OptimizationModels +from utils.config import DEFAULT_SOLVER +from utils.portfolio import Portfolio + + + +class Optimizer: + + def __init__(self, portfolio: Portfolio, simulation_tensor: torch.Tensor, solver: str = DEFAULT_SOLVER): + self._portfolio = portfolio + self._simulation_tensor = simulation_tensor + self._solver = solver + self._ran = False + + def run(self): + self._initiate_ampl_engine() + self._set_ampl_data() + self._solve_optimzation_problem() + print(self._ampl.get_variable("x_buy").get_values()) + print(self._ampl.get_variable("x_sell").get_values()) + self._ran = True + + @property + def solution(self): + self._check_ran() + ... + + @property + def model(self): + return self._optimization_model + + @model.setter + def model(self, other): + self._optimization_model = other + + + def _initiate_ampl_engine(self): + environment = Environment(os.environ.get("AMPL_PATH")) + self._ampl = AMPL(environment) + self._ampl.option["solver"] = self._solver + self._ampl.read(f"optimization_models/{self._optimization_model.value}") + + def _set_ampl_data(self): + if self._optimization_model == OptimizationModels.SP_MAXIMIZE_UTILITY: + assets = self._portfolio.instruments + asset_identifiers = [instrument.identifier for instrument in assets] + instrument_holdings = np.array(list(self._portfolio.holdings.values())) + price_tensor = np.array(self._simulation_tensor[:,-1,:]) + tensor_size = price_tensor.shape + number_of_assets = tensor_size[0] + number_of_scenarios = tensor_size[1] + + price_dict = {(j+1, asset.identifier): price_tensor[asset.id][j] for asset in assets + for j in range(number_of_scenarios)} + + self._ampl.get_set("assets").set_values(asset_identifiers) + self._ampl.param["gamma"] = -24 + self._ampl.param["risk_free_rate"] = 0.04 + self._ampl.param["dt"] = 1/365 + self._ampl.param["number_of_assets"] = number_of_assets + self._ampl.param["number_of_scenarios"] = number_of_scenarios + self._ampl.param["inital_cash"] = self._portfolio._cash + self._ampl.param["inital_holdings"] = instrument_holdings + self._ampl.param["prices"] = price_dict + + + elif self._optimization_model == OptimizationModels.MPC_MAXIMIZE_UTILITY: + ... + + def _solve_optimzation_problem(self): + self._ampl.solve() + + def _check_ran(self): + if not self._ran: + raise ValueError("Optimizer has not been run.") diff --git a/src/abacus/optimizer/optimizer.py b/src/abacus/optimizer/optimizer.py index 7ed976e..eede165 100644 --- a/src/abacus/optimizer/optimizer.py +++ b/src/abacus/optimizer/optimizer.py @@ -1,82 +1,104 @@ # -*- coding: utf-8 -*- import os + import torch import numpy as np +import amplpy as ap +from abc import ABC, abstractmethod +from typing import ClassVar -from amplpy import AMPL, Environment +from src.abacus.utils.portfolio import Portfolio +from src.abacus.config import DEFAULT_SOLVER +from src.abacus.utils.enumerations import OptimizationSpecifications -from .enums import OptimizationModels -from utils.config import DEFAULT_SOLVER -from utils.portfolio import Portfolio +class OptimizationModel(ABC): -class Optimizer: + _model_specification: ClassVar[int] - def __init__(self, portfolio: Portfolio, simulation_tensor: torch.Tensor, solver: str = DEFAULT_SOLVER): + def __init__(self, portfolio: Portfolio, simulation_tensor: torch.Tensor, solver: str=DEFAULT_SOLVER): self._portfolio = portfolio self._simulation_tensor = simulation_tensor self._solver = solver - self._ran = False + self._solved = False + self._ampl = None - def run(self): + def solve(self): self._initiate_ampl_engine() self._set_ampl_data() self._solve_optimzation_problem() - print(self._ampl.get_variable("x_buy").get_values()) - print(self._ampl.get_variable("x_sell").get_values()) - self._ran = True + self._solved = True - @property - def solution(self): - self._check_ran() + @abstractmethod + def _set_ampl_data(self): ... - @property - def model(self): - return self._optimization_model - - @model.setter - def model(self, other): - self._optimization_model = other - - def _initiate_ampl_engine(self): - environment = Environment(os.environ.get("AMPL_PATH")) - self._ampl = AMPL(environment) + environment = ap.Environment(os.environ.get("AMPL_PATH")) + self._ampl = ap.AMPL(environment) self._ampl.option["solver"] = self._solver - self._ampl.read(f"optimization_models/{self._optimization_model.value}") - - def _set_ampl_data(self): - if self._optimization_model == OptimizationModels.SP_MAXIMIZE_UTILITY: - assets = self._portfolio.instruments - asset_identifiers = [instrument.identifier for instrument in assets] - instrument_holdings = np.array(list(self._portfolio.holdings.values())) - price_tensor = np.array(self._simulation_tensor[:,-1,:]) - tensor_size = price_tensor.shape - number_of_assets = tensor_size[0] - number_of_scenarios = tensor_size[1] - - price_dict = {(j+1, asset.identifier): price_tensor[asset.id][j] for asset in assets - for j in range(number_of_scenarios)} - - self._ampl.get_set("assets").set_values(asset_identifiers) - self._ampl.param["gamma"] = -24 - self._ampl.param["risk_free_rate"] = 0.04 - self._ampl.param["dt"] = 1/365 - self._ampl.param["number_of_assets"] = number_of_assets - self._ampl.param["number_of_scenarios"] = number_of_scenarios - self._ampl.param["inital_cash"] = self._portfolio._cash - self._ampl.param["inital_holdings"] = instrument_holdings - self._ampl.param["prices"] = price_dict - - - elif self._optimization_model == OptimizationModels.MPC_MAXIMIZE_UTILITY: - ... + self._ampl.read(f"src/abacus/optimizer/optimization_models/{self._model_specification.value}") def _solve_optimzation_problem(self): + self._check_initialization() self._ampl.solve() - def _check_ran(self): - if not self._ran: + def _check_solved(self): + if not self._solved: raise ValueError("Optimizer has not been run.") + + def _check_initialization(self): + if not self._ampl: + raise ValueError("AMPL has not been initalized.") + + def _check_valid_model_specification(self): + # TODO: Implement when approriate. + ... + + + +class SPMaximumUtility(OptimizationModel): + + _model_specification = OptimizationSpecifications.SP_MAXIMIZE_UTILITY + + def __init__(self, portfolio: Portfolio, price_tensor: torch.Tensor): + super().__init__(portfolio, price_tensor) + + def solve(self): + super().solve() + print(self._ampl.get_variable("x_buy").get_values()) + print(self._ampl.get_variable("x_sell").get_values()) + + def _set_ampl_data(self): + assets = self._portfolio.instruments + asset_identifiers = [instrument.identifier for instrument in assets] + instrument_holdings = np.array(list(self._portfolio.holdings.values())) + price_tensor = np.array(self._simulation_tensor[:,-1,:]) + tensor_size = price_tensor.shape + number_of_assets = tensor_size[0] + number_of_scenarios = tensor_size[1] + + price_dict = {(j+1, asset.identifier): price_tensor[asset.id][j] for asset in assets for j in range(number_of_scenarios)} + + self._ampl.get_set("assets").set_values(asset_identifiers) + self._ampl.param["gamma"] = -50 + self._ampl.param["risk_free_rate"] = 0.04 + self._ampl.param["dt"] = 10/365 + self._ampl.param["number_of_assets"] = number_of_assets + self._ampl.param["number_of_scenarios"] = number_of_scenarios + self._ampl.param["inital_cash"] = self._portfolio._cash + self._ampl.param["inital_holdings"] = instrument_holdings + self._ampl.param["prices"] = price_dict + + +class MPCMaximumUtility(OptimizationModel): + + _model_specification = OptimizationSpecifications.MPC_MAXIMIZE_UTILITY + + def __init__(self, portfolio: Portfolio, return_tensor: torch.Tensor): + super().__init__(portfolio, return_tensor) + raise NotImplementedError + + def _set_ampl_data(self): + return super()._set_ampl_data() diff --git a/src/abacus/utils/data_handler.py b/src/abacus/utils/data_handler.py deleted file mode 100644 index db5e9d5..0000000 --- a/src/abacus/utils/data_handler.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -import yfinance as yf - -from datetime import datetime -from abc import ABC, abstractmethod -from pandas_datareader import data as pdr - -from utils.price_history import PriceHistory - - - -class DataHandler(ABC): - @staticmethod - @abstractmethod - def get_price_history(identifier: str, start_date: datetime, end_date: datetime) -> PriceHistory: - ... - -class YahooDataHandler(DataHandler): - def __init__(self): - yf.pdr_override() - - @staticmethod - def get_price_history(identifier: str, start_date: datetime, end_date: datetime) -> PriceHistory: - stock_data = pdr.get_data_yahoo(identifier, start=start_date, end=end_date)["Adj Close"] - return PriceHistory(mid_history=stock_data) - -class RefinitivDataHandler(DataHandler): - def __init__(self): - ... - - @staticmethod - def get_price_history(identifier: str, start_date: datetime, end_date: datetime) -> PriceHistory: - ... - -class BloombergDataHandler(DataHandler): - def __init__(self): - ... - - @staticmethod - def get_price_history(identifier: str, start_date: datetime, end_date: datetime) -> PriceHistory: - ... diff --git a/src/abacus/utils/generalized_pareto.py b/src/abacus/utils/distributions.py similarity index 100% rename from src/abacus/utils/generalized_pareto.py rename to src/abacus/utils/distributions.py diff --git a/src/abacus/optimizer/enums.py b/src/abacus/utils/enumerations.py similarity index 79% rename from src/abacus/optimizer/enums.py rename to src/abacus/utils/enumerations.py index c1bdd56..0deb7a5 100644 --- a/src/abacus/optimizer/enums.py +++ b/src/abacus/utils/enumerations.py @@ -2,6 +2,6 @@ from enum import Enum -class OptimizationModels(Enum): +class OptimizationSpecifications(Enum): SP_MAXIMIZE_UTILITY = "sp_maximize_utility.mod" MPC_MAXIMIZE_UTILITY = "mpc_maximize_utility.mod" diff --git a/src/abacus/utils/instrument.py b/src/abacus/utils/instrument.py index 4f09a98..44f82f7 100644 --- a/src/abacus/utils/instrument.py +++ b/src/abacus/utils/instrument.py @@ -6,7 +6,9 @@ class Instrument: - def __init__(self, identifier: str, instrument_type: str, price_history: pd.DataFrame): + # TODO: Ensure uniqueness of identifier in whatever script creates instruments from ids. + def __init__(self, id: int, identifier: str, instrument_type: str, price_history: pd.DataFrame): + self.id = id self.identifier = identifier self.instrument_type = instrument_type self.price_history = price_history @@ -18,6 +20,7 @@ def initial_price(self) -> float: @property def log_returns(self) -> pd.DataFrame: + # TODO: Consider which way is best to compute log returns with. # return np.log(self.mid_history / self.mid_history.shift(1))[1:] return np.log(1 + self.price_history.pct_change())[1:] @@ -43,3 +46,6 @@ def model(self, other): def __str__(self) -> str: return f"{self.identifier}" + + def __repr__(self) -> str: + return self.__str__() diff --git a/src/abacus/utils/portfolio.py b/src/abacus/utils/portfolio.py index edb4d6c..8fac04c 100644 --- a/src/abacus/utils/portfolio.py +++ b/src/abacus/utils/portfolio.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- + + class Portfolio: def __init__(self, holdings: dict[str:int], cash: float): @@ -8,18 +10,22 @@ def __init__(self, holdings: dict[str:int], cash: float): @property def weights(self): - total_holdings = sum(self._holdings.values()) + total_holdings = sum(map(abs, self._holdings.values())) if total_holdings == 0: return {ticker: 0 for ticker in self._holdings.items()} return {ticker: holding/total_holdings for ticker, holding in self._holdings.items()} + @property + def indices(self): + return [instrument.id for instrument in self._holdings] + @property def holdings(self): return self._holdings @property def instruments(self): - return [instrument for instrument in self._holdings] + return self._holdings.keys() def __str__(self): output = "\nPortfolio Holdings\n" diff --git a/tests/test_config.py b/tests/test_config.py index cc8be69..f17265a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -16,3 +16,19 @@ "AXP", "AMZN", ) + +TEST_YAHOO_STOCK_UNIVERSE_8 = ("XOM", + "GS", + "T", + "AAPL", + "MSFT", + "PG", + "K", + "ADI", + ) + +TEST_YAHOO_STOCK_UNIVERSE_4 = ("XOM", + "GS", + "T", + "ADI", + )