From 74ead551e0e1c3f6f8e75c78b3b69003a948c1b4 Mon Sep 17 00:00:00 2001 From: simaki Date: Sun, 31 Oct 2021 13:48:25 +0900 Subject: [PATCH] Release/0.14.0 (#382) * ENH: Add `entropic_risk_measure` to `nn.functional` (close #352) (#372) * ENH: Add `value_at_risk` to `nn.functional` (#371) * MAINT: Add typing (#378) * MAINT: Drop Python 3.6 (close #356) (#357) * Python 3.6 is no longer supported. Please update to >=3.7. * CHORE: Support PyTorch 1.10 (#377) * CHORE: Update README.md (#375) * CHORE: Update pytest-cov requirement from ^2.8.1 to ^3.0.0 (#367) * CHORE: Update Makefile (#381) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: GitHub Actions --- .github/workflows/ci.yml | 9 ++- .github/workflows/doc.yml | 6 ++ Makefile | 17 ++--- README.md | 2 +- docs/source/index.rst | 1 - docs/source/nn.functional.rst | 6 +- pfhedge/_utils/hook.py | 4 +- pfhedge/_utils/operations.py | 1 - pfhedge/features/_base.py | 2 +- pfhedge/features/container.py | 6 +- .../instruments/derivative/american_binary.py | 2 +- pfhedge/instruments/derivative/base.py | 6 +- pfhedge/instruments/derivative/european.py | 2 +- .../instruments/derivative/variance_swap.py | 2 +- pfhedge/nn/functional.py | 65 +++++++++++++++++-- pfhedge/nn/modules/loss.py | 3 +- pfhedge/stochastic/heston.py | 2 +- pyproject.toml | 16 ++--- tests/nn/test_functional.py | 12 ++++ 19 files changed, 123 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79559cad..87a7ae8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI env: PROJECT_NAME: pfhedge + PYTHON_VERSION: '3.9' on: push: @@ -28,7 +29,7 @@ jobs: strategy: matrix: - python-version: ['3.6', '3.7', '3.8', '3.9'] + python-version: ['3.7', '3.8', '3.9'] steps: @@ -64,6 +65,8 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} - run: pip install poetry && poetry install @@ -83,6 +86,8 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} - uses: psf/black@21.5b1 with: @@ -107,6 +112,8 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} - uses: psf/black@21.5b1 with: diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index c62aa5ba..f45bb44e 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -1,5 +1,9 @@ name: Doc +env: + PROJECT_NAME: pfhedge + PYTHON_VERSION: '3.9' + on: push: branches: @@ -26,6 +30,8 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} - run: pip install poetry && poetry install diff --git a/Makefile b/Makefile index f38c2352..b54e2fd7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ PROJECT_NAME := pfhedge +RUN := poetry run .PHONY: check check: test lint mypy @@ -12,41 +13,41 @@ test: doctest pytest .PHONY: doctest doctest: - @poetry run pytest --doctest-modules $(PROJECT_NAME) + $(RUN) pytest --doctest-modules $(PROJECT_NAME) .PHONY: pytest pytest: - @poetry run pytest --doctest-modules tests + $(RUN) pytest --doctest-modules tests .PHONY: test-cov test-cov: - @poetry run pytest --cov=$(PROJECT_NAME) --cov-report=html + $(RUN) pytest --cov=$(PROJECT_NAME) --cov-report=html .PHONY: lint lint: lint-black lint-isort .PHONY: lint-black lint-black: - @poetry run black --check --diff --quiet --skip-magic-trailing-comma . + $(RUN) black --check --diff --quiet --skip-magic-trailing-comma . .PHONY: lint-isort lint-isort: - @poetry run isort --check --force-single-line-imports --quiet . + $(RUN) isort --check --force-single-line-imports --quiet . .PHONY: mypy mypy: - @poetry run mypy $(PROJECT_NAME) + $(RUN) mypy $(PROJECT_NAME) .PHONY: format format: format-black format-isort .PHONY: format-black format-black: - @poetry run black --quiet --skip-magic-trailing-comma . + $(RUN) black --quiet --skip-magic-trailing-comma . .PHONY: format-isort format-isort: - @poetry run isort --force-single-line-imports --quiet . + $(RUN) isort --force-single-line-imports --quiet . .PHONY: doc doc: diff --git a/README.md b/README.md index 21fcd3a9..938ffd31 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ We hope PFHedge accelerates the research and development of Deep Hedging. ## Install ```sh -$ pip install pfhedge +pip install pfhedge ``` ## How to Use diff --git a/docs/source/index.rst b/docs/source/index.rst index 9bd81a0a..4a9fa8c4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -47,7 +47,6 @@ Install: notes/* - .. toctree:: :caption: Development :hidden: diff --git a/docs/source/nn.functional.rst b/docs/source/nn.functional.rst index dfa76326..e0eb376e 100644 --- a/docs/source/nn.functional.rst +++ b/docs/source/nn.functional.rst @@ -14,12 +14,14 @@ Payoff Functions .. autofunction:: american_binary_payoff .. autofunction:: european_binary_payoff -Utility Functions ------------------ +Criterion Functions +------------------- .. autofunction:: exp_utility .. autofunction:: isoelastic_utility +.. autofunction:: entropic_risk_measure .. autofunction:: expected_shortfall +.. autofunction:: value_at_risk Other Functions ----------------- diff --git a/pfhedge/_utils/hook.py b/pfhedge/_utils/hook.py index 0eb1c244..d15476a0 100644 --- a/pfhedge/_utils/hook.py +++ b/pfhedge/_utils/hook.py @@ -17,8 +17,8 @@ def save_prev_output( >>> hook = m.register_forward_hook(save_prev_output) >>> input = torch.randn(1, 3) >>> m(input) - tensor([[-1.1647, 0.0244]], grad_fn=) + tensor([[-1.1647, 0.0244]], ...) >>> m.prev_output - tensor([[-1.1647, 0.0244]], grad_fn=) + tensor([[-1.1647, 0.0244]], ...) """ module.register_buffer("prev_output", output, persistent=False) diff --git a/pfhedge/_utils/operations.py b/pfhedge/_utils/operations.py index eaadbac0..bce4952f 100644 --- a/pfhedge/_utils/operations.py +++ b/pfhedge/_utils/operations.py @@ -19,7 +19,6 @@ def ensemble_mean( torch.Tensor Examples: - >>> function = lambda: torch.tensor([1.0, 2.0]) >>> ensemble_mean(function, 5) tensor([1., 2.]) diff --git a/pfhedge/features/_base.py b/pfhedge/features/_base.py index a6fe3a53..1f1f8203 100644 --- a/pfhedge/features/_base.py +++ b/pfhedge/features/_base.py @@ -21,7 +21,7 @@ class Feature(ABC): derivative: Derivative hedger: Optional[Module] - def __init__(self): + def __init__(self) -> None: self.register_hedger(None) @abstractmethod diff --git a/pfhedge/features/container.py b/pfhedge/features/container.py index 293a5e03..d3f9e108 100644 --- a/pfhedge/features/container.py +++ b/pfhedge/features/container.py @@ -40,14 +40,14 @@ class FeatureList(Feature): def __init__(self, features: List[Union[str, Feature]]): self.features = list(map(get_feature, features)) - def __len__(self): + def __len__(self) -> int: return len(self.features) def get(self, time_step: Optional[int]) -> Tensor: # Return size: (N, T, F) return torch.cat([f.get(time_step) for f in self.features], dim=-1) - def __repr__(self): + def __repr__(self) -> str: return str(list(map(str, self.features))) def of(self: T, derivative: Derivative, hedger: Optional[Module] = None) -> T: @@ -125,5 +125,5 @@ def of(self, derivative=None, hedger=None): self.inputs = self.inputs.of(derivative, hedger) return self - def is_state_dependent(self): + def is_state_dependent(self) -> bool: return self.inputs.is_state_dependent() diff --git a/pfhedge/instruments/derivative/american_binary.py b/pfhedge/instruments/derivative/american_binary.py index 6c1997df..0807d820 100644 --- a/pfhedge/instruments/derivative/american_binary.py +++ b/pfhedge/instruments/derivative/american_binary.py @@ -103,7 +103,7 @@ def __init__( "Specify them in the constructor of the underlier instead." ) - def extra_repr(self): + def extra_repr(self) -> str: params = [] if not self.call: params.append("call=" + str(self.call)) diff --git a/pfhedge/instruments/derivative/base.py b/pfhedge/instruments/derivative/base.py index 757086a2..1bcf74a5 100644 --- a/pfhedge/instruments/derivative/base.py +++ b/pfhedge/instruments/derivative/base.py @@ -47,7 +47,7 @@ class Derivative(Instrument): pricer: Optional[Callable[[Any], Tensor]] _clauses: Dict[str, Callable[["Derivative", Tensor], Tensor]] - def __init__(self): + def __init__(self) -> None: super().__init__() self.pricer = None self.cost = 0.0 @@ -252,7 +252,9 @@ def time_to_maturity(self, time_step: Optional[int] = None) -> Tensor: t = torch.tensor([[time]]).to(self.underlier.spot) * self.underlier.dt return t.expand(n_paths, -1) - def max_moneyness(self, time_step: Optional[int] = None, log=False) -> Tensor: + def max_moneyness( + self, time_step: Optional[int] = None, log: bool = False + ) -> Tensor: """Returns the cumulative maximum of the moneyness. Args: diff --git a/pfhedge/instruments/derivative/european.py b/pfhedge/instruments/derivative/european.py index 1375332c..01123acd 100644 --- a/pfhedge/instruments/derivative/european.py +++ b/pfhedge/instruments/derivative/european.py @@ -137,7 +137,7 @@ def __init__( "Specify them in the constructor of the underlier instead." ) - def extra_repr(self): + def extra_repr(self) -> str: params = [] if not self.call: params.append("call=" + str(self.call)) diff --git a/pfhedge/instruments/derivative/variance_swap.py b/pfhedge/instruments/derivative/variance_swap.py index 66c01a3a..cd142ea4 100644 --- a/pfhedge/instruments/derivative/variance_swap.py +++ b/pfhedge/instruments/derivative/variance_swap.py @@ -79,7 +79,7 @@ def __init__( "Specify them in the constructor of the underlier instead." ) - def extra_repr(self): + def extra_repr(self) -> str: return ", ".join( ( "strike=" + _format_float(self.strike), diff --git a/pfhedge/nn/functional.py b/pfhedge/nn/functional.py index c11e4e82..cfbcd062 100644 --- a/pfhedge/nn/functional.py +++ b/pfhedge/nn/functional.py @@ -153,6 +153,14 @@ def isoelastic_utility(input: Tensor, a: float) -> Tensor: return input.pow(1.0 - a) +def entropic_risk_measure(input: Tensor, a: float = 1.0) -> Tensor: + """Returns the entropic risk measure. + + See :class:`pfhedge.nn.EntropicRiskMeasure` for details. + """ + return (-exp_utility(input, a=a).mean(0)).log() / a + + def topp(input: Tensor, p: float, dim: Optional[int] = None, largest: bool = True): """Returns the largest :math:`p * N` elements of the given input tensor, where :math:`N` stands for the total number of elements in the input tensor. @@ -166,7 +174,7 @@ def topp(input: Tensor, p: float, dim: Optional[int] = None, largest: bool = Tru Args: input (torch.Tensor): The input tensor. - p (float): Quantile level. + p (float): The quantile level. dim (int, optional): The dimension to sort along. largest (bool, default=True): Controls whether to return largest or smallest elements. @@ -175,7 +183,6 @@ def topp(input: Tensor, p: float, dim: Optional[int] = None, largest: bool = Tru torch.Tensor Examples: - >>> from pfhedge.nn.functional import topp >>> >>> input = torch.arange(1.0, 6.0) @@ -197,15 +204,15 @@ def expected_shortfall(input: Tensor, p: float, dim: Optional[int] = None) -> Te Args: input (torch.Tensor): The input tensor. - p (float): Quantile level. + p (float): The quantile level. dim (int, optional): The dimension to sort along. Examples: >>> from pfhedge.nn.functional import expected_shortfall >>> - >>> input = -torch.arange(1.0, 10.0) + >>> input = -torch.arange(10.0) >>> input - tensor([-1., -2., -3., -4., -5., -6., -7., -8., -9.]) + tensor([-0., -1., -2., -3., -4., -5., -6., -7., -8., -9.]) >>> expected_shortfall(input, 0.3) tensor(8.) @@ -218,6 +225,52 @@ def expected_shortfall(input: Tensor, p: float, dim: Optional[int] = None) -> Te return -topp(input, p=p, largest=False, dim=dim).values.mean(dim=dim) +def _min_values(input: Tensor, dim: Optional[int] = None) -> Tensor: + return input.min() if dim is None else input.min(dim=dim).values + + +def _max_values(input: Tensor, dim: Optional[int] = None) -> Tensor: + return input.max() if dim is None else input.max(dim=dim).values + + +def value_at_risk(input: Tensor, p: float, dim: Optional[int] = None) -> Tensor: + """Returns the value at risk of the given input tensor. + + Note: + If :math:`p \leq 1 / N`` with :math:`N` being the number of elements to sort, + returns the smallest element in the tensor. + If :math:`p > 1 - 1 / N``, returns the largest element in the tensor. + + Args: + input (torch.Tensor): The input tensor. + p (float): The quantile level. + dim (int, optional): The dimension to sort along. + + Examples: + >>> from pfhedge.nn.functional import value_at_risk + >>> + >>> input = -torch.arange(10.0) + >>> input + tensor([-0., -1., -2., -3., -4., -5., -6., -7., -8., -9.]) + >>> value_at_risk(input, 0.3) + tensor(-7.) + + Returns: + torch.Tensor + """ + n = input.numel() if dim is None else input.size(dim) + + if p <= 1 / n: + output = _min_values(input, dim=dim) + elif p > 1 - 1 / n: + output = _max_values(input, dim=dim) + else: + q = (p - (1 / n)) / (1 - (1 / n)) + output = input.quantile(q, dim=dim) + + return output + + def leaky_clamp( input: Tensor, min: Optional[Tensor] = None, @@ -327,7 +380,7 @@ def terminal_value( cost: float = 0.0, payoff: Optional[Tensor] = None, deduct_first_cost: bool = True, -): +) -> Tensor: r"""Returns the terminal portfolio value. The terminal value of a hedger's portfolio is given by diff --git a/pfhedge/nn/modules/loss.py b/pfhedge/nn/modules/loss.py index f85181ee..f35ac42f 100644 --- a/pfhedge/nn/modules/loss.py +++ b/pfhedge/nn/modules/loss.py @@ -9,6 +9,7 @@ from pfhedge._utils.bisect import bisect from pfhedge._utils.str import _format_float +from ..functional import entropic_risk_measure from ..functional import exp_utility from ..functional import expected_shortfall from ..functional import isoelastic_utility @@ -112,7 +113,7 @@ def extra_repr(self) -> str: return "a=" + _format_float(self.a) if self.a != 1 else "" def forward(self, input: Tensor) -> Tensor: - return (-exp_utility(input, a=self.a).mean(0)).log() / self.a + return entropic_risk_measure(input, a=self.a) def cash(self, input: Tensor) -> Tensor: return -self(input) diff --git a/pfhedge/stochastic/heston.py b/pfhedge/stochastic/heston.py index 8e7ac14c..e2712408 100644 --- a/pfhedge/stochastic/heston.py +++ b/pfhedge/stochastic/heston.py @@ -17,7 +17,7 @@ class HestonTuple(namedtuple("HestonTuple", ["spot", "variance"])): __module__ = "pfhedge.stochastic" - def __repr__(self): + def __repr__(self) -> str: items_str_list = [] for field, tensor in self._asdict().items(): items_str_list.append(field + "=\n" + str(tensor)) diff --git a/pyproject.toml b/pyproject.toml index f39c0e30..ce107975 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,22 @@ [tool.poetry] name = "pfhedge" -version = "0.13.2" +version = "0.14.0" description = "Deep Hedging in PyTorch" authors = ["Shota Imaki "] license = "MIT" repository = "https://github.com/pfnet-research/pfhedge" [tool.poetry.dependencies] -python = "^3.6.13" -torch = "^1.9.0" -tqdm = "^4.59.0" +python = "^3.7.12" +torch = "^1.10.0" +tqdm = "^4.62.3" [tool.poetry.dev-dependencies] -pytest = "^6.2.2" -black = "^21.4b0" -isort = "^5.7.0" +pytest = "^6.2.5" +black = "^21.9b0" +isort = "^5.9.3" mypy = "^0.910" -pytest-cov = "^2.12.1" +pytest-cov = "^3.0.0" Sphinx = "^4.2.0" sphinx-autobuild = "^2021.3.14" sphinx-copybutton = "^0.4.0" diff --git a/tests/nn/test_functional.py b/tests/nn/test_functional.py index 7126d874..041afa18 100644 --- a/tests/nn/test_functional.py +++ b/tests/nn/test_functional.py @@ -10,6 +10,7 @@ from pfhedge.nn.functional import realized_volatility from pfhedge.nn.functional import terminal_value from pfhedge.nn.functional import topp +from pfhedge.nn.functional import value_at_risk def test_exp_utility(): @@ -60,6 +61,17 @@ def test_expected_shortfall(): assert_close(result, expect) +def test_value_at_risk(): + input = -torch.arange(10.0) + + assert_close(value_at_risk(input, 0.0), -torch.tensor(9.0)) + assert_close(value_at_risk(input, 0.1), -torch.tensor(9.0)) + assert_close(value_at_risk(input, 0.2), -torch.tensor(8.0)) + assert_close(value_at_risk(input, 0.8), -torch.tensor(2.0)) + assert_close(value_at_risk(input, 0.9), -torch.tensor(1.0)) + assert_close(value_at_risk(input, 1.0), -torch.tensor(0.0)) + + def test_leaky_clamp(): input = torch.tensor([-1.0, 0.0, 0.5, 1.0, 2.0])