Skip to content

Commit ad8d46b

Browse files
committed
refactored portfolio
1 parent cb230a4 commit ad8d46b

File tree

1 file changed

+84
-106
lines changed

1 file changed

+84
-106
lines changed

tia/analysis/model/port.py

+84-106
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import pandas as pd
44

5-
from tia.analysis.plots import plot_return_on_dollar
65
from tia.analysis.model.interface import CostCalculator, EodMarketData, PositionColumns as PC
76
from tia.analysis.model.pos import Positions
87
from tia.analysis.model.ret import RoiiRetCalculator
98
from tia.analysis.model.txn import Txns
9+
from tia.analysis.util import insert_level
1010
from tia.util.decorator import lazy_property
1111

1212

@@ -68,60 +68,21 @@ def __init__(self, pricer, trades, ret_calc=None):
6868

6969
txns = lazy_property(lambda self: Txns(self.trades, self.pricer, self.ret_calc), 'txns')
7070
positions = lazy_property(lambda self: Positions(self.txns), 'positions')
71+
pl = property(lambda self: self.txns.pl)
72+
performance = property(lambda self: self.txns.performance)
73+
74+
# --------------------------------------------------
75+
# direct access to common attributes
76+
dly_pl = property(lambda self: self.pl.dly)
77+
monthly_pl = property(lambda self: self.pl.monthly)
78+
dly_rets = property(lambda self: self.performance.dly)
79+
monthly_rets = property(lambda self: self.performance.monthly)
7180

7281
def clear_cache(self):
7382
for attr in ['_txns', '_positions', '_long', '_short']:
7483
if hasattr(self, attr):
7584
delattr(self, attr)
7685

77-
# Direct access to the series
78-
ltd_pl = property(lambda self: self.txns.pl.ltd)
79-
weekly_pl = property(lambda self: self.txns.pl.weekly)
80-
monthly_pl = property(lambda self: self.txns.pl.monthly)
81-
quarterly_pl = property(lambda self: self.txns.pl.quarterly)
82-
annual_pl = property(lambda self: self.txns.pl.annual)
83-
dly_pl = property(lambda self: self.txns.pl.dly)
84-
ltd_rets = property(lambda self: self.txns.rets.ltd)
85-
weekly_rets = property(lambda self: self.txns.rets.weekly)
86-
monthly_rets = property(lambda self: self.txns.rets.monthly)
87-
quarterly_rets = property(lambda self: self.txns.rets.quarterly)
88-
annual_rets = property(lambda self: self.txns.rets.annual)
89-
dly_rets = property(lambda self: self.txns.rets.dly)
90-
# direct access to details
91-
ltd_txn_pl_frame = property(lambda self: self.txns.pl.ltd_txn_frame)
92-
dly_txn_pl_frame = property(lambda self: self.txns.pl.dly_txn_frame)
93-
ltd_pl_frame = property(lambda self: self.txns.pl.ltd_frame)
94-
dly_pl_frame = property(lambda self: self.txns.pl.dly_frame)
95-
# direct access to stats
96-
weekly_ret_stats = property(lambda self: self.txns.rets.weekly_stats)
97-
monthly_ret_stats = property(lambda self: self.txns.rets.monthly_stats)
98-
quarterly_ret_stats = property(lambda self: self.txns.rets.quarterly_stats)
99-
annual_ret_stats = property(lambda self: self.txns.rets.annual_stats)
100-
dly_ret_stats = property(lambda self: self.txns.rets.dly_stats)
101-
weekly_pl_stats = property(lambda self: self.txns.pl.weekly_stats)
102-
monthly_pl_stats = property(lambda self: self.txns.pl.monthly_stats)
103-
quarterly_pl_stats = property(lambda self: self.txns.pl.quarterly_stats)
104-
annual_pl_stats = property(lambda self: self.txns.pl.annual_stats)
105-
dly_pl_stats = property(lambda self: self.txns.pl.dly_stats)
106-
107-
position_frame = property(lambda self: self.positions.frame)
108-
109-
def plot_ret_on_dollar(self, freq='M', title=None, show_maxdd=1, figsize=None, ax=None, append=0, label=None,
110-
**plot_args):
111-
freq = freq.lower()
112-
if freq == 'a':
113-
rets = self.annual_rets
114-
elif freq == 'q':
115-
rets = self.quarterly_rets
116-
elif freq == 'm':
117-
rets = self.monthly_rets
118-
elif freq == 'w':
119-
rets = self.weekly_rets
120-
else:
121-
rets = self.dly_rets.asfreq('B')
122-
plot_return_on_dollar(rets, title=title, show_maxdd=show_maxdd, figsize=figsize, ax=ax, append=append,
123-
label=label, **plot_args)
124-
12586
def subset(self, pids):
12687
txns = self.txns
12788
stxns = txns.subset(pids)
@@ -147,7 +108,7 @@ def short(self):
147108
winner = property(lambda self: PortfolioSubset.winners(self))
148109
loser = property(lambda self: PortfolioSubset.losers(self))
149110

