Skip to content

Commit 830d714

Browse files
ratchet
1 parent c583c3c commit 830d714

File tree

10 files changed

+231
-11
lines changed

10 files changed

+231
-11
lines changed

Diff for: precise/skaters/covariance/bufsk.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44
from sklearn.covariance import GraphicalLasso, GraphicalLassoCV, LedoitWolf, MinCovDet, OAS, EmpiricalCovariance
55

66

7-
# Sklearn
7+
# A collection of "online" covariance skaters that just call down to batch-style
8+
# sklearn covariance estimators (so not terribly efficient) evaluated on a buffer.
89

910

10-
11-
12-
1311
def buf_sk_emp_pcov_d0_n100(y: X_TYPE = None, s: dict = None, k=1, e=1):
1412
""" Sklearn empirical covariancecomparisonutil based estimator for IID observations """
1513
assert k == 1
@@ -77,7 +75,6 @@ def buf_sk_glcv_pcov_d0_n100_t0(y: X_TYPE = None, s: dict = None, k=1, e=1):
7775
return buf_sk_factory(cls=GraphicalLassoCV, y=y, s=s, n_buffer=100, n_emp=30, cls_kwargs=cls_kwargs, e=e)
7876

7977

80-
8178
def buf_sk_glcv_pcov_d0_n200(y: X_TYPE = None, s: dict = None, k=1, e=1):
8279
""" Graphical Lasso CV covariancecomparisonutil based estimator for IID observations """
8380
assert k == 1
@@ -186,10 +183,10 @@ def buf_sk_oas_pcov_d1_n300(y: X_TYPE = None, s: dict = None, k=1, e=1):
186183
return d1_factory(f=buf_sk_oas_pcov_d0_n300, y=y, s=s, k=1, e=e)
187184

188185

189-
def buf_sk_gl_pcov_d1_n100(y: X_TYPE = None, s: dict = None, n_buffer: int = 100, k=1, e=1):
186+
def buf_sk_gl_pcov_d1_n100(y: X_TYPE = None, s: dict = None, k=1, e=1):
190187
""" GL based estimator for IID changes """
191188
assert k == 1
192-
return d1_factory(f=buf_sk_gl_pcov_d0_n100, y=y, s=s, n_buffer=n_buffer, e=e)
189+
return d1_factory(f=buf_sk_gl_pcov_d0_n100, y=y, s=s, e=e)
193190

194191

195192
def buf_sk_glcv_pcov_d1_n100(y: X_TYPE = None, s: dict = None, k=1, e=1):

Diff for: precise/skaters/managers/buyandholdfactory.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def buy_and_hold_manager_factory(mgr, j: int, y, s: dict, e=1000, q=1.0):
4444
s_mgr = s['s_mgr']
4545
_ignore_w, s_mgr = mgr(y=y, s=s_mgr, e=-1)
4646
s['s_mgr'] = s_mgr
47-
# ... instead we let it ride
47+
# ... instead we just hold the assets
4848
w_prev = s['w']
4949
w = normalize([wi * math.exp(yi) for wi, yi in zip(w_prev, y)])
5050
s['w'] = [wi for wi in w]

