fynance 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fynance/__init__.py +91 -0
- fynance/_exceptions.py +36 -0
- fynance/_wrappers.py +250 -0
- fynance/backtest/__init__.py +45 -0
- fynance/backtest/_basis_plot.py +93 -0
- fynance/backtest/backtest_neural_net.py +121 -0
- fynance/backtest/cost.py +61 -0
- fynance/backtest/dynamic_plot_backtest.py +536 -0
- fynance/backtest/engine.py +107 -0
- fynance/backtest/loss.py +117 -0
- fynance/backtest/plot.py +208 -0
- fynance/backtest/plot_backtest.py +211 -0
- fynance/backtest/plot_tools.py +219 -0
- fynance/backtest/print_stats.py +141 -0
- fynance/backtest/result.py +76 -0
- fynance/core/__init__.py +32 -0
- fynance/core/price_series.py +429 -0
- fynance/core/protocols.py +109 -0
- fynance/data/__init__.py +33 -0
- fynance/data/align.py +134 -0
- fynance/data/base.py +141 -0
- fynance/data/csv.py +52 -0
- fynance/data/parquet.py +36 -0
- fynance/data/split.py +99 -0
- fynance/estimator/__init__.py +13 -0
- fynance/estimator/estimator.py +109 -0
- fynance/features/__init__.py +58 -0
- fynance/features/_metrics_helpers.py +378 -0
- fynance/features/engineering.py +156 -0
- fynance/features/filters.py +439 -0
- fynance/features/indicators.py +830 -0
- fynance/features/momentums.py +629 -0
- fynance/features/money_management.py +66 -0
- fynance/features/regime.py +83 -0
- fynance/features/roll_functions.py +237 -0
- fynance/features/scale.py +545 -0
- fynance/features/stats.py +395 -0
- fynance/metrics/__init__.py +31 -0
- fynance/metrics/drawdown.py +260 -0
- fynance/metrics/ratios.py +617 -0
- fynance/metrics/returns.py +387 -0
- fynance/metrics/summary.py +79 -0
- fynance/models/__init__.py +108 -0
- fynance/models/_base.py +453 -0
- fynance/models/_recurrent_base.py +217 -0
- fynance/models/attention.py +174 -0
- fynance/models/cv_result.py +39 -0
- fynance/models/econometric_models.py +449 -0
- fynance/models/ensemble.py +109 -0
- fynance/models/gru.py +260 -0
- fynance/models/loss/__init__.py +41 -0
- fynance/models/loss/_base.py +53 -0
- fynance/models/loss/calmar.py +39 -0
- fynance/models/loss/directional.py +107 -0
- fynance/models/loss/hybrid.py +61 -0
- fynance/models/loss/omega.py +47 -0
- fynance/models/loss/sharpe.py +108 -0
- fynance/models/loss/sortino.py +92 -0
- fynance/models/lstm.py +373 -0
- fynance/models/mlp.py +186 -0
- fynance/models/rnn.py +115 -0
- fynance/models/rolling.py +557 -0
- fynance/models/tcn.py +179 -0
- fynance/models/training.py +101 -0
- fynance/models/transformer.py +190 -0
- fynance/plot/__init__.py +26 -0
- fynance/plot/_helpers.py +41 -0
- fynance/plot/equity.py +51 -0
- fynance/plot/returns.py +58 -0
- fynance/plot/tearsheet.py +82 -0
- fynance/portfolio/__init__.py +32 -0
- fynance/portfolio/allocation.py +801 -0
- fynance/portfolio/sizing.py +147 -0
- fynance/signal/__init__.py +24 -0
- fynance/signal/mappers.py +122 -0
- fynance/signal/pipeline.py +58 -0
- fynance/strategy/__init__.py +16 -0
- fynance/strategy/strategy.py +194 -0
- fynance/tests/__init__.py +0 -0
- fynance/tests/backtest/__init__.py +0 -0
- fynance/tests/backtest/test_backtest.py +103 -0
- fynance/tests/backtest/test_cost.py +36 -0
- fynance/tests/backtest/test_engine.py +71 -0
- fynance/tests/backtest/test_result.py +35 -0
- fynance/tests/core/__init__.py +0 -0
- fynance/tests/core/test_price_series.py +152 -0
- fynance/tests/core/test_protocols.py +68 -0
- fynance/tests/data/__init__.py +0 -0
- fynance/tests/data/test_align.py +46 -0
- fynance/tests/data/test_csv.py +40 -0
- fynance/tests/data/test_parquet.py +22 -0
- fynance/tests/data/test_registry.py +27 -0
- fynance/tests/data/test_split.py +45 -0
- fynance/tests/estimator/__init__.py +0 -0
- fynance/tests/estimator/test_estimator.py +75 -0
- fynance/tests/features/__init__.py +13 -0
- fynance/tests/features/test_engineering.py +58 -0
- fynance/tests/features/test_filters.py +141 -0
- fynance/tests/features/test_filters_benchmark.py +48 -0
- fynance/tests/features/test_indicators.py +145 -0
- fynance/tests/features/test_momentums.py +138 -0
- fynance/tests/features/test_momentums_numba.py +60 -0
- fynance/tests/features/test_property.py +130 -0
- fynance/tests/features/test_regime.py +35 -0
- fynance/tests/features/test_robustness.py +30 -0
- fynance/tests/features/test_roll_functions_numba.py +46 -0
- fynance/tests/features/test_scale.py +159 -0
- fynance/tests/features/test_technical_indicators.py +95 -0
- fynance/tests/metrics/__init__.py +0 -0
- fynance/tests/metrics/test_metrics.py +525 -0
- fynance/tests/metrics/test_metrics_numba.py +91 -0
- fynance/tests/metrics/test_summary.py +37 -0
- fynance/tests/models/__init__.py +13 -0
- fynance/tests/models/test_econometric_models.py +107 -0
- fynance/tests/models/test_econometric_numba_parity.py +61 -0
- fynance/tests/models/test_ensemble.py +74 -0
- fynance/tests/models/test_loss.py +212 -0
- fynance/tests/models/test_neural_network.py +520 -0
- fynance/tests/models/test_rnn.py +63 -0
- fynance/tests/models/test_rolling.py +129 -0
- fynance/tests/models/test_signalmodel.py +47 -0
- fynance/tests/models/test_tcn.py +96 -0
- fynance/tests/models/test_training.py +48 -0
- fynance/tests/models/test_transformer.py +106 -0
- fynance/tests/plot/__init__.py +0 -0
- fynance/tests/plot/test_smoke.py +55 -0
- fynance/tests/portfolio/__init__.py +0 -0
- fynance/tests/portfolio/test_allocation.py +249 -0
- fynance/tests/portfolio/test_sizing.py +49 -0
- fynance/tests/signal/__init__.py +0 -0
- fynance/tests/signal/test_signal.py +79 -0
- fynance/tests/strategy/__init__.py +0 -0
- fynance/tests/strategy/test_playground.py +49 -0
- fynance/tests/strategy/test_strategy.py +113 -0
- fynance-2.1.0.dist-info/METADATA +173 -0
- fynance-2.1.0.dist-info/RECORD +139 -0
- fynance-2.1.0.dist-info/WHEEL +5 -0
- fynance-2.1.0.dist-info/licenses/LICENSE.txt +20 -0
- fynance-2.1.0.dist-info/top_level.txt +1 -0
fynance/__init__.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# @Author: ArthurBernard
|
|
4
|
+
# @Email: arthur.bernard.92@gmail.com
|
|
5
|
+
# @Date: 2019-02-15 12:50:12
|
|
6
|
+
# @Last modified by: ArthurBernard
|
|
7
|
+
# @Last modified time: 2019-11-05 16:55:36
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
Fynance : A Python package for quant financial research
|
|
11
|
+
=======================================================
|
|
12
|
+
|
|
13
|
+
Documentation is available at
|
|
14
|
+
https://fynance.readthedocs.io/en/latest/index.html.
|
|
15
|
+
|
|
16
|
+
Contents
|
|
17
|
+
--------
|
|
18
|
+
Fynance is a python/cython project that includes several machine learning,
|
|
19
|
+
econometric and statistical subpackages specialy adapted for financial
|
|
20
|
+
analysis, portfolio allocation, and backtest trading strategies.
|
|
21
|
+
|
|
22
|
+
Subpackages
|
|
23
|
+
-----------
|
|
24
|
+
portfolio --- Portfolio allocation & sizing
|
|
25
|
+
backtest --- Backtest strategy tools
|
|
26
|
+
estimator --- Parameter estimation (Cython ARMA/GARCH)
|
|
27
|
+
features --- Features extraction
|
|
28
|
+
models --- Econometric and Neural Network models (using PyTorch)
|
|
29
|
+
|
|
30
|
+
Utility tools
|
|
31
|
+
-------------
|
|
32
|
+
_exceptions --- Fynance exceptions
|
|
33
|
+
tests --- Run fynance unittests
|
|
34
|
+
_wrappers --- Fynance wrapper functions
|
|
35
|
+
|
|
36
|
+
API stability policy (1.x series)
|
|
37
|
+
---------------------------------
|
|
38
|
+
The symbols re-exported below from :mod:`fynance.models`,
|
|
39
|
+
:mod:`fynance.portfolio.allocation`, :mod:`fynance.features` and
|
|
40
|
+
:mod:`fynance.estimator` form the **public, stable API** for the 1.x
|
|
41
|
+
release line. Within 1.x:
|
|
42
|
+
|
|
43
|
+
- public function and class signatures are frozen — no removals, no
|
|
44
|
+
backward-incompatible signature changes;
|
|
45
|
+
- behavioural changes that would break user code go through one
|
|
46
|
+
release of :class:`DeprecationWarning` before becoming the new
|
|
47
|
+
default (see ``CONTRIBUTING.md``);
|
|
48
|
+
- :mod:`fynance.backtest` and :mod:`fynance.models` *internal* helpers
|
|
49
|
+
(names prefixed with ``_``) remain free to evolve.
|
|
50
|
+
|
|
51
|
+
Breaking changes are reserved for the 2.x line and tracked in
|
|
52
|
+
``CHANGELOG.md``.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
__version__ = version("fynance")
|
|
60
|
+
except PackageNotFoundError:
|
|
61
|
+
__version__ = "unknown"
|
|
62
|
+
|
|
63
|
+
__all__ = ['__version__']
|
|
64
|
+
|
|
65
|
+
import sys as _sys
|
|
66
|
+
|
|
67
|
+
from .backtest import *
|
|
68
|
+
from .core import *
|
|
69
|
+
from .data import *
|
|
70
|
+
from .estimator import *
|
|
71
|
+
from .features import *
|
|
72
|
+
from .metrics import *
|
|
73
|
+
from .models import *
|
|
74
|
+
from .plot import *
|
|
75
|
+
from .portfolio import *
|
|
76
|
+
from .signal import *
|
|
77
|
+
from .strategy import *
|
|
78
|
+
|
|
79
|
+
# Aggregate each subpackage's public surface. Use ``sys.modules`` rather than the
|
|
80
|
+
# package attributes, which a star import may have shadowed with a name that
|
|
81
|
+
# collides with a submodule (e.g. the ``backtest`` engine function).
|
|
82
|
+
for _name in ("core", "data", "models", "estimator", "features", "metrics",
|
|
83
|
+
"plot", "signal", "backtest", "portfolio", "strategy"):
|
|
84
|
+
__all__ += _sys.modules[f"{__name__}.{_name}"].__all__
|
|
85
|
+
|
|
86
|
+
# Restore the subpackage attribute shadowed by such a collision so that
|
|
87
|
+
# ``fynance.backtest`` resolves to the package (``fynance.backtest.backtest``
|
|
88
|
+
# stays the engine function).
|
|
89
|
+
backtest = _sys.modules[__name__ + ".backtest"]
|
|
90
|
+
|
|
91
|
+
del _sys, _name
|
fynance/_exceptions.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# @Author: ArthurBernard
|
|
4
|
+
# @Email: arthur.bernard.92@gmail.com
|
|
5
|
+
# @Date: 2019-10-23 17:01:36
|
|
6
|
+
# @Last modified by: ArthurBernard
|
|
7
|
+
# @Last modified time: 2019-10-23 17:17:32
|
|
8
|
+
|
|
9
|
+
""" Define some various richly-typed exceptions. """
|
|
10
|
+
|
|
11
|
+
# Built-in packages
|
|
12
|
+
|
|
13
|
+
# Third party packages
|
|
14
|
+
|
|
15
|
+
# Local packages
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ArraySizeError(ValueError, IndexError):
|
|
19
|
+
""" Size of the array was invalid. """
|
|
20
|
+
|
|
21
|
+
def __init__(self, size, axis=None, min_size=None, msg_prefix=None):
|
|
22
|
+
""" Initialize the array size error. """
|
|
23
|
+
msg = 'array of size {}'.format(size)
|
|
24
|
+
|
|
25
|
+
if axis is not None:
|
|
26
|
+
msg += ' in axis {}'.format(axis)
|
|
27
|
+
|
|
28
|
+
msg += ' is not allowed'
|
|
29
|
+
|
|
30
|
+
if min_size is not None:
|
|
31
|
+
msg += ', minimum size is {}'.format(min_size)
|
|
32
|
+
|
|
33
|
+
if msg_prefix is not None:
|
|
34
|
+
msg = '{}: {}'.format(msg_prefix, msg)
|
|
35
|
+
|
|
36
|
+
super().__init__(msg)
|
fynance/_wrappers.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# @Author: ArthurBernard
|
|
4
|
+
# @Email: arthur.bernard.92@gmail.com
|
|
5
|
+
# @Date: 2019-10-11 10:10:43
|
|
6
|
+
# @Last modified by: ArthurBernard
|
|
7
|
+
# @Last modified time: 2019-11-08 11:11:54
|
|
8
|
+
|
|
9
|
+
""" Some wrappers functions. """
|
|
10
|
+
|
|
11
|
+
# Built-in packages
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from warnings import warn
|
|
14
|
+
|
|
15
|
+
# Third party packages
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
# Local packages
|
|
19
|
+
from fynance._exceptions import ArraySizeError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _check_dtype(X, dtype):
|
|
23
|
+
if dtype is None:
|
|
24
|
+
dtype = X.dtype
|
|
25
|
+
|
|
26
|
+
if X.dtype != np.float64:
|
|
27
|
+
X = X.astype(np.float64)
|
|
28
|
+
|
|
29
|
+
return X, dtype
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def wrap_dtype(func):
|
|
33
|
+
""" Check the dtype of the `X` array.
|
|
34
|
+
|
|
35
|
+
Convert dtype of X to np.float64 before to pass to cython function and
|
|
36
|
+
convert to specified dtype at the end.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
@wraps(func)
|
|
40
|
+
def check_dtype(X, *args, dtype=None, **kwargs):
|
|
41
|
+
X, dtype = _check_dtype(X, dtype)
|
|
42
|
+
|
|
43
|
+
if dtype != np.float64:
|
|
44
|
+
return func(X, *args, dtype=dtype, **kwargs).astype(dtype)
|
|
45
|
+
|
|
46
|
+
return func(X, *args, dtype=dtype, **kwargs)
|
|
47
|
+
|
|
48
|
+
return check_dtype
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def wrap_axis(func):
|
|
52
|
+
""" Check if computation on `axis` of `X` array is available. """
|
|
53
|
+
@wraps(func)
|
|
54
|
+
def check_axis(X, *args, axis=0, min_size=0, **kwargs):
|
|
55
|
+
shape = X.shape
|
|
56
|
+
if X.ndim > 2:
|
|
57
|
+
warn(
|
|
58
|
+
'currently, array of dimensions larger than 2 are not '
|
|
59
|
+
'supported, it may lead to some issues',
|
|
60
|
+
category=UserWarning,
|
|
61
|
+
stacklevel=2,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if shape[axis] < min_size:
|
|
65
|
+
|
|
66
|
+
raise ArraySizeError(shape[axis], axis=axis, min_size=min_size)
|
|
67
|
+
|
|
68
|
+
elif X.ndim <= axis:
|
|
69
|
+
|
|
70
|
+
raise np.AxisError(axis, len(X.shape))
|
|
71
|
+
|
|
72
|
+
elif axis == 1 and X.ndim == 2:
|
|
73
|
+
|
|
74
|
+
return func(X.T, *args, axis=0, **kwargs).T
|
|
75
|
+
|
|
76
|
+
return func(X, *args, axis=axis, **kwargs)
|
|
77
|
+
|
|
78
|
+
return check_axis
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def wrap_lags(func):
|
|
82
|
+
# Not clean, may lead to undesirable behavior
|
|
83
|
+
""" Check the max available lag for `X` array. """
|
|
84
|
+
@wraps(func)
|
|
85
|
+
def check_lags(X, k, *args, axis=0, **kwargs):
|
|
86
|
+
if k <= 0:
|
|
87
|
+
raise ValueError('lag {} must be greater than 0.'.format(k))
|
|
88
|
+
|
|
89
|
+
elif X.shape[axis] < k:
|
|
90
|
+
warn(
|
|
91
|
+
'{} lags is out of bounds for axis {} with size {}'.format(
|
|
92
|
+
k, axis, X.shape[axis]
|
|
93
|
+
),
|
|
94
|
+
category=UserWarning,
|
|
95
|
+
stacklevel=2,
|
|
96
|
+
)
|
|
97
|
+
k = X.shape[axis]
|
|
98
|
+
|
|
99
|
+
return func(X, k, *args, axis=axis, **kwargs)
|
|
100
|
+
|
|
101
|
+
return check_lags
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def wrap_window(func):
|
|
105
|
+
""" Check if the lagged window `w` is available for `X` array. """
|
|
106
|
+
@wraps(func)
|
|
107
|
+
def check_window(X, w=None, **kwargs):
|
|
108
|
+
if w == 0 or w is None:
|
|
109
|
+
w = X.shape[0]
|
|
110
|
+
|
|
111
|
+
elif 'min_size' in kwargs.keys() and w < kwargs['min_size']:
|
|
112
|
+
|
|
113
|
+
raise ValueError('lagged window of size {} is not available, \
|
|
114
|
+
must be greater than {}'.format(w, kwargs['min_size']))
|
|
115
|
+
|
|
116
|
+
elif w < 0:
|
|
117
|
+
|
|
118
|
+
raise ValueError('lagged window of size {} is not available, \
|
|
119
|
+
must be positive.'.format(w))
|
|
120
|
+
|
|
121
|
+
elif w > X.shape[0]:
|
|
122
|
+
warn(
|
|
123
|
+
'lagged window of size {} is out of bounds with time axis '
|
|
124
|
+
'of size {}'.format(w, X.shape[0]),
|
|
125
|
+
category=UserWarning,
|
|
126
|
+
stacklevel=2,
|
|
127
|
+
)
|
|
128
|
+
w = X.shape[0]
|
|
129
|
+
|
|
130
|
+
return func(X, w=int(w), **kwargs)
|
|
131
|
+
|
|
132
|
+
return check_window
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def wrap_expo(func):
|
|
136
|
+
# Not clean, may lead to undesirable behavior
|
|
137
|
+
""" Check if parameters is allowed by the `kind` of moving avg/std. """
|
|
138
|
+
@wraps(func)
|
|
139
|
+
def check_expo(X, *args, w=None, kind=None, **kwargs):
|
|
140
|
+
if kind == 'e':
|
|
141
|
+
w = 1 - 2 / (1 + w)
|
|
142
|
+
|
|
143
|
+
return func(X, *args, w=w, kind=kind, **kwargs)
|
|
144
|
+
|
|
145
|
+
return check_expo
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def wrap_null(func):
|
|
149
|
+
""" Check if there is any null value and raise an exception. """
|
|
150
|
+
@wraps(func)
|
|
151
|
+
def check_null(X, *args, **kwargs):
|
|
152
|
+
if (X == 0).any():
|
|
153
|
+
|
|
154
|
+
raise ValueError('null value in X is not allowed.')
|
|
155
|
+
|
|
156
|
+
return func(X, *args, **kwargs)
|
|
157
|
+
|
|
158
|
+
return check_null
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def wrap_ddof(func):
|
|
162
|
+
""" Check if ddof is not greater than the number of timeframe. """
|
|
163
|
+
@wraps(func)
|
|
164
|
+
def check_ddof(X, *args, ddof=0, **kwargs):
|
|
165
|
+
if ddof >= X.shape[0]:
|
|
166
|
+
msg_prefix = 'with degree of freedom {}'.format(ddof)
|
|
167
|
+
|
|
168
|
+
raise ArraySizeError(X.shape[0], msg_prefix=msg_prefix)
|
|
169
|
+
|
|
170
|
+
elif ddof < 0:
|
|
171
|
+
|
|
172
|
+
raise ValueError('ddof must be a positive value')
|
|
173
|
+
|
|
174
|
+
return func(X, *args, ddof=ddof, **kwargs)
|
|
175
|
+
|
|
176
|
+
return check_ddof
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def wrap_keepdims(func):
|
|
180
|
+
""" Check that output have same dimensions as input. """
|
|
181
|
+
@wraps(func)
|
|
182
|
+
def check_keepdims(X, *args, keepdims=False, **kwargs):
|
|
183
|
+
if keepdims:
|
|
184
|
+
out = func(X, *args, **kwargs)
|
|
185
|
+
|
|
186
|
+
return out.reshape(out.shape + (1,))
|
|
187
|
+
|
|
188
|
+
return func(X, *args, **kwargs)
|
|
189
|
+
|
|
190
|
+
return check_keepdims
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class WrapperArray:
|
|
194
|
+
""" Object to wrap function that handle numpy arrays.
|
|
195
|
+
|
|
196
|
+
This object mix several wrapper functions.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
*args : {'dtype', 'axis', 'lags', 'window', 'null', 'ddof', 'keepdims'}
|
|
201
|
+
Wrapper functions.
|
|
202
|
+
**kwargs
|
|
203
|
+
Keyword arguments to pass to wrapper functions.
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
handler = {
|
|
208
|
+
'dtype': wrap_dtype,
|
|
209
|
+
'axis': wrap_axis,
|
|
210
|
+
'lags': wrap_lags,
|
|
211
|
+
'window': wrap_window,
|
|
212
|
+
'null': wrap_null,
|
|
213
|
+
'ddof': wrap_ddof,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
def __init__(self, *args, **kwargs):
|
|
217
|
+
""" Initialize wrapper functions. """
|
|
218
|
+
self.wrappers = {key: self.handler[key] for key in args}
|
|
219
|
+
self.kw = kwargs
|
|
220
|
+
|
|
221
|
+
def __call__(self, func):
|
|
222
|
+
""" Wrap `func`.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
func : function
|
|
227
|
+
Function to wrap.
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
function
|
|
232
|
+
Wrapped function.
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
@wraps(func)
|
|
236
|
+
def wrap(X, *args, **kwargs):
|
|
237
|
+
wrap_func = None
|
|
238
|
+
kwargs = {**kwargs, **self.kw}
|
|
239
|
+
|
|
240
|
+
for k, w in self.wrappers.items():
|
|
241
|
+
|
|
242
|
+
if wrap_func is None:
|
|
243
|
+
wrap_func = w(func)
|
|
244
|
+
|
|
245
|
+
else:
|
|
246
|
+
wrap_func = w(wrap_func)
|
|
247
|
+
|
|
248
|
+
return wrap_func(X, *args, **kwargs)
|
|
249
|
+
|
|
250
|
+
return wrap
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
Some tools to backtest strategies
|
|
7
|
+
|
|
8
|
+
.. currentmodule:: fynance.backtest
|
|
9
|
+
|
|
10
|
+
.. toctree::
|
|
11
|
+
:maxdepth: 1
|
|
12
|
+
:caption: Contents:
|
|
13
|
+
|
|
14
|
+
backtest.tools
|
|
15
|
+
backtest.plot_object
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from . import (
|
|
20
|
+
backtest_neural_net,
|
|
21
|
+
cost,
|
|
22
|
+
dynamic_plot_backtest,
|
|
23
|
+
engine,
|
|
24
|
+
plot_backtest,
|
|
25
|
+
plot_tools,
|
|
26
|
+
print_stats,
|
|
27
|
+
result,
|
|
28
|
+
)
|
|
29
|
+
from .backtest_neural_net import *
|
|
30
|
+
from .cost import *
|
|
31
|
+
from .dynamic_plot_backtest import *
|
|
32
|
+
from .engine import *
|
|
33
|
+
from .plot_backtest import *
|
|
34
|
+
from .plot_tools import *
|
|
35
|
+
from .print_stats import *
|
|
36
|
+
from .result import *
|
|
37
|
+
|
|
38
|
+
__all__ = print_stats.__all__
|
|
39
|
+
__all__ += cost.__all__
|
|
40
|
+
__all__ += engine.__all__
|
|
41
|
+
__all__ += result.__all__
|
|
42
|
+
__all__ += plot_tools.__all__
|
|
43
|
+
__all__ += plot_backtest.__all__
|
|
44
|
+
__all__ += dynamic_plot_backtest.__all__
|
|
45
|
+
__all__ += backtest_neural_net.__all__
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# @Author: ArthurBernard
|
|
4
|
+
# @Email: arthur.bernard.92@gmail.com
|
|
5
|
+
# @Date: 2020-10-24 09:03:49
|
|
6
|
+
# @Last modified by: ArthurBernard
|
|
7
|
+
# @Last modified time: 2020-11-20 09:09:02
|
|
8
|
+
|
|
9
|
+
""" Description. """
|
|
10
|
+
|
|
11
|
+
# Built-in packages
|
|
12
|
+
from abc import ABCMeta
|
|
13
|
+
|
|
14
|
+
# Third party packages
|
|
15
|
+
# import numpy as np
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
|
|
18
|
+
# import seaborn as sns
|
|
19
|
+
|
|
20
|
+
# Local packages
|
|
21
|
+
|
|
22
|
+
__all__ = []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _BasisAxes(metaclass=ABCMeta):
|
|
26
|
+
def __call__(self, fig, n_rows, n_cols, n_axes):
|
|
27
|
+
self.fig = fig
|
|
28
|
+
self.n_axes = n_axes
|
|
29
|
+
self.ax = self.fig.add_subplot(n_rows, n_cols, n_axes)
|
|
30
|
+
|
|
31
|
+
# @abstractmethod
|
|
32
|
+
def plot(self, y, x=None):
|
|
33
|
+
if x is None:
|
|
34
|
+
self.ax.plot(y)
|
|
35
|
+
|
|
36
|
+
else:
|
|
37
|
+
self.ax.plot(x, y)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _BasisPlot:
|
|
41
|
+
def __init__(self, **kwargs):
|
|
42
|
+
self.fig = plt.figure(**kwargs)
|
|
43
|
+
self._n_axes = 0
|
|
44
|
+
self._n_cols = 1
|
|
45
|
+
self._n_rows = 0
|
|
46
|
+
self.axes = {}
|
|
47
|
+
self.keys = []
|
|
48
|
+
|
|
49
|
+
def __setitem__(self, key, value: _BasisAxes):
|
|
50
|
+
# Set optimal number of axes on figure plot
|
|
51
|
+
self._n_axes += 1
|
|
52
|
+
sqrt_n_axes = self._n_axes ** 0.5
|
|
53
|
+
if self._n_cols * self._n_rows < self._n_axes:
|
|
54
|
+
if self._n_rows > self._n_cols and self._n_rows >= sqrt_n_axes:
|
|
55
|
+
self._n_cols += 1
|
|
56
|
+
|
|
57
|
+
else:
|
|
58
|
+
self._n_rows += 1
|
|
59
|
+
|
|
60
|
+
print(self._n_rows, self._n_cols)
|
|
61
|
+
self.axes[key] = value
|
|
62
|
+
self.keys.append(key)
|
|
63
|
+
|
|
64
|
+
def __delitem__(self, key):
|
|
65
|
+
# Set optimal number of axes on figure plot
|
|
66
|
+
self._n_axes -= 1
|
|
67
|
+
n_cols = self._n_cols
|
|
68
|
+
n_rows = self._n_rows
|
|
69
|
+
if (n_cols - 1) * n_rows >= self._n_axes:
|
|
70
|
+
self._n_cols -= 1
|
|
71
|
+
|
|
72
|
+
elif (n_rows - 1) * n_cols >= self._n_axes:
|
|
73
|
+
self._n_rows -= 1
|
|
74
|
+
|
|
75
|
+
if self._n_axes < 1:
|
|
76
|
+
self.fig = None
|
|
77
|
+
print("destruct fig")
|
|
78
|
+
|
|
79
|
+
del self.axes[key]
|
|
80
|
+
self.keys.remove(key)
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, key):
|
|
83
|
+
return self.axes[key]
|
|
84
|
+
|
|
85
|
+
def set_axes(self):
|
|
86
|
+
for i, key in enumerate(self.keys, 1):
|
|
87
|
+
self.axes[key](self.fig, self._n_rows, self._n_cols, i)
|
|
88
|
+
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
pass
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
""" Live multi-panel backtest figure for neural-network training.
|
|
5
|
+
|
|
6
|
+
Provides :class:`BacktestNeuralNet`, which composes the dynamic
|
|
7
|
+
loss / accuracy / performance plots from
|
|
8
|
+
:mod:`fynance.backtest.dynamic_plot_backtest` into a single updating figure.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Third-party packages
|
|
12
|
+
from matplotlib import pyplot as plt
|
|
13
|
+
|
|
14
|
+
# Local packages
|
|
15
|
+
from fynance.backtest.dynamic_plot_backtest import (
|
|
16
|
+
DynaPlotAccuracy,
|
|
17
|
+
DynaPlotLoss,
|
|
18
|
+
DynaPlotPerf,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = ['BacktestNeuralNet']
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BacktestNeuralNet:
|
|
25
|
+
|
|
26
|
+
def __init__(self, figsize=(9, 6), loss_xlim=None, perf_xlim=None,
|
|
27
|
+
accu_xlim=None, plot_accuracy=False, plot_loss=True,
|
|
28
|
+
plot_perf=False, **subplot_kw):
|
|
29
|
+
# Set dynamic plot object
|
|
30
|
+
n_rows = plot_accuracy + plot_loss + plot_perf
|
|
31
|
+
self.f, self.axes = plt.subplots(n_rows, 1, figsize=figsize,
|
|
32
|
+
**subplot_kw)
|
|
33
|
+
|
|
34
|
+
if n_rows == 1:
|
|
35
|
+
self.axes = [self.axes]
|
|
36
|
+
|
|
37
|
+
plt.ion()
|
|
38
|
+
self.accu_is_plot = False
|
|
39
|
+
self.loss_is_plot = False
|
|
40
|
+
self.perf_is_plot = False
|
|
41
|
+
|
|
42
|
+
if plot_accuracy:
|
|
43
|
+
self.set_plot_accuracy(self.axes[0], accu_xlim=accu_xlim)
|
|
44
|
+
|
|
45
|
+
if plot_loss:
|
|
46
|
+
self.set_plot_loss(self.axes[int(plot_accuracy)],
|
|
47
|
+
loss_xlim=loss_xlim)
|
|
48
|
+
|
|
49
|
+
if plot_perf:
|
|
50
|
+
self.set_plot_perf(self.axes[int(plot_accuracy + plot_loss)],
|
|
51
|
+
perf_xlim=perf_xlim)
|
|
52
|
+
|
|
53
|
+
def set_plot_accuracy(self, ax, accu_xlim=None):
|
|
54
|
+
""" Set plot accuracy object. """
|
|
55
|
+
self.dp_accu = DynaPlotAccuracy(self.f, ax)
|
|
56
|
+
self.dp_accu.ax.grid()
|
|
57
|
+
self.dp_accu.ax.set_autoscaley_on(True)
|
|
58
|
+
|
|
59
|
+
if accu_xlim is not None:
|
|
60
|
+
self.dp_accu.ax.set_xlim(*accu_xlim, auto=False)
|
|
61
|
+
print("setup xlim:", accu_xlim)
|
|
62
|
+
self.dp_accu.ax.set_autoscalex_on(False)
|
|
63
|
+
|
|
64
|
+
else:
|
|
65
|
+
self.dp_accu.ax.set_autoscalex_on(True)
|
|
66
|
+
|
|
67
|
+
def plot_accuracy(self, test, eval, train=None, clear=True):
|
|
68
|
+
""" Plot accuracy scores for test and evaluate set. """
|
|
69
|
+
self.dp_accu.plot(test=test, eval=eval, train=train, clear=clear)
|
|
70
|
+
self.accu_is_plot = True
|
|
71
|
+
|
|
72
|
+
def update_accuracy(self, test, eval, train=None):
|
|
73
|
+
""" Plot accuracy scores for test and evaluate set. """
|
|
74
|
+
self.dp_accu.update(test=test, eval=eval, train=train)
|
|
75
|
+
|
|
76
|
+
def set_plot_loss(self, ax, loss_xlim=None):
|
|
77
|
+
""" Set plot loss object. """
|
|
78
|
+
self.dp_loss = DynaPlotLoss(self.f, ax)
|
|
79
|
+
self.dp_loss.ax.grid()
|
|
80
|
+
self.dp_loss.ax.set_autoscaley_on(True)
|
|
81
|
+
|
|
82
|
+
if loss_xlim is not None:
|
|
83
|
+
self.dp_loss.ax.set_xlim(*loss_xlim, auto=False)
|
|
84
|
+
print("setup xlim:", loss_xlim)
|
|
85
|
+
self.dp_loss.ax.set_autoscalex_on(False)
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
self.dp_loss.ax.set_autoscalex_on(True)
|
|
89
|
+
|
|
90
|
+
def plot_loss(self, test, eval, train=None, clear=True):
|
|
91
|
+
""" Plot loss function values for test and evaluate set. """
|
|
92
|
+
self.dp_loss.plot(test=test, eval=eval, train=train, clear=clear)
|
|
93
|
+
self.loss_is_plot = True
|
|
94
|
+
|
|
95
|
+
def update_loss(self, test, eval, train=None):
|
|
96
|
+
""" Plot loss function values for test and evaluate set. """
|
|
97
|
+
self.dp_loss.update(test=test, eval=eval, train=train)
|
|
98
|
+
|
|
99
|
+
def set_plot_perf(self, ax, perf_xlim=None):
|
|
100
|
+
# set perf plot
|
|
101
|
+
self.dp_perf = DynaPlotPerf(self.f, ax)
|
|
102
|
+
self.dp_perf.ax.grid()
|
|
103
|
+
self.dp_perf.ax.set_autoscaley_on(True)
|
|
104
|
+
if perf_xlim is not None:
|
|
105
|
+
self.dp_perf.ax.set_xlim(*perf_xlim, auto=False)
|
|
106
|
+
print("setup xlim:", perf_xlim)
|
|
107
|
+
self.dp_perf.ax.set_autoscalex_on(False)
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
self.dp_perf.ax.set_autoscalex_on(True)
|
|
111
|
+
|
|
112
|
+
def plot_perf(self, test, eval, underlying=None, index=None, clear=True):
|
|
113
|
+
""" Plot performance values for test and eval set. """
|
|
114
|
+
self.dp_perf.plot(test=test, eval=eval, underlying=underlying,
|
|
115
|
+
index=index, clear=clear)
|
|
116
|
+
self.perf_is_plot = True
|
|
117
|
+
|
|
118
|
+
def update_perf(self, test, eval, underlying=None, index=None, clear=True):
|
|
119
|
+
""" Update performance values for test and eval set. """
|
|
120
|
+
self.dp_perf.update(test=test, eval=eval, underlying=underlying,
|
|
121
|
+
index=index, clear=clear)
|
fynance/backtest/cost.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
""" Transaction cost models for the vectorized backtest engine.
|
|
5
|
+
|
|
6
|
+
Concretizes the :class:`~fynance.core.protocols.CostModel` seam. Only the
|
|
7
|
+
proportional (turnover-based) model ships in 2.0; non-linear slippage is a
|
|
8
|
+
documented extension point.
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
# Third-party packages
|
|
15
|
+
import numpy as np
|
|
16
|
+
from numpy.typing import NDArray
|
|
17
|
+
|
|
18
|
+
# Local packages
|
|
19
|
+
from fynance.portfolio.sizing import transaction_cost
|
|
20
|
+
|
|
21
|
+
__all__ = ['ProportionalCost']
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProportionalCost:
|
|
25
|
+
""" Proportional transaction cost: ``(fee + slippage) * turnover``.
|
|
26
|
+
|
|
27
|
+
Turnover at each step is the absolute traded weight
|
|
28
|
+
:math:`\\sum_i |w_{t,i} - w_{t-1,i}|` (the first step charges the initial
|
|
29
|
+
position). Conforms to :class:`~fynance.core.protocols.CostModel`.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
fee : float
|
|
34
|
+
Proportional fee per unit traded (e.g. ``0.001`` = 10 bps).
|
|
35
|
+
slippage : float
|
|
36
|
+
Additional proportional slippage per unit traded.
|
|
37
|
+
|
|
38
|
+
Examples
|
|
39
|
+
--------
|
|
40
|
+
>>> import numpy as np
|
|
41
|
+
>>> cost = ProportionalCost(fee=0.01)
|
|
42
|
+
>>> cost(np.array([[1.0, 0.0], [0.5, 0.5], [0.5, 0.5]]))
|
|
43
|
+
array([0.01, 0.01, 0. ])
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, fee: float = 0.0, slippage: float = 0.0):
|
|
48
|
+
""" Store the cost rates. """
|
|
49
|
+
self.fee = fee
|
|
50
|
+
self.slippage = slippage
|
|
51
|
+
|
|
52
|
+
def __call__(self, weights: NDArray) -> NDArray[np.float64]:
|
|
53
|
+
""" Return the per-step proportional cost of a weight book. """
|
|
54
|
+
rate = self.fee + self.slippage
|
|
55
|
+
|
|
56
|
+
if rate == 0.0:
|
|
57
|
+
w = np.asarray(weights, dtype=np.float64)
|
|
58
|
+
|
|
59
|
+
return np.zeros(w.shape[0], dtype=np.float64)
|
|
60
|
+
|
|
61
|
+
return transaction_cost(weights, fee=rate)
|