150-
def buy_and_hold(self, qty=1., start=None, end=None, start_px=None, end_px=None):
111+
def buy_and_hold(self, qty=1., start_dt=None, end_dt=None, start_px=None, end_px=None):
151112
"""Construct a portfolio which opens a position with size qty at start (or first data in pricer) and
152113
continues to the specified end date. It uses the end of day market prices defined by the pricer
153114
(or prices supplied)
@@ -161,23 +122,18 @@ def buy_and_hold(self, qty=1., start=None, end=None, start_px=None, end_px=None)
161122
"""
162123
from tia.analysis.model.trd import TradeBlotter
163124

164-
pricer = self.pricer
165-
eod = pricer.get_eod_frame().close
166-
eod_start, eod_end = eod.index[0], eod.index[-1]
167-
168-
start = start and pd.to_datetime(start) or eod_start
169-
end = end and pd.to_datetime(end) or eod_end
170-
171-
if start != eod_start or end != eod_end:
172-
pricer = pricer.truncate(start, end)
125+
eod = self.pricer.get_eod_frame().close
173126

174-
start_px = start_px or eod[start]
175-
end_px = end_px or eod[end]
127+
start_dt = start_dt and pd.to_datetime(start_dt) or eod.index[0]
128+
start_px = start_px or eod.asof(start_dt)
129+
end_dt = end_dt and pd.to_datetime(end_dt) or eod.index[-1]
130+
end_px = end_px or eod.asof(end_dt)
176131

132+
pricer = self.pricer.trunace(start_dt, end_dt)
177133
blotter = TradeBlotter()
178-
blotter.ts = start
134+
blotter.ts = start_dt
179135
blotter.open(qty, start_px)
180-
blotter.ts = end
136+
blotter.ts = end_dt
181137
blotter.close(end_px)
182138
trds = blotter.trades
183139
return SingleAssetPortfolio(pricer, trds, ret_calc=self.ret_calc)
@@ -240,20 +196,24 @@ def __init__(self):
240196
self.total_key = 'All'
241197
self.iter_fcts = []
242198