Diff for: precise/skaters/managers/ratchetfactory.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
2+
import math
3+
from precise.skaters.locationutil.vectorfunctions import normalize
4+
from copy import deepcopy
5+
import numpy as np
6+
7+
8+
def index_of_closest(w, ws):
9+
l1s = [ sum(abs(np.array(w)-np.array(wsi))) for wsi in ws ]
10+
return l1s.index(min(l1s))
11+
12+
13+
def ratchet_manager_factory(mgr, y, s:dict, e=-1, j:int=1000000, q=0.8, vup=1.1, vdn=0.85):
14+
"""
15+
16+
Sets a new target weight when either s['count'] % j = 0 or e>0
17+
Otherwise, we use the stale target and ratchet towards it.
18+
19+
For this to make any sense, 'y' must be changes in log prices.
20+
The manager must respect the "e" convention. That is, the
21+
manager must do little work when e<0
22+
23+
:param mgr: Any "manager"
24+
:param y:
25+
:param s: State
26+
:param e: If e>0 is send, this will be passed to the manager and we assume a full update
27+
:param j:
28+
:param q: New portfolio is q*w + (1-q)*w_prev
29+
:param vup: Upper multiplier of target weights
30+
:param vdn: Lower multiplier of target weights
31+
:param n_seed Number of times to call the manager repeatedly with (almost) the same state
32+
:param mgr_kwargs:
33+
:return: w Portfolio weights
34+
"""
35+
if s.get('w') is None:
36+
# Initialization
37+
s['count'] = 0
38+
s_mgr = {}
39+
w, s_mgr = mgr(y=y, s=s_mgr, e=1000)
40+
s['s_mgr'] = s_mgr
41+
s['w'] = w
42+
return w, s
43+
else:
44+
s['count'] = s['count']+1
45+
if (s['count'] % j == 0) or (e>0):
46+
# Sporadically use the manager to change the target weights
47+
s_mgr = s['s_mgr']
48+
s['w_target'] = mgr(y=y, s=s_mgr, e=e)
49+
50+
# Buy and hold
51+
w_prev = s['w']
52+
w_roll = normalize([wi * math.exp(yi) for wi, yi in zip(w_prev, y)])
53+
54+
# Use the last call to set new manager state.
55+
s['s_mgr'] = s_mgr
56+
57+
# Then move towards target
58+
w = [ wi*q + (1-q)*wpi for wi, wpi in zip(s['w_target'], w_roll) ]
59+
s['w'] = [wi for wi in w]
60+
return w, s
61+
else:
62+
# Tell the manager not to worry too much about this data point, as the weights won't be used
63+
s_mgr = s['s_mgr']
64+
_ignore_w, s_mgr = mgr(y=y, s=s_mgr, e=e)
65+
s['s_mgr'] = s_mgr
66+
# ... instead we let it ride
67+
w_prev = s['w']
68+
w_roll = normalize( [ wi*math.exp(yi) for wi,yi in zip(w_prev,y)] )
69+
70+
# Now we ratchet by generating possible trades outside the no-trade zone
71+
w_upper = [ wi*vup for wi in s['w_target']]
72+
w_lower = [ wi*vdn for wi in s['w_target']]
73+
74+
# Convert trade list to weight changes
75+
from precise.skaters.managerutil.ratcheting import ratchet_trades
76+
dw = ratchet_trades(w=w_roll, w_lower=w_lower, w_upper=w_upper)
77+
w = [wi+dwi for wi,dwi in zip(w_roll,dw )]
78+
79+
return w, s
80+
81+

Diff for: precise/skaters/managerutil/ratcheting.md

Whitespace-only changes.

Diff for: precise/skaters/managerutil/ratcheting.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
2+
3+
def ratchet_trades(w, w_lower, w_upper, min_dw:float=1e-6)->[float]:
4+
"""
5+
Given a current portfolio w, and upper and lower envelopes, this
6+
procedure will try to construct a list of allocation adjustments dw summing
7+
to zero that take w to a new portfolio w+dw that is closer to satisfying
8+
9+
w_lower <= w + dw <= w_upper
10+
11+
The algorithm is somewhat heuristic but motivated by results suggesting a
12+
no-trade region for portfolios managed on a continuous basis in the presence of
13+
trading costs.
14+
15+
16+
:param w: Current portfolio
17+
:param w_lower: Lower envelope
18+
:param w_upper: Upper envelope
19+
:param min_dw: Minimum size of a reallocation
20+
:return: dw: Trades that move w towards envelope
21+
"""
22+
23+
# Create a list of sell and buy opportunities (size, direction, ndx) in decreasing size
24+
sells = sorted(
25+
[[max(0, wi - wui), -1, i] for i, (wi, wui) in enumerate(zip(w, w_upper)) if (wi > wui + min_dw)],
26+
reverse=True)
27+
buys = sorted(
28+
[[max(0, wli - wi), 1, i] for i, (wi, wli) in enumerate(zip(w, w_lower)) if (wli - wi > min_dw)],
29+
reverse=True)
30+
31+
if len(sells):
32+
trades = [sells.pop(0)]
33+
elif len(buys):
34+
trades = [buys.pop(0)]
35+
else:
36+
# We're already in the envelope
37+
return [0 for _ in w]
38+
39+
# Walk down the queues and greedy balance buys and sells
40+
while True:
41+
net = sum([trade[0] * trade[1] for trade in trades])
42+
if (net > 0):
43+
# We have been net buyers thus far
44+
if len(sells) == 0:
45+
break
46+
else:
47+
trades.append(sells.pop(0))
48+
else:
49+
# We have been net sellers thus far
50+
if len(buys) ==0:
51+
break
52+
else:
53+
trades.append(buys.pop(0))
54+
55+
# Walk back up and balance
56+
pos = len(trades)-1
57+
while True:
58+
tr = trades[pos]
59+
tr_dir = tr[1]
60+
if ((net > 0) and (tr_dir>0)) or ((net < 0) and (tr_dir<0)):
61+
trades[pos][0] = trades[pos][0] - abs(net)
62+
break
63+
else:
64+
pos = pos-1
65+
66+
# Kill zero trades
67+
trades = [ tr for tr in trades if abs(tr[1])>1e-9 ]
68+
69+
# Convert back to trades
70+
dw = [0 for _ in w ]
71+
for tr in trades:
72+
dw[tr[2]] = tr[0]*tr[1]
73+
return dw
74+
75+
76+
77+
78+
if __name__=='__main__':
79+
w = [0.1, 0.2, 0.1, 0.05, 0.025, 0.1, 0.025, 0.3, 0.1]
80+
w_lower = [0.075]*len(w)
81+
w_upper = [0.125]*len(w)
82+
dw = ratchet_trades(w=w,w_lower=w_lower,w_upper=w_upper)
83+
84+
85+