243-
def __call__(self, port, analyze_fct):
244-
"""
245-
analyze_fct: fct(port) which can return Series, or map of key to Series. If key to series, then
199+
def __call__(self, port, analyze_fct=None):
200+
""" analyze_fct: fct(port) which can return Series, or map of key to Series. If key to series, then
246201
the key is used as an additional index value.
202+
203+
:param port: Portfolio or dict of key->Portfolio
204+
:param analyze_fct:
205+
:return:
247206
"""
248-
results = []
249207
iter_fcts = self.iter_fcts
250208
lvls = len(iter_fcts)
251209

252-
def _iter_all_lvls(lvl, keys, parent):
210+
analyze_fct = self.analyze_returns if analyze_fct is None else analyze_fct
211+
212+
def _iter_all_lvls(lvl, keys, parent, results):
253213
if lvl < (lvls - 1):
254214
# exhaust combinations
255215
for key, child in iter_fcts[lvl](parent):
256-
_iter_all_lvls(lvl + 1, keys + [key], child)
216+
_iter_all_lvls(lvl + 1, keys + [key], child, results)
257217
else:
258218
# at the bottom
259219
for key, child in iter_fcts[lvl](parent):
@@ -274,13 +234,37 @@ def _iter_all_lvls(lvl, keys, parent):
274234
results.append(v)
275235

276236
if lvls == 0:
277-
res = analyze_fct(port)
278-
if isinstance(res, pd.Series):
279-
res = res.to_frame().T
280-
results.append(res)
237+
def _get_res(p):
238+
res = analyze_fct(p)
239+
return res.to_frame().T if isinstance(res, pd.Series) else res
240+
241+
if hasattr(port, 'iteritems'):
242+
pieces = []
243+
for k, p in port.iteritems():
244+
res = _get_res(p)
245+
defidx = res.index.nlevels == 1 and (res.index == 0).all()
246+
res = insert_level(res, k, axis=1, level_name='lvl1')
247+
if defidx:
248+
res.index = res.index.droplevel(1)
249+
pieces.append(res)
250+
return pd.concat(pieces)
251+
else:
252+
return _get_res(port)
281253
else:
282-
_iter_all_lvls(0, [], port)
283-
return pd.concat(results)
254+
if hasattr(port, 'iteritems'):
255+
pieces = []
256+
for k, p in port.iteritems():
257+
results = []
258+
_iter_all_lvls(0, [], p, results)
259+
tmp = pd.concat(results)
260+
tmp.index.names = ['lvl%s' % (i + 2) for i in range(len(tmp.index.names))]
261+
tmp = insert_level(tmp, k, level_name='lvl1', axis=1)
262+
pieces.append(tmp)
263+
return pd.concat(pieces)
264+
else:
265+
results = []
266+
_iter_all_lvls(0, [], port, results)
267+
return pd.concat(results)
284268

285269
def add_iter_fct(self, siter):
286270
self.iter_fcts.append(siter)
@@ -290,8 +274,8 @@ def include_win_loss(self, total=1):
290274
def _split_port(port):
291275
if total:
292276
yield self.total_key, port
293-
yield 'Winner', PortfolioSubset.winners(port)
294-
yield 'Loser', PortfolioSubset.losers(port)
277+
yield 'winner', PortfolioSubset.winners(port)
278+
yield 'loser', PortfolioSubset.losers(port)
295279

296280
self.add_iter_fct(_split_port)
297281
return self
@@ -300,28 +284,28 @@ def include_long_short(self, total=1):
300284
def _split_port(port):
301285
if total:
302286
yield self.total_key, port
303-
yield 'Long', port.long
304-
yield 'Short', port.short
287+
yield 'long', port.long
288+
yield 'short', port.short
305289

306290
self.add_iter_fct(_split_port)
307291
return self
308292

309293
@staticmethod
310294
def analyze_returns(port):
311-
mstats = port.monthly_ret_stats
312-
dstats = port.dly_ret_stats
295+
monthly = port.performance.monthly_details
296+
dly = port.performance.dly_details
313297
stats = port.positions.stats
314298
data = OrderedDict()
315-
data[('port', 'cagr')] = mstats.total_ann
316-
data[('port', 'mret avg')] = mstats.ret_avg
317-
data[('port', 'mret avg ann')] = mstats.ret_avg_ann
318-
data[('port', 'mret std ann')] = mstats.std_ann
319-
data[('port', 'sharpe ann')] = mstats.sharpe_ann
320-
data[('port', 'sortino')] = mstats.sortino
321-
data[('port', 'maxdd')] = dstats.maxdd
322-
data[('port', 'maxdd dt')] = dstats.maxdd_dt
323-
data[('port', 'avg dd')] = dstats.dd_avg
324-
data[('port', 'nmonths')] = mstats.cnt
299+
data[('port', 'ltd ann')] = monthly.ltd_ann
300+
data[('port', 'mret avg')] = monthly.mean
301+
data[('port', 'mret avg ann')] = monthly.mean_ann
302+
data[('port', 'mret std ann')] = monthly.std_ann
303+
data[('port', 'sharpe ann')] = monthly.sharpe_ann
304+
data[('port', 'sortino')] = monthly.sortino
305+
data[('port', 'maxdd')] = dly.maxdd
306+
data[('port', 'maxdd dt')] = dly.maxdd_dt
307+
data[('port', 'avg dd')] = dly.dd_avg
308+
data[('port', 'nmonths')] = monthly.cnt
325309
# pos data
326310
data[('pos', 'cnt')] = stats.cnt
327311
data[('pos', 'win cnt')] = stats.win_cnt
@@ -337,20 +321,20 @@ def analyze_returns(port):
337321

338322
@staticmethod
339323
def analyze_pl(port):
340-
mstats = port.monthly_pl_stats
341-
dstats = port.dly_pl_stats
324+
monthly = port.pl.monthly_details
325+
dstats = port.pl.dly_details
342326
stats = port.positions.stats
343327
data = OrderedDict()
344-
data[('port', 'ltd')] = mstats.pl.sum()
345-
data[('port', 'mpl avg')] = mstats.avg
346-
data[('port', 'mpl std')] = mstats.std
347-
data[('port', 'mpl std ann')] = mstats.std_ann
348-
data[('port', 'mpl max')] = mstats.pl.max()
349-
data[('port', 'mpl min')] = mstats.pl.min()
328+
data[('port', 'ltd')] = monthly.ltd_frame.pl.iloc[-1]
329+
data[('port', 'mpl avg')] = monthly.mean
330+
data[('port', 'mpl std')] = monthly.std
331+
data[('port', 'mpl std ann')] = monthly.std_ann
332+
data[('port', 'mpl max')] = monthly.frame.pl.max()
333+
data[('port', 'mpl min')] = monthly.frame.pl.min()
350334
data[('port', 'maxdd')] = dstats.maxdd
351335
data[('port', 'maxdd dt')] = dstats.maxdd_dt
352336
data[('port', 'avg dd')] = dstats.dd_avg
353-
data[('port', 'nmonths')] = mstats.cnt
337+
data[('port', 'nmonths')] = monthly.cnt
354338
# pos data
355339
data[('pos', 'cnt')] = stats.cnt
356340
data[('pos', 'win cnt')] = stats.win_cnt
@@ -361,9 +345,3 @@ def analyze_pl(port):
361345
data[('pos', 'pl min')] = stats.pl_min
362346
data[('pos', 'pl max')] = stats.pl_max
363347
return pd.Series(data, index=pd.MultiIndex.from_tuples(data.keys()))
364-
365-
366-
367-
368-
369-

0 commit comments

Comments
 (0)