Diff for: precise/skaters/portfoliostatic/weakportfactory.py

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def rel_entropish(w):
3636
""" Always non-positive """
3737
return entropish(w) - max_entropish(w)
3838

39+
3940
def scaled_entropish(w):
4041
return rel_entropish(w)/rel_entropish_like(w)
4142

Diff for: setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
setup(
99
name="precise",
10-
version="0.13.4",
11-
description="Online covariancecomparisonutil, precision, portfolios and ensembles",
10+
version="0.13.5",
11+
description="The home of Schur Hierarchical Portfolios: an aesthetically pleasing version of Hierarchical Risk Parity",
1212
long_description=README,
1313
long_description_content_type="text/markdown",
1414
url="https://github.com/microprediction/precise",

Diff for: tests/managers/test_schur_l21.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def test_random_schur_manager():
66
from precise.skaters.managers.schurmanagers import schur_diag_weak_pm_t0_r050_n25_s5_g010_l21_long_manager as mgr
77
j = random.choice([1, 5, 20])
88
q = random.choice([1.0, 0.1])
9-
manager_debug_run(mgr=mgr,j=j, q=q, n_obs=500,n_dim=67)
9+
manager_debug_run(mgr=mgr,j=j, q=q, n_obs=50,n_dim=17)
1010
print(mgr.__name__+' ran okay')
1111

1212

Diff for: tests/managerutil/__init__.py

Whitespace-only changes.

Diff for: tests/managerutil/test_ratchet.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from precise.skaters.managerutil.ratcheting import ratchet_trades
2+
import numpy as np
3+
4+
5+
def test_ratchet_trades_with_no_opportunity():
6+
w = [5/15, 5/15, 5/15]
7+
w_upper = [6/15, 6/15, 6/15]
8+
w_lower = [4/15, 4/15, 4/15]
9+
min_dw = 2
10+
expected_output = [0, 0, 0]
11+
assert ratchet_trades(w, w_lower, w_upper, min_dw) == expected_output
12+
13+
14+
def test_ratchet_trades_with_opportunity():
15+
w = [7/21, 7/21, 7/21]
16+
w_upper = [10/21, 6/21, 10/21]
17+
w_lower = [8/21, 4/21, 8/21]
18+
min_dw = 0.1
19+
dw = ratchet_trades(w, w_upper, w_lower, min_dw)
20+
assert dw[1]<0
21+
assert dw[2]>0
22+
23+
24+
def test_ratchet_trades_with_mixed_opportunity():
25+
w = [1/7, 3/7, 3/7]
26+
w_upper = [6/7, 6/7, 2/7]
27+
w_lower = [2/7, 4/7, 1/7]
28+
min_dw = 0.1
29+
dw = ratchet_trades(w, w_lower, w_upper, min_dw)
30+
assert dw[0]+dw[1]>0
31+
assert dw[2]<0
32+
33+
34+
35+
def test_random():
36+
w = sorted(np.random.rand(50))
37+
w = w/sum(w)
38+
39+
w_ = np.random.rand(50)
40+
w_ = w_ / sum(w_)
41+
42+
w_upper = 1.01*w_
43+
w_lower = 0.99*w_
44+
45+
dw = ratchet_trades(w=w, w_lower=w_lower, w_upper=w_upper, min_dw=0.001)
46+
assert abs(sum(dw))<1e-6
47+
48+
49+
50+
51+
52+
if __name__=='__main__':
53+
test_random()
54+
test_ratchet_trades_with_mixed_opportunity()
55+
test_ratchet_trades_with_opportunity()
56+
test_ratchet_trades_with_no_opportunity()

0 commit comments

Comments
 (0)