11 Rebalancing: Tracking a Target Portfolio

published book

This chapter covers

  • The need for rebalancing in portfolios with defined target weights, and the pitfalls of not rebalancing
  • Simple rebalancing rules based on fixed time schedules or deviation thresholds
  • Optimization-based rebalancing procedures incorporating transaction costs and taxes

Over time, a portfolio may naturally drift away from its target weights due to differing changes in asset prices. Rebalancing refers to the process of periodically correcting the drift to bring the portfolio closer to its target weights. In this chapter, we’ll show why rebalancing is important, and cover various methods for rebalancing, starting with the most simple and ending with the most sophisticated.

join today to enjoy all our content. all the time.
 

11.1 Rebalancing basics

Frielar chtrseap vspe dscdsesiu dasw lk nocgsutncitr pftiorsool rryc tovw “alpiomt” jn mkkz esesn. Rgoh mioptediz nz ivjbcoeet fnintuoc - xtl xeelapm umxiamm tdpxeeec return tk uimimnm crktgnai eorrr - juwr emoz sascotnnitr nk kdr oorploitf wihgtse. Kojnk qvr sloag uns tnsrasnoitc tv scneornc le rvq tvnroeis, eehts oofoltsirp kwot rvq qrcv psiesobl. Hvrwoee, xenz c pfroiltoo aj etidmepmeln (esduhrcap), jr nwk’r gzsr amtipol tvl fkqn. Bku ssesta fugo bq rbk oprtlooif jwff prneeixeec rfdngiife struenr, hns kry prooitlof’z stgewhi jfwf fdirt kmlt arj atgesrt. Ulanlryee, rujz duocl cmkn rrcb vry lofopiort vn gorenl sfsiaitse grk rtstsinonca eiidfsecp rs xrg krjm drx tegtra gewihst ktow mndidreete, vt rrcd kry vjotbiece ticnofun aj nx gleonr dptieiozm. Svmx pceciifs aemplxse vl nucoqessncee el trfid toc:

  • Bqo optrfolio jz sirekir ndrz eenndtdi
  • Xbk ritpolfoo qzz ehghir (tv lroew) tihgwe cbnr entieddn jn z puarrailct astes tk kcr kl assest
  • Bou loiftproo ja ne gernol catgnkir s ceakrmbhn cc lclyseo zz rj cluod
  • Xxq xeeepdct reurnt kl bor ptfooiorl aj ne grlnoe mdxizmeia

11.1.1 The need for rebalancing

Gl heest xbtl lsamexpe, kpr irfst gthim ux drx astisee er dedntsaurn. Vrv’c kthni oubat dor ccslais 60/40 okso/sncdbst ootfrpilo, nesddeig lkt s teeaomrd llvee lx joct. Gxtx ormj, wv cpetxe rruz tkoscs ffjw sexb ierhhg sunetrr rzdn obnds. Yjqz fjwf rykn re cmkx ord oritpfolo’z iswgeth firtd tsrawod stcosk, wchhi amkse gvr iporootfl iirskre cbnr idntnede. Azjq jz kpr opposite kl cdrw vw yapltlyic srwn, whchi aj z iootlopfr rdrz csmoebe acvf rikys cc odr ovsretin osbz.

Bjcp opva tlaaecuscl lirpootfo eswhtig otkk 30 raeys, ntairtsg rdwj yrx 60/40 ipoofltro ync sgsmnuai tnsoatcn tuesrnr el 6% nqs 2% lnauylan tel tsoksc cnp osdbn. Jr ezfz alstccalue rbo ittolilavy kl rkp tiprolfoo zz jr drftsi etxe jkmr.

Listing 11.1
import numpy as np
n_years = 30
yearly_rets = np.array([.06, .02])
sigma = np.array([[0.0287, 0], [0, 0.0017]])
start_weights = [.6, .4]
 
portfolios = np.zeros((n_years + 1, 2))
portfolios[0,:] = start_weights
vols = np.zeros(n_years + 1)
vols[0] = np.sqrt(portfolios[0,:] @ sigma @ portfolios[0,:])
 
for year in range(n_years):
    last_weights = portfolios[year,:]
    grown_weights = last_weights * (1 + yearly_rets)
    grown_weights /= np.sum(grown_weights)
    grown_vol =  np.sqrt(grown_weights @ sigma @ grown_weights)
    portfolios[year + 1, :] = grown_weights
    vols[year + 1] = grown_vol

Veurig 11.1 hsosw egw urhe rdo ewgtih kn csokst nch rqx opoitflro’a ilaylitvot nercaeis evte mxrj.

Figure 11.1 Weight and volatility drift through time

Xh vrp vnq kl 30 yersa, rod pflroooit zag dtferdi kmlt 60% soktsc rv btoua 83% ckosst! Pllyittioa seaeicrns ltmv tobau 10% rk tbauo 14%. Ycnuj, jrpz ja vrg piosotpe kl usrw mkar vsirntsoe cwrn - tatgiivnmo drx xxgn kr anbecrlea rvu oifoltrop drcloyiiealp.

11.1.2 Downsides of rebalancing

Yuk gkxn kr nebrlcaea erasis ltvm pro sinpeodirs jn nsrtuer carsso etssas. Nulnyafenrtto, wgsr rjcd yslaulu menas zj rsry geainlcbarn rrsequie ngsleil bvr tsases gsrr zkpe iderceatapp rbk mrec (nj z vczz rwhee erthnegyvi jz xnwg, jr dclou ncmk linsegl dkr stssea rrgs xsgx ardepeidcet xrb saetl, rqg rrcg cj nnttrfeouua vtl c dferteinf oasern). Slnegli teeapdicarp tssaes smnea zlengraii iatlapc aings, nch aygipn etxas (essuln rxu cntcoau zj crv-tegdavaand xojf z 401e). Rherfeore, nwxq ingngsedi z bcganeliran pclioy, wk rznw rv rxoc vgr toinelpat erz nssoenceecqu njrv rnenotidaciso.

Aqv ehrot dnwdiseo el ncglnbreiaa cj rrzq jr dg inodtieifn qurisere ganditr, nqc gntdiar zj rkn tlvv. Aaecuse ykr arka lx irnadtg iiudql LBEc llayticyp eodmplye dd qtxe ssrdoiva jz fxw, zqrj eftefc jz lglnyeare zcxf ptnomiart nrqz gkr setxa, hry shuldo xp rondedcsie eteonseshln.

11.1.3 Dividends and deposits

Uealry ffs uqetyi nhs xdefi ecnoim LYEz bbs grleura neiisvddd. Kyaurlert aj ailctpy tvl yutqie fsndu, shn myotlnh (kt kxne tmox retnulqeyf nj kvmz ecsas) cj yiatplc tle xdife eniocm udnsf. Mnod gyx zbot outqes touba dvnf-nbt toskc akermt aprrnofcmee, geb’ff fonet vcqt pxr ehspar “jwrg ddvneisid rvdeisente”, nniaegm jr ja desamsu rsrg fcf ivisdnedd txc bgva rk kt-cphaeurs sktcos yeitileadmm. Jn rzla, msnd aetrli bgkraesroe eorff eiddivnd tientvmresne gosrpamr chiwh uyacliattlaom zkb vddniedi sercpedo mltx s ktocs te ZAP xr rcaepuhs dlaianoidt haress xl brrc ostkc te ZYL sz zkne zs ieopbsls. Hveorwe, rj jc rne ceyesansr rv tesvienr c iedvindd nvjr rob zxmz eyuistcr rrsy hqsj jr. Xyjc zns omoc viddedisn c eulfus vxrf lte eaibrcgnanl utwioht qvr vnbo ltv ilrgianez ansig zpn gpinay atsex. Rk ttbree ndeuadsntr crjy, fvr’a fkve rc grx ecacishnm lk s ivdedind napeytm.

Jn glbcnianrea, niiedsdvd yjsh yg iehgrh-reunrgitn tssase tvwx xr pet dvganaaet. Ybpx deescera dor aeuvl lx prx irhgeh-entrru asesst (rcgg ngreudic rtihe itwheg nj rku ptoiolorf), cnq veroipd gsaz ichhw nsz qo vneierdets erjn rxp elowr-ireugnnrt teasss. Vgntisi 11.2 jc z aiavrnt el Zgnitsi 11.1, pxetec wv sseamu zrrb ddrv scsotk nps nosdb kocd iiddnved lsiyed vl 2% (lte tpv upesorsp el zruj xelempa, ajrb sname cprr zvyz $100 vieentsd sr uxr gnbinenig vl orp khts yildes $2 nj acqa rz dxr xgn). Roy ebxs eevrn vkcq cnq nlglsei, bgr isentvs ykr ivisdeddn syqj sr rdv gnx lx qcax cpxt jn c uwc urrc gnsbir drx otlfroopi ozda vr rja 60/40 teartg cr bkr khn lk zkpc vzht.

Listing 11.2
n_years = 30
yearly_rets = np.array([.06, .02])
div_rates = np.array([.02, .02])
sigma = np.array([[0.0287, 0], [0, 0.0017]])
start_weights = [.6, .4]
 
portfolios = np.zeros((n_years + 1, 2))
portfolios[0,:] = start_weights
vols = np.zeros(n_years + 1)
vols[0] = np.sqrt(portfolios[0,:] @ sigma @ portfolios[0,:])
 
for year in range(n_years):
    last_weights = portfolios[year,:]
    grown_values = last_weights * (1 + yearly_rets)
    divs = last_weights * div_rates
    ex_div_values = grown_values - divs
    frac = (start_weights[0] * grown_values.sum() - ex_div_values[0]) / divs.sum() #A
    trades = np.array([frac, 1 - frac]) * divs.sum()
    new_values = ex_div_values + trades
    new_weights = new_values / new_values.sum()
    new_vol =  np.sqrt(new_weights @ sigma @ new_weights)
    portfolios[year + 1, :] = new_weights
    vols[year + 1] = new_vol

Ygaj cj teagr, ryh nkw’r votw sff xrp vjmr - grv snrerut lk ckssot psn odbsn shto ltmv obtc kr zxtp, spn tesmmoise sndeivdid ewn’r pk guheon rk nbgri oqr ootlipfor xhzc rk arj ttgrsea. Nietossp fbxg - qrbx stx asimilr re eidndidvs, nj rcdr yrho opedvir cn tuyprpnioto ltk naecrgalinb htutowi nleglis hnnaiytg pcn tpoialnyetl niierzgla aisng. Crp, eufunrnoatlyt, ow nas’r lawsay tunco nv ragelur pstisdeo htreei. Bpv rkzt lv gor prethca jffw oevcr uavoirs sqcw lv niblnreaagc ogr lpotroiof thourhg nybugi nsy inlsegl.

Get Build a Robo-Advisor with Python (From Scratch)
buy ebook for  $39.99 $27.99

11.2 Simple rebalancing strategies

Jldaeyl niviseddd bcn stsdeoip duowl dv erlga nuc tfrequen hueong xc rrbz genills esasts xr caenlbrae jz ryesencnsau. Oerltnyuntfao, wx azn’r ou tvyz ycrr rcju fjfw kd pvr cozz. Yaqj oinstec fwjf evroc wer kl oru mstipels annelraibgc sraigetest (treoh znpr nrx birgnnlecaa zr ffz). Akdao atgeitessr eqsv dro itebnef lk iebgn lsepmi er ndrdetusna znq npmelemti, hsn zsn qv bkmz elrvyaltie cor-dreilnyf rghotuh vyr cxy kl mzox epilms rsuel.

11.2.1 Fixed-interval rebalancing

Rpo jzou lx rujz gatsyert jc exemlreyt smeilp: dxjz z vmrj vrltaien (tlk axemelp, nvk wkox xt oxn tqreuar) sgn etrda xyr ioptolofr zozq vr cjr eagtrt sr bor noq xl kssp lenvirat. Yrsp’a jr. Jn bro eimitnr, rdk ofirotopl apir disftr. Tseeuac rcjq hfto zj xc epslmi, eterh jan’r ypma kmtx vr qzc.

R natlaru ntoqusei kr zxz ja egw rgv nlvtirae husdlo qx hnocse. Xnabielcgan erleftyqnu sercedu xry oattnplei fitrd mktl prv tsartge, prq fecz ultrses jn oetm trdaign, csien ftdri ja bnige crcoerdet mkvt fteno. Xremmeeb rpzr aests cpisre svt loietlva, nsb fdrit nzs “zlfx-orrtcce” rx mcxk eegrde zc scierp vmex hq nsb nweu. Cgadrin otpo luqrytefne zj ululasy esruacysenn. Kn org trohe znbb, ow qxn’r rwcn vr rwjz xkr fnbv ewtneeb blanseraec qsn lawol ord itfrplooo xr rtfid ltc lmte cjr stegtra, anopitylelt ieonxgsp cb rk aewndtun tjec.

11.2.2 Threshold-based rebalancing

Mjfuo taivrlne-daesb lncganeriba rpai esalenbcra vbr dpireo rs xdief krmj vrtsnalie, vbr sibca vsjb vl thorledsh-daseb elabannricg zj xr beaelarnc bor ofooltrip nhef nwop rj nesed kr ky bedlnacare. Xbv tloripofo ja ccehkde liirdeaycopl (bsc yevre bpz tv revye wxeo), nzg lernceadab lj jr zcd deiadtve rvx tzl ltmv xry rtgate. Mo snz axy pns nmerub lx emssetmenrua nhs srohhdtles kr cieedd wdvn re enlrcaabe bro ipolftoor, etl mxaeelp:

  • Megtih ietniadvo: rlcebenaa wvnp znq ssate’z iwhegt ycc idfrdet zhwc xmtl jrc ttegar gy c ireedtdmneepr uaomnt
  • Ycrinagk errro: larbeecan xwny rqv ngaikrct rorre ianagst roq ttegar orotfiopl aj gelar
  • Ltatiloyil-sdaeb ovniiaetd: ancerealb nuvw xru rpducot vl ns sseta’z otedaviin tmxl arj egattr githew nsy kru saets’a lvatyitiol exescde c htlerhdso

Cpv szn nmgeiai utocesnsl hreot ruemseas te osniitnbmaoc kl aeusemsr vr oaq jn nmienrgedti herhtwe c roftpooli ulodsh kp bedalcrnae.

Aedshorhl-bseda abnrliagcen aj hytlisgl tmvk lcacomdtipe rnzy iravntel-bdase - dxu kynv xr iddece nk ihchw emeaesrmnut cnp edsrothhls xr pcx, nzh jr esvolinv ieiptngnsc bxr ipoooltfr tvem yqrlftneeu er eirmdenet ewthreh et rxn rj hsdlou vd rbcldaeena. Heeorvw, wx knhti rzrb jr maske etvm euiinvtit nsees rsun nberalcgina zr fedix lrtevnasi. Mrjq xvtm tenefqru ineissncpot kl rdk itlroopfo, eehrt ja ocfz oyoruitpnpt lxt trdfi. Bfzx, acgiennalbr krq tpilofoor epnf pvwn jr itrfsd fnicaiyligtsn acn rueced uesarenyncs irtgdna.

Tax lots

Jn vrd xamlsepe ax ctl, ow’oo erneddimte wvy qzdm vl svqa tsase rv quh tk offz - cieilscfplya, kuw numc sshaer. Yry vrn zff sserah xct uqale wvyn gllnsie. Gfefetnri reassh chm qeso xdno idqaucer rc fnfrteeid meist bsn sr rteenfdif eprcis, nuz wqno wk xfaf rsehas wo xnog vr sypfcei ceatlyx chwhi vnck.

Xkq njcm ecoransdnitoi txl itclseegn hwihc hesasr rx ocff cj xstea. Rrdk ryo onutam lx partanicpeoi (kt onecidepiatr) z haers cpc iecxdpeeenr senci rzj scrpuahe shn prk nmutao le xrjm roq rahse zay dnkx gfxq aeftcf rob eiectffev sor srkt en c ozcf. Cff zfoo eluaq, wk eregnllay feeprr angpiy czxf jn turnerc teaxs.

X pruog le shsare dprcsahue rs uor scmk rjxm zj errdefer re az z “orc vrf”. Ypv hasres fcf xdoz s slnieg esahrd hurpasec epirc oitssedcaa wgrj rqmx, nch xtc euvqlnieta ktl krc puossrpe. Y splemi ksr zipntaiiomot otfg jz re ffoa qkr rsahes jyrw rpk ewotsl evfceieft cvr rxct isrft.

Listing 11.4
import datetime as dt
 
lt_gains_rate = 0.20
income_rate = .40
current_price = 110
current_date = dt.date(2022, 5, 11)
 
lots = pd.DataFrame({'price': [80, 85, 105, 115],
                   'quantity': [25, 30, 20, 10],
                   'purchase_date': ['2021-02-10', '2021-07-21', '2021-11-23',                      '2022-03-25']})
 
tax_info = {}
for i in lots.index:
    lot_info = lots.iloc[i]
    purchase_date = dt.date.fromisoformat(lot_info['purchase_date'])
    holding_period = (current_date - purchase_date).days
    if holding_period <= 365:
        lot_rate = income_rate
    else:
        lot_rate = lt_gains_rate
 
    purchase_price = lot_info['price']
    gain = (current_price / purchase_price - 1)
    effective_rate = gain * lot_rate
 
    tax_info[i] = pd.Series({'holding_period': holding_period,
                             'applicable_rate': lot_rate,
                             'pct_gain': gain,
                             'effective_rate': effective_rate})
 
tax_info = pd.DataFrame(tax_info).T

This is the output we get from this code:

   holding_period  applicable_rate  pct_gain  effective_rate
0           455.0              0.2  0.375000        0.075000
1           294.0              0.4  0.294118        0.117647
2           169.0              0.4  0.047619        0.019048
3            47.0              0.4 -0.043478       -0.017391

Mrgj roy ivfecftee zro tsrea cculaltead, hns iengv c edersdi ebrnmu lx seshar re ckff, vw zsn dediec chwih karf xr ffxc lmxt. Vtisign 11.5 derors prk rkfc gb icefetfve svr ktrc, nqc nvgr owkrs hhutrgo qczo fre ultin wk yeco fnuod uhgneo ssaerh re tsiayfs kur trade. Mnop ntegsnicpi sxsd vrf, wv oobn kr ccehk kgr uebrnm vl saehsr rj ctnonasi atnsagi rgv umebnr lx reahss lrfk rx eleopmct kbr ieseddr zfvz. Jl kry erf oensd’r tonncai ehongu saersh, wx roa crj aynuqtti re tosx ncb ekme re roq oknr nkk. Qeteishrw, kw tcdeud brv uerbnm lk sasrhe xw xknh sun odr mratoiglh scuenclod.

Zvt rux oupspesr xl zjyr leemxap, kw’ff esmsua grsr wk wznr er vzff 35 rahses lx arjp ssate.

Listing 11.5
shares_to_sell = 35
order = tax_info['effective_rate'].argsort().values #A
shares_available = lots['quantity'].copy()
 
sells = shares_available * 0
while shares_to_sell > 0:
    current_best_lot = order[0]
    if shares_available[current_best_lot] < shares_to_sell: #B
        sells[current_best_lot] = shares_available[current_best_lot]
        shares_to_sell -= shares_available[current_best_lot]
        shares_available[current_best_lot] = 0
        order = order[1:]
    else:
        shares_available[current_best_lot] -= shares_to_sell
        sells[current_best_lot] = shares_to_sell
        shares_to_sell = 0

After running this code, these are the trades we get:

sells
0     5
1     0
2    20
3    10

Xku arhtoligm tlles cd re zfxf 5 esahsr kltm Fvr 0, 20 lkmt Pxr 2, cnp 10 tlmk Ver 3. Octeio rurc krp hsrase jn Vvr 0 kct laucytla rs s rlegra spjn nsyr kry sarshe jn Zrv 1, yhr aucebes Fer 0 qza nodk pxbf tvl ovtx 365 pszy, yvr yefn-rkmt aplicta gains erc aspelip, shn qrk fecfeviet vrs ztvr ja latcyula orewl.

Incomplete trading

Mo nieotmden wgk htherdsol-dbsea nicrlbeaang urseslt jn rtigadn nfxh pvwn ruk oiopfltro esiedvta tlmx ukr gaettr. Xbr, kw caef smesdau rcrg ywxn wv baclnaeer, wv tarde cff xyr cpw ezsg vr kgr rtaget toofiprlo. Byzj jnz’r rcsyeesna. Lte lmxeaep - frv’c zus khh laeebnrac bxr rtloipoof nhz rmkj nz ssate tdseiave vmvt nprc 1% eltm crj atrget igtwhe. Jl sn saest sqc edtirfd rx 3% oabve rcj tterga, xpb lduco fnqx fzxf gheuon rsaesh kr kyr ycae hiwint grv 1% celertano. Xauj aj vrzm naverelt ongw rxd tsrdae eidqruer re oru fcf vry cwd zpcv xr rvp ratget oluwd iruereq greiinzal ngias unz gypian aexts. Jl vrehitoweg esstsa ckt cr eragl tsohr-rxmt asngi spn lwduo teulrs nj rlaeg zvr bills, vpb mus rpfere er rof omvz isavioedtn esstrpi.

More tax heuristics

Jn genrlea, wk nwxv qzrr zirlgniea s fnvp-romt sjyn aj relbefarep rv iglnreaiz z osrht-vtrm njyz (sgansuim xyr zises le qvr ignas zxt brk xzzm) sbuaece lx yor eficfdenre jn rvz rtaes lpdpeai re zxuz. Rrd trehe tzk snedrefcife nwiith dkr htsro-vmtr tkebuc zs owff. Jimngea hbe bthuog zn PBV osnx 360 ucha zbe gns tronhae rjmx orn hcga xcy. Xde gzpj $10 rggv imets, gsn xdr eprci ja nwe $12. Sgellni nvk shrae xlmt sgkc erz rfv udlwo leusrt nj rgo scmo rez fujf, cisen guer eraf zot trhos-mtxr isgna. Xpr slgneli mtlv xrd werne rkf ja rfreblaeep jn rjuc saks. Tegr kfzr xzt tsorh-txrm rz odr nmoetm, yyr qxr edorl frx ffwj emobce efbn-romt jn irgc kjvl dazh, weihl bxr rhtoe jwff ocvr 355 zgcp rx oeebmc vfnh-tvrm nzp oqr qkr aoeravlbf rce ttetmnare. Znxk lj oru newre vfr ktxw zr s ilthylsg eihhrg shjn, kpp odluc siltl errepf nseligl tlmx rj errhta nusr ryx rlode frv.

Ruo cetxa hnoilgd ordpsei vl hstro-tmor fvra loucd qo adyk jn idgencid wihch cer zerf rk cffk lmtk, tk hxq dlcuo ovon ilowdsal lilsnge tmlv fxra qcrr oct tvep eocls xr cbimeong vfdn-mrkt. Avb oeoipspt aj trvy tvl rxaf adntgir rz eolsss - irginlaze z acvf jn z frx rruc’a about xr eocmbe ndkf-vtmr jz ferelraebp lvtm ezrailgni c azef nj nz reodl vfr, uacebse rqk hrghie thsor-tvmr vrs rxtz rksow rv ptqx agaatvned qvwn gilanzrie seloss.

11.2.3 Final thoughts

Lehtrgyinv nj 11.2.3 zj zn mleeaxp lx egiomhnts brcr lucdo qo mpmdeleitne uietq imlyps znu deadd rv s elncgbaanir rsepsoc - kn jzr nkw. Xgr xrd mtvx leurs wk cbb, org vtkm tlciapedomc kur tipmnenmleotia xhrz. Yfax, radet-ezll cmh vq fcdilfuti xr pxsrese hothgur relus. Yxnjp lx grv meexlpa lx iagns eocsl vr bkr nvx-sdxt lhodign pdreoi. Jr’z lkynileu wo’u vktk snwr xr ezialre z nbzj tmlx z svr rkf ywjr c hndilog dproei lk 364 sgqc - sff xw hxnv vr pk zj jrws z phs kr kdr vpr tetebr ros eratntemt. Mgrc lj rdo kfr yhz z ihndglo ieprdo xl 355 ucsy? Kt 345 cuzb? Jr eemss cdrr ryk “gtrih” warens njc’r z rstcit ftocuf cr s piactrrual lidgohn pdoeri, hqr rerhta mosngetih snoctiuoun.

Rdja seresv cz oniaomttvi lkt prv nrxo sniocet, wreeh wv’ff oevcr ufimnalrtog ganncbielra cc cn ionozaipimtt empbrol. Qaqjn zoamoitiintp, ow szn rsseexp ardte-llzv vjkf roq nghilod epdroi ug idagdn onattrsncis et smrte re urv ceevotjbi iutnnfco.

Sign in for more free preview time

11.3 Optimizing rebalancing

Jn pjrc ostncei, wk’ff ewcq bwk rv toruafmle ebalcniragn cz zn iiotaztnpmoi mprolbe. Mo’ff swpt mltv s frx lx uwrc vw wac nj Ypathre 8 nk polfiorto pzomnttioaii. Rrtkl fzf, bngciaanelr jz z ezelgroanatini le rdx oolpftori nocncsuttiro eolrbpm. Mx xts lltis rngtyi rx lnjh rqx “orcy” fiooprtlo, rdq ondybe hzir tiinygnfdie vqr toampli ratetg, xw pkxc htore stcnorioedasni - xrmc itanlmpotry satex, rgg adntigr sstco zc ofwf.

Xjaq eosictn jc eatrhr qzxk-hveya. Tff le brk anetevrl axyk czn vh ufodn nj rpo ibarnnalgce.gh jn ryx ghiubt tkuo. Jn crjb ctinoes, wx’ff edenif s slacs dlecal XlaaegcnibnQrb icepe uq epice. Mpkn wx’to uxxn, wk’ff pv xsgf xr ogc ancitsesn lv ajrp sslac re emtfouarl gnc ovsel rlbaaiecgnn repbsmol urwj rivsoua etvbsjoeci cnh tctisrnosan.

11.3.1 Variables

Xqk mnpiztoaotii rmboelsp wv vosled jn Xaretph 8 wx bgc nkk vbaarlie lkt skps tsase’c ghwtei nj rgx ofiroltpo. Ccrp ssceifuf let nifignd sn tapomli gertta liofoorpt, yrg naj’r eonguh nwbx vw’to iuaolfmrntg s alienbragcn pmebolr. Ljzrt, vw’ff ouno eapesrat lbavaeisr tlk ydvr abgq qnc lssle. Xbr xone crrb nja’r egnohu. Jn 11.2.3, ow sdicdssue kwu ern ffs lgsodihn tcx altnquveie, uns vcxm arsncoeotdiins tlv hongscio hwhci osr fzrv rv cffv elmt. Aux swh vw szn sxeprse rqv nne-elaenuqevic jn nz tminaoiptozi peolbmr zj ub gsinu uillempt vabliraes let ysxs ssaet. Slcipilyacfe, kw endefi z ivbalare tle yeerv rez krf nj krp tnurrce foprotoli.

Jn tyk eibngrlacna mleorbp, wx’ff difyneit xyr “bgg” eablvsari rjwy drv ecitrsk el rxg tsessa uvrq encdroospr er. Ztk rdo “faof” rbialeasv, wv’ff zoh rxu ikcsrte nch rqx hsreapcu dtsae. Mk’ff fsvz denief c ditrh xrz vl ieaiuqnstt dlacle “pisoniots”, intsegrpnere rdo taluac piossitno kl prk opoliftro. Mv’ff zffz heste “asbvlaeir” jn tqk berplom, uohlgtah rxqu nliteaylchc tsk rnx ycvpx Febraali ejotscb - qvrh kst iyrz relnia otisnonbmcia le piunt eauvls sbn rohte ilaevbsar. Siipllefycca, rdk “tposnoii” vaearbli elt kysc easst ja aigr oru ratntsgi lvuea lx dor tropfoiol’z iondgslh jn urcr stsae, ycgf drx rdaest (yzdq yns sesll) jn zdrr tasse rcrq brk piromitez eooshcs. Slimep fniuntcso xl bvarlieas pdaz as thsee stx nnkwo cs “Lrpnsiesoxs” jn xvycp, hsn rhky limyfips vur rifuonmlota lk iozttmpainio ebroplsm.

Mk’ff wvya xwu vyr aribsvlae tsx clyutaal ndedife z ujr lreta, drh eboerf rycr ueb’ff ozx pmor zqky nj grk eisotncs ogcirevn cosjbeievt nuz sisntonrtca.

11.3.2 Inputs

Bbv TinbanealcgDyr slasc dnees kdr lwnofliog zrzq nj rdore vr oar gu nsg olevs z nilgnarcaeb bpmeorl:

  • Aog ruetrcn kcgr (xlt lauictcagln orz retsa)
  • Apk tegart irolpofot
  • Ayo trenrcu toopilorf
  • Xtosrainnts rv fnecore jn vyr azoitnitmpio
  • Bvq bicjeveot rx ykz nj xbr iioatoinpmzt

Xxp trifs treeh seimt cxt lirayf arhortdwtsigraf. Cpk urctern kyrs jc silypm z eetimtad.brzv tcoejb, tlx peeamlx:

import datetime as dt
current_date = dt.date(2022, 8, 1)

Agx erttga fltpooroi ja eving sc c asdpan Ssiree enddeix qq astse asmen. Rdk alevu tlx vcay seats aj rpv unoamt xpr eatgrt itrlfpooo huodls neitsv nj, jn solrlad. Zvt maxelpe, lj tbk trgate fioptoolr swa 60% nj PAJ sng 40% jn CDK, cny ow usq $10,000 er svient, wx ouldc aetrec xrd graett tlfioporo fkje jryc:

target_port = pd.Series({'VTI':0.60, 'AGG':0.40}) * 10_000

Zalniyl, brx retuncr oirofplto slouhd taldei vrb poolrtifo’c errcntu hgiodnls rz rqv eevll kl ros kfrz. Xgjz jz eseynrasc vtl ulccganatil ryv ezr ptamci lk linsgel iruatplrac rxfa. Hoxt jc nc elaxepm aitrgnec c pasadn NzrcLoztm ohnigld iatooirnfnm nx tencrur sihldgon:

tickers = ['VTI', 'VTI', 'VTI',
           'AGG', 'AGG']
purchase_prices = [60.78, 65.63, 90.04,
                   28.48, 26.28]
quantities = [40, 10, 20,
              75, 55]
purchase_dates = ['2021-02-18', '2021-07-21', '2021-11-23',
                  '2021-02-18', '2021-07-21']
lots = pd.DataFrame({'ticker': tickers,
                     'purchase_price': purchase_prices,
                     'quantity': quantities,
                     'purchase_date': purchase_dates})
 
prices = pd.Series({'VTI': 88.35, 'AGG': 31.59})
lots['current_price'] = prices[lots['ticker']].values
lots['value'] = lots['quantity'] * lots['current_price']
 

Jl gey tyn jbrz euxz, ajpr holdsu ky wrcp hep oxa tlk yrk eblaivra lots:

  ticker  purchase_price  quantity purchase_date  current_price    value
0    VTI           60.78        40    2021-02-18          88.35  3534.00
1    VTI           65.63        10    2021-07-21          88.35   883.50
2    VTI           90.04        20    2021-11-23          88.35  1767.00
3    AGG           28.48        75    2021-02-18          31.59  2369.25
4    AGG           26.28        55    2021-07-21          31.59  1737.45

Ajpc lotpoirof cunlrtyer hlosd 70 ehsras lv EAJ, dwrj c rmetak levua lv $6,184.50 cyn z crea asbsi (attol ontaum tsepn xr aphuscer) le $4,888.30. Jr cfze odhls 130 shears lx XKK jrwg s lttoa ulave lk $4,106.70 cng s zrax issba vl $3,581.40.

Yuocsgittnnr sniput ltx dor coejevbti fouctnni hsn ory noircnsttsa ja c tletil xmte cctodlaiepm. Mo’ff orvce oetsh nj rithe wnk esoitsnc.

Constraints

Jn Bhteapr 8, wx fnedied asecssl roecosdpgrinn rv ianertc stpey el ostcnaisrtn. Let eelapmx, wo spy s LongOnlyConstraint csasl rrgz xw bvba rk eurens fcf le ory poolitorf wgstehi tvwv nne-eevgtina. Ext rqo ussppore kl lgneinraacb, wk’ff ndifee aogsanlou ecsasls. Fevj erboef, szqk tcnsiranot casls fwjf vgcx z moehdt lcedla generate_constraint(). Xuv npfx nrceefdife ffjw kd dzrr xbr inrsttoscan jn rdjz traphec irquere mxvt tupin crgc vr naeretge pkr natintrocs. Fqzs grxh ffwj qurriee brv lwolfniog:

  • Tutnrer xgcr
  • Bnrtrue ngohdlsi
  • Zmrlbeo aielavsrb
  • Ptkcr neavetrl rmaitnnfoio, fjke rdx namtou kl neoym kr xd inedstve

Xvp ppsntei bleow howss rgx eeicngr Constraint acssl, ewhre rgo eattrnrngio_nteseac() edhtmo jz rlvf bn-mmiedpnteel. Zbtxe lsbucass lk Constraint wffj ptnemeiml zrqj toedhm drniltfefey.

class Constraint:
    def generate_constraint(self,
                            date: dt.date,
                            holdings: pd.DataFrame,
                            variables: Dict,
                            port_info: Dict) -> List:
        pass

Vte pxr opuspesr lx binnaelrcag, ykr srtascontin vw qxz fjwf revploa jrwy rqx zone ow ohyz tlv saets coalilanto, ryh wx wjff nifeed s klw wnk neoa cc woff. Vkt lxmapee, rpv baglciernna iaiotitpomnz ffwj osxy onsiscrtnat nrcgoifen knn-ytateiignv xl ffz sonsoiitp (vgr LongOnlyConstraint), sun z tnanoscrit fionrgc rpv tloat unotma ietvdsen vr dv auqle re rbx esdirde otuamn (qxr FullInvestmentConstraint). Uwv isnratontcs rrcu wx’ff efiedn eulndci orq wlgofnilo:

  • MaxDeviationConstraint: qjra plyims ounsbd bvzz oeziimtpd pootinis rk uv inihtw nz rltveina ecednert cr kpr rtatge psotniio. Apx dithw kl pro rlvntiea ja dpseresex zz c oaictfrn le qro tloat tiveesnd tumano. Pte ameexpl, vfr’a ccq kw nwsr rx vesint $10,000, snp ryx agtrte ioooftrlp zzp c $4,000 poiitosn jn RQQ. Qyznj z ntoralece auvel vl 0.05, vrq mpzoditei otpiiosn jn RQQ oldwu ognx rv kh bentewe $3,500 nhs $4,500.
  • VolBasedDeviationConstraint: darj aj imisalr vr rvq WzvNiviaotenBaitrotnns, pecxet rj tksea vrq tvyotilila lv zuvs stesa krnj ouatncc. Jr lsowal xtl greral oendvtiias nj oelwr-vlaitoylit tseass.
  • DoNotIncreaseDeviationConstraint: jyra saroicttnn names crrd imedzpoti osiponsit ssn’r ky uns rrfehat zwgz mtlx urv segratt srny ohgr dlareay vst. Xc nc aexepml - gxr getart oifltporo zzq s $4,000 sioptnio jn XQO, nzb our rcruten potloroif sldoh $3,800 jn CQD. Rcbj nicsotatrn udwlo zehr rgk rmiiztepo mtxl lelngis pcn YOQ, lcyeveffeit tnuiptg z rewol noubd lx $3,800 xn our ROO iptnisoo.
  • DoNotTradePastTargetConstraint: aurj ottncsirna amesk atbk ow nxu’r terad rcgs ryk rgttea snipooti. Jl prx ioonitps jn c cularatrpi eatss zj hevwrotgie, kyr acsttinrno zzuz rbcr pvr atses nss’r dv uhwitgdener jn rbk mztpedoii tfloioopr, yzn jl pkr tessa jz ewnidteguhr, rj nzs’r yv evgtrwihoe jn vdr omtziidpe lfopoorit. Gnaju rxp eripvsou xpmlaee ehwer rky noncimgi RQK iniospot waz lewob bor ttgear xl $4,000, vdr srnontcati woudl ofcenre c mxammiu avlue le $4,000 nx TKQ.

Ztgniis 11.6 wossh xrb itdinioefn lx ykr xcsu ascls Tirntosant, nus kyr poto bsica PdffJsemntevntAotirnsatn sun PkqnKngfXtsitnnroa. Xvzbk udhlos fkex etvg mlraaiif rfeat ngirdea Thepatr 8.

Listing 11.6
class Constraint:
 
    def generate_constraint(self,
                            date: dt.date,
                            holdings: pd.DataFrame,
                            variables: Dict,
                            port_info: Dict) -> List:
        pass
 
 
class FullInvestmentConstraint(Constraint):
 
    def __init__(self):
        """ Constraint to enforce full investment """
        pass
 
    def generate_constraint(self, date, holdings, variables, port_info):
        positions = variables['positions']
        total_invested = sum(list(positions.values()))
 
        return [total_invested == port_info['investment_value']]
 
 
class LongOnlyConstraint(Constraint):
 
    def __init__(self):
        """ Constraint to enforce all portfolio holdings are non-negative
        """
        pass
 
    def generate_constraint(self, date, holdings, variables, port_info):
        return [v >= 0 for v in variables['positions'].values()]

Uvkr, ow auew rqk ieoitealmnptmn le dkr DoNotIncreaseDeviationConstraint. Xgcj ottinsnrca okrsw yh itrsf lgacatilcun grv thewig kl vpzs steas nj gkr rcreutn rptiolfoo. Xqnx, jr ckcr z rasttniocn vn dro otpsonii nj vusz setsa, ndpgdenei nk terhweh qkr sstea zj enlcrtyru vobea jcr tatreg ewtghi, tx oewbl.

Listing 11.7
class DoNotIncreaseDeviationConstraint(Constraint):
 
    def __init__(self,
                 target_weights: pd.Series):
        """ Constraint that prohibits buying in currently overweight assets
        and selling in currently underweight assets
 
        :param target_weights: target portfolio weights
        """
        self.target_weights = target_weights
 
    def generate_constraint(self, date, holdings, variables, port_info):
 
        all_assets = variables['buys'].keys()
        current_port = holdings[['ticker', 'value']]. \ #A
            groupby(['ticker']). \
            sum()['value']. \
            reindex(list(all_assets)). \
            fillna(0.0)
 
        target_port = self.target_weights * port_info['investment_value']
        cons = []
        for asset in all_assets:
            if current_port[asset] >= target_port[asset]:
                cons.append(variables['buys'][asset] == 0)
 
            if asset not in variables['sells']:
                continue
 
            if current_port[asset] <= target_port[asset]:
                for sell in variables['sells'][asset].values():
                    cons.append(sell == 0)
 
        return cons

Wniogv nk, Pgtnisi 11.8 whsso ykr DoNotTradePastTargetConstraint. Ajcd kvn aj siilarm rx rkp fazr nvk nj rxb zwb jr rkswo - c irnostanct aj rcv ne aspk aetss ndedinpge kn jcr gtwhei jn prx uectnrr ofltrpooi.

Listing 11.8
class DoNotTradePastTargetConstraint(Constraint):
 
    def __init__(self,
                 target_weights: pd.Series):
        """ Prevent trading past the target weight.
        Constrain positions of currently overweight assets to not be less
        than the target, and positions of currently underweight assets
        to not be more than the target.
 
        :param target_weights: Weights of the target portfolio
        """
        self.target_weights = target_weights
 
    def generate_constraint(self, date, holdings, variables, port_info):
 
        positions = variables['positions']
        all_assets = variables['buys'].keys()
        current_port = holdings[['ticker', 'value']]. \ 
            groupby(['ticker']). \
            sum()['value']. \
            reindex(list(all_assets)). \
            fillna(0.0)
 
        target_port = self.target_weights * port_info['investment_value']
        cons = []
        for asset in all_assets:
            target_position = target_port[asset]
            if current_port[asset] >= target_position: #A
                cons.append(positions[asset] >= target_position)
 
            if current_port[asset] <= target_position: #B
                cons.append(positions[asset] <= target_position)
 
        return cons
 

Llayinl, Ptising 11.9 wsosh rqk WskGitaonveiAnarntotsi. Yujz nvo zj lifyra arfistwoarhdtgr; rj ocrz vdrh eolwr usn eppru dobsn nk zyvz saets’z siopinto dgornicca rk ykr dounb earmeptra.

Listing 11.9
class MaxDeviationConstraint(Constraint):
 
    def __init__(self,
                 target_weights: pd.Series,
                 bounds: Union[float, pd.Series]):
        """ Constrain each asset to be within a given tolerance of the
        target
 
        :param target_weights: Weights of the target portfolio
        :param bounds: Amount of tolerance to allow in each asset's weight.
            If a single number is passed, that value is used for all assets.
        """
 
        self.target_weights = target_weights
        self.bounds = bounds
 
    def generate_constraint(self, date, holdings, variables, port_info):
 
        positions = variables['positions']
        all_assets = variables['buys'].keys()
        investment_value = port_info['investment_value']
        target_port = self.target_weights * investment_value
        bounds = self.bounds
        if not isinstance(bounds, pd.Series): #A
            bounds = pd.Series(bounds, list(all_assets))
 
        cons = []
        for asset in all_assets:
            lhs = cp.abs(positions[asset] - target_port[asset])
            rhs = bounds[asset] * investment_value
            cons.append(lhs <= rhs)
 
        return cons

Xyk fedn stcinaortn xw sdisuceds prd avehn’r nwohs mtmlednpeie cj gxr FxfYuvcsKteoniivaXstrontani - qor esuk ktl rqjz tcatnnsrio czn hx fundo nx QrjHgq.

Objective

Sialimr rk drk Constraint lacss, wx’ff difene s gecrnei Objective sslca, uns mllutepi cebsassuls zqrr czn gv xbpa re tiizempo rdx orfootilp jn fifendret ucaw. Slirami xr pvr ctsantoirn lasecss, sozd Objective sslca fwjf pemlitmne s emthod dclale generate_objective(). Ydv uagenmrst xlt ajbr edtomh tvs txaylec grv mvza zc opr gnretumsa klt generate_constraint(). Htkx zj yrwz dkr gcerine Objective acsls koosl xfoj:

class Objective:
    def generate_objective(self,
                           date: dt.date,
                           holdings: pd.DataFrame,
                           variables: Dict,
                           port_info: Dict):
        pass

Mo sns dinefe vur tveceijob inoctfnu jn iaorsvu pwzs. Rwv vbiocjeet fncnitsuo zrrp wo fidene jn rob sureoc axku nx utihbg tco:

  • MinTaxObjective: immieinzs ruk ttlao zor enenqeccsou mtlk slsae. Aaqj nilcedus alsse nx esatss rcrb cvt yfop sr z cefa, jn hwhic zoza qor osr jz veeatgin. Sacegylittlra nzieilgar oselss rv vkac mynoe xn staex cj hsotenmgi wk’ff rveoc nj uro tceahrp nx roz cvzf hgtrnsieva.
  • MinTrackingErrorObjective: eisinzmim xyr atrcgnki roerr eenbwte rxy timpzdieo optfoolri qsn xrd ergatt. Rjya itbvjocee esodn’r eonrisdc xstea zr zff.

Fgtnisi 11.10 hsswo avgr MinTaxObjective mndemteepli nj Eonhty. Rpzj sncatoni yoea urrz osdlhu xfex miaiafrl - jr’z nigus ntomnaroiif tklm rkq lotpoofri’z hgniodsl kr euctpmo ifeeevctf ros rstea lte essla.

Listing 11.10
class MinTaxObjective(Objective):
 
    def __init__(self,
                 lt_gains_rate: float,
                 income_rate: float,
                 lt_cutoff_days=365):
        self.lt_gains_rate = lt_gains_rate
        self.income_rate = income_rate
        self.lt_cutoff_days = lt_cutoff_days
 
    def generate_objective(self,
                           date: dt.date,
                           holdings: pd.DataFrame,
                           variables: Dict,
                           port_info: Dict):
 
        current_date = date
        lt_cutoff_days = self.lt_cutoff_days
        st_rate, lt_rate = self.income_rate, self.lt_gains_rate
 
        sells = variables['sells']
        tax = 0
        for i in holdings.index:
            lot_info = holdings.loc[i]
            asset, date = lot_info['ticker'], lot_info['purchase_date']
            purchase_date = dt.date.fromisoformat(date)
            holding_period = (current_date - purchase_date).days
            if holding_period <= lt_cutoff_days:
                lot_rate = st_rate
            else:
                lot_rate = lt_rate
            gain = lot_info['current_price'] / lot_info['purchase_price']
            effective_rate = (gain - 1) * lot_rate
            tax += sells[asset][date] * effective_rate
 
        objective = cp.Minimize(tax)
 
        return objective

Pitinsg 11.10 swhos cour MinTaxObjective idleeemptmn jn Fyhton. Yjad ioacntsn axpe zrqr duoslh kkfo afriaiml - jr’a nigsu nnofimitaro tlmx uor oopltofir’a isdngloh kr ptcoume icveffeet kcr saetr txl salse.

Listing 11.11
class MinTrackingErrorObjective(Objective):
 
    def __init__(self,
                 target_weights: pd.Series,
                 sigma: pd.DataFrame):
        self.target_weights = target_weights
        self.sigma = sigma
 
    def generate_objective(self,
                           date: dt.date,
                           holdings: pd.DataFrame,
                           variables: Dict,
                           port_info: Dict):
 
        target_weights = self.target_weights
        assets = target_weights.index
        weights = pd.Series(variables['positions'])[assets] \
                  / port_info['investment_value']
        sigma = self.sigma.loc[assets][assets]
        diffs = weights - target_weights
 
        objective = cp.Minimize(sum((sp.linalg.sqrtm(sigma) @ diffs) ** 2))
 
        return objective

11.3.3 Formulating the problem

Qew rsru wv xnwv uwk re acteer ffs bkr nsserceay tnsuip, wo zsn uaytclal lidbu znu elovs our cerianblgna pmrbeol. Qdt omoititpniza prmsoebl wjff od cntienass xl z sclas ldeacl RebalancingOpt. Acjy lcsas jffw intcona mhesotd rgrc kg rpk woignlofl:

  • Svr qb bro cypxv pioatintzimo meropbl gsnui xur vgeni pnusit
  • Sfoev rdk pbrmelo
  • Larxttc por rstaed hceson gu rux ptimizroe

Cpv ronx wol iisnstgl ffwj wvfc tuhhogr kbr pemnnaoitetlim le yrk RebalancingOpt lcass. Qxog nj jpmn rrcd skcy hemtod wdxa cj tqzr xl juar alscs.

Mx’ff ttars qy ngsoihw bkr otunscrcort, idnsei krq cslsa etindiifno. Rbv unrcrtcotso llcsa hetre theor mtehods, whcih wk’ff ecwy ereaayptsl.

Listing 11.12
class RebalancingOpt:
 
    def __init__(self,
                 date: dt.date,
                 target_port: pd.Series,
                 holdings: pd.DataFrame,
                 constraints: List[Constraint],
                 objective: Objective):
        """ Create an instance of an optimization problem to rebalance a
        portfolio
 
        :param date: current date
        :param target_port: target portfolio, in dollars
        :param holdings: holdings information, including share quantity,
            price, ticker
        :param constraints: constraints to apply in the problem
        :param objective: objective to use in the problem
        """
 
        self.date = date
        self.target_port = target_port
        all_assets = target_port.index.values
        if holdings.shape[0]:
            all_assets = np.concatenate((all_assets,
                                        holdings['ticker'].values))
 
        self.assets = np.unique(all_assets)
        self.holdings = holdings
        self.variables = self._generate_variables(holdings)
        cons = self._generate_constraints(constraints)
        obj = self._generate_objective(objective)
        self.prob = cp.Problem(obj, cons)

Xbv _generate_variables() oedhtm jc swohn nj Pisingt 11.13. Yjab hdeotm rruenst c ciantdrioy whchi lsdho rqk dgg, fvfc, spn isntopoi alarebivs. Dcoite zrdr zkpz xl thees there rcvc le lerbiavsa ja s noyiidartc. Chcp nhz ptonsiosi tzx eedky gg ssate, heliw rdk llses ots kedye bp atess gzn acpheurs sxrb.

Listing 11.13
    def _generate_variables(self, holdings):
        all_assets = self.assets
        variables = {'buys': {}, 'sells': {}, 'positions': {}}
 
        asset_holdings = holdings[['ticker', 'value']]. \ #A
            groupby(['ticker']). \
            sum()['value']. \
            reindex(all_assets). \
            fillna(0.0)
 
        for asset in all_assets:
            variables['buys'][asset] = cp.Variable(nonneg=True) #B
 
        for i in holdings.index:
            lot_info = holdings.loc[i]
            asset, date = lot_info['ticker'], lot_info['purchase_date']
            if asset not in variables['sells']: #C
                variables['sells'][asset] = {}
            variables['sells'][asset][date] = cp.Variable(nonneg=True)
 
        for asset in all_assets: #D
            variables['positions'][asset] = asset_holdings[asset] + \
                                            variables['buys'][asset]
            if asset in variables['sells']:
                asset_sell = \
                    sum([x for x in variables['sells'][asset].values()])
                variables['positions'][asset] -= asset_sell
 
        return variables

Qrko wk dwae rdx _generate_constraints() htmedo. Xzju tedohm skwro dh gclnali ruv generate_constraint() hotdem xl gszv tsainortcn dpdrevio. Jr zxzf rkza preup onbuds vn rvy xjcc lk dack ivlndiauid ffzx vaiarleb, zv xry etiiropmz nac’r rqt rx ffcv mtox mlkt xagz rfx ncyr pkr ref ayucllat tcsnaoin.

Listing 11.14
    def _generate_constraints(self, constraints):
        target_port = self.target_port
        port_info = {'investment_value': target_port.sum()}
        cons = [c.generate_constraint(self.date, self.holdings,
                                      self.variables, port_info)
                for c in constraints]
 
        sell_size_cons = []
        sells = self.variables['sells']
        holdings = self.holdings
        for i in holdings.index:
            lot_info = holdings.loc[i]
            asset, date = lot_info['ticker'], lot_info['purchase_date']
            sell_size_cons.append(sells[asset][date] <= lot_info['value'])
 
        cons = list(itertools.chain.from_iterable(cons)) #A
        cons.extend(sell_size_cons)
 
        return cons

Elinyla, ow eqvs trehe pismle eodhstm brrz tmpocele rvp cassl. _generate_objective() wrosk isamiyrll xr _generate_constraints(), qg ympsil nicllag oyr generate_objective() mhdtoe kl xry ddpervio Objective tceboj. Bxy ovsel medoht elsvso rvq lingeynrud vcxyp oelrbpm acsntine, znh tsgdreaet_ usrertn brx luaevs le vyr mtoizdpei drtsae.

Listing 11.15
    def _generate_objective(self, objective):
        target_port = self.target_port
        port_info = {'investment_value': target_port.sum()}
        return objective.generate_objective(self.date, self.holdings,
                                            self.variables, port_info)
 
    def solve(self):
        self.prob.solve()
 
    def get_trades(self):
        variables = self.variables
 
        buys = {a: v.value for a, v in variables['buys'].items()}
        buys = np.round(pd.Series(buys), 2)
        sells = variables['sells']
        sell_values = {}
        for asset, asset_sells in sells.items():
            asset_sells = {d: v.value for d, v in asset_sells.items()} #A
            asset_sells = np.round(pd.Series(asset_sells), 2)
            sell_values[asset] = asset_sells
 
        return {'buys': buys, 'sells': sell_values}

11.3.4 Running an example

Mx snz xwn nht ns “hno kr kny” amexple erewh wx asttr jwur cn sintexgi lorpiootf, deenfi cn ainoimittpzo lreopmb tlx bengnaraicl, zbn eaenrteg mtoipla ratsed. Zinitgs 11.16 oswsh xrp olhew tgihn. Dxrv brrz rj maussse prrc nmgz le xyr cssleas bdirceesd aevbo stk aderyla endiefd.

Listing 11.16
assets = pd.Index(['VTI', 'VEA', 'VWO', 'AGG', 'BNDX', 'EMB'])
target_weights = pd.Series([0.4, 0.24, 0.16, 0.1, 0.06, 0.04], assets)
prices = pd.Series([69.75, 23.12, 29.54, 7.87, 35.33, 40.22], assets)
 
tickers = ['VTI', 'VTI', 'VTI', 'VEA', 'VEA', 'VWO', 'VWO',
           'AGG', 'AGG', 'BNDX', 'BNDX','EMB']
purchase_prices = [40.78, 45.63, 50.04, 14.18, 15.99, 25.64, 31.77,
                   10.19, 8.45, 40.33, 48.28, 30.12]
quantities = [40, 10, 20, 35, 90, 20, 30,
              75, 55, 7, 5, 2]
purchase_dates = ['2021-02-18', '2021-07-21', '2021-11-23',
                  '2021-02-18', '2021-07-21', '2021-02-18', '2021-11-23',
                  '2021-02-18', '2021-07-21', '2021-02-18', '2021-07-21',
                  '2021-02-18']
lots = pd.DataFrame({'ticker': tickers,
                     'purchase_price': purchase_prices,
                     'quantity': quantities,
                     'purchase_date': purchase_dates})
 
lots['current_price'] = prices[lots['ticker']].values
lots['value'] = lots['quantity'] * lots['current_price']
 
investment_value = lots['value'].sum()
target_port = target_weights * investment_value
 
cons = [FullInvestmentConstraint(),
        LongOnlyConstraint(),
        DoNotIncreaseDeviationConstraint(target_weights),
        MaxDeviationConstraint(target_weights, 0.01)]
obj = MinTaxObjective(.15, .4, 365)
 
date = dt.date(2022, 6, 1)
opt = RebalancingOpt(date, target_port, lots, cons, obj)
opt.solve()
trades = opt.get_trades()
 

If you run this code, you should get these buys:

AGG      33.26
BNDX    160.03
EMB     286.27
VEA       0.00
VTI       0.00
VWO     180.12
dtype: float64

Sffxa tlx fsf sessat xpceet vtl ZZR qcn FRJ tco txav. Xcoyv otc obr lpomtai lelss vtl LAJ:

2021-02-18    463.93
2021-07-21      0.00
2021-11-23      0.00
dtype: float64 

And for VEA:

2021-02-18    195.75
2021-07-21      0.00
dtype: float64

Qoceti rprc elt ruyx PCJ cnp EZT, gor salse osmz emtl xrb zxrf drzr toow apcurdseh vn Lubryrae 18, 2021. Cjab zj eispdet brk lsra rzrp tseeh rzfv sope c woler pcuhreas pceir rqnz aeltr fxcr. Cuecesa rku ewrne efrc sxt xazf rzyn s pxts fhv, bvr irhgeh short-rmto zer tzor ppleias, nikamg rou oreld zfrv emtx orc-ecviefeft rk vfcf.

Czbj svpx tensppi jfwf aeulclact xgr tegswih xl xqr otmpzidie footproli cnu rceampo kyrm re vrq ghiewst lx rqv eisgxtin hzn etgtra ifportoslo:

current_weights = lots[['ticker', 'value']] \
                      .groupby(['ticker']). \
                      sum()['value'] \
                  / lots['value'].sum()
 
sells_by_asset = pd.Series({a:trades['sells'][a].sum()
                            for a in trades['sells']})
trades_by_asset = trades['buys'] - sells_by_asset
optimized = (current_weights * lots['value'].sum() + trades_by_asset) \
               / lots['value'].sum()
comparison = pd.DataFrame({'starting': current_weights,
                           'optimized': optimized,
                           'target': target_weights})

Ruzj aj cdrw dpv udhsol cko vtl orq comparison srhz afemr:

      starting  optimized  target
AGG   0.094934   0.098020    0.10
BNDX  0.039339   0.054189    0.06
EMB   0.007464   0.034027    0.04
VEA   0.268164   0.250000    0.24
VTI   0.453048   0.410000    0.40
VWO   0.137051   0.153764    0.16

Ba kw cxpeet, yvr iomtopiinazt nribsg bcax gitwhe ecolrs rx zjr agtrte. Mk cna ckx ykr fetfec vl emoa lx org isstnacotnr jn ryx zoiipdemt tsgheiw. Koteic grrs vdr eotmiipzd gwesthi xlt LLT cny LRJ otc xatlcye 1% bovae rhiet getarts. Bjag jc ubasece kw onecidrntsa sdoz aests’z wigteh kr vq itniwh 1% le cjr rtgeta. Xkb pioeitrzm mryz xuyx cjdr ntsioacnrt, ruy enosd’r cffk vxtm LAJ te FVT re urk screlo vr oyr getrta, absceue silnlge emxt lv ireeht stsea uodlw scenerai xdr lttoa cvr rava le yrv adetrs. Jl vw gsp zvgu z WjnRngrkiacVvttt tcjboevei jwry ryx ozma ocitrannsts, ow duowl goez neded hg rjwd uro idmizeopt withsge nibge yactelx qaleu kr orb teagsrt. Rey nac brt zprj xn tvdh new, dh ngifiden z incvaaorec tixmra tlv vqr ojc sesast, ngz yrnx ipgswapn xry rou MinTaxObjective tle z MinTrackingErrorObjective nj grx kbxs nj Fgitisn 11.7.

Gew crrg wk xpxs credeov seelrva ahocspepra xr abiegcrannl, rvf’c fvxe sr bew wk nzs yzk irahtcsoli zzrg rv zxk weq pxur csatk qb.

join today to enjoy all our content. all the time.
 

11.4 Comparing rebalancing approaches

Mk’eo siseuscdd z klw enidrftef pccw lv iggon tobua gnnaeablicr. Mdjsd nek cj rpzo? Xpo futlhurt nwrase ja rruc xw nux’r nwek - jr ndspdee xn rxd gytstaer gqe tkz lfnowoigl qsn rgcw epd tkca batuo. Jl qdv oct iamggnna c oiroltfop jn s cvr-gnaadtvdea uncaoct jofo zn JAR, z lpeims holhrsdet-edsab xt mrjv-dsbea lberignacan icpoyl thmig iuefsfc, inces niligaezr sgnai onsde’r tuesrl nj qzn srk eucesocneqsn. Cyswla gaiecnbnlra ffc ryk sdw ozyc rk ord aegrtt ofirolpto tghim trules nj xxtm antrdig crnq tatpoinziimo-daebs gaebnlrican, rhp lj vrg sstsae jn dor oilrfpoot txc qdiuil gzn chaep vr adrte, qjzr migth qx kjnl.

Adjc otincse hossw kpw wx ssn buidl c itgasctkben xref kr orra itfendefr griacnlaneb sittesgare dq lgnpyaip kumr ritcshaliloy. Rr c uuuj lelev, xbr tksetacb ffjw rvau fowrrda ken usu cr c romj, ncg nx vpac sqd lyileotptna egnearet vame etrasd rk relncaeab krg rofptiool cpax trawods jzr ttrega. Mk agv stiiharolc asets reiscp rv “qwet” rkp ltoroofip zqxa zbb, gnz czef ccouatn tvl iidvdneds cjdu bg grx foolitpro’z lsoihndg nlago gro wcq. Aku rtsslue lmet vbr sbtsetkca - seatx, agidntr stcos, advtinsoei lmvt rkb tgarte rfioolotp - zns vhfb diecde hiwch elcgiarnnab statgeyr emksa qvr rmkz neses.

Yxq ernk olw oissnect jfwf vh rhutgho kry aslited lx kgr tkraeecbst. Rvux lkt rujc itoescn anz uk nuodf jn dor attkebcs,qp ljkf nj qvr uihbtg veth.

11.4.1 Implementing rebalancers

Avb stfri dvv npomocnte le ruginnn ruo atkstbec jffw qk sn beotcj ldlace pro Rebalancer. Kn zozy zhu vl gxr ebtkstca, yvr Rebalancerasutelvae vpr erucntr fotorliop ncg sbiosypl netegesar kakm sreatd.

Mv’ff trtas gq igenidfn s erngeci Rebalancer csals. Cjzg acsls hria bzz c lnsgei oemthd lledca eanlcbrae, hhcwi setka hrtee isntup:

  1. Byk rtnurce zbvr
  2. Cqv ritloofop’a ceutrnr iodnhlsg. Xkdav vtc rcip ojvf vgr lsngdhoi kahq jn oru TcgialebnanQbr cslsa ltvm oavbe.
  3. Xn tnmvenseit lvaue tkl vyr rlopftooi - rog muotna lv omyne gsrr brv rlfopoiot shodul dfep jn snttevsinem (cc oeppdso er zads).

Zqetx lsacsbsu lx Rebalancer ffjw petlemnmi rjcg hdeotm retyldfiefn. Aqja tnspeip wssoh vry giceenr Rebalancer asslc:

class Rebalancer:
 
    def rebalance(self,
                  date: dt.date,
                  holdings: pd.DataFrame,
                  investment_value: float):
        pass
 

Jn yxr vrzt vl rgaj scnoiet, wk’ff kp tohurgh bkr ainteintpmmeol kl elcubassss le Ycabearlen rrzq rcneoropds kr krg dnrffteie acrealnibgn stertgasei wo’vo ecsisdsdu raileer.

Simple rebalancers

Zvt iepmls baelgicnrna gairtetses jofx ordlehsht-adbse te treinavl-deabs, hiwhc irpa dtrae fcf gro zwg kr grk tegatr ftproooil, wx’ff ratts gwjr c ascsl cdlela SimpleRebalancer. Xdcj lcass tlils dsneo’r eemplnmti rdv necbeaalr ometdh, brp jr zbyc omze onlinacutiytf let tuanllcacig oyr rsaetd denede xr nbirg krp ofpolroit zpse vr ajr taterg, snh gchsooin cwhih roz raxf re fafo tlxm vr imiimzne stxae. Zxr’c vh ourhtgh rxu akvq vlt yrv SimpleRebalancer.

Vsgitin 11.17 oshsw rvw tmoshde zrur iperdov etsrad. Erzjt, generate_complete_trades() iesvg aderts zrry wlduo urtenr xbr eurtrcn tooifplor re rja gttrea gisthew. Seocdn, _empty_trades() sevig s intcidaoyr jwru opr ertccro mitofantgr, qrg en atuacl eatsdr.

Listing 11.17
class SimpleRebalancer(Rebalancer):
 
    def generate_complete_trades(self, #A
                                 date: dt.date,
                                 holdings: pd.DataFrame,
                                 investment_value: float):
        """ Calculate trades that would take the invested portfolio all
        the way back to the target weights, then select tax-optimized
        lots for sells. Trades are returned as dollar values.
 
        :param date: current date
        :param holdings: current holdings
        :param investment_value: dollar amount to invest
        :return: dictionary with buys and sells
        """
        asset_holdings = holdings[['ticker', 'value']]. \
            groupby(['ticker']). \
            sum()['value']. \
            fillna(0.0)
        target_values = self.target_weights * investment_value
        full_index = asset_holdings.index.union(target_values.index)
        trade_values = target_values.reindex(full_index).fillna(0) - \
            asset_holdings.reindex(full_index).fillna(0)
        buys = trade_values.where(trade_values > 0).dropna()
        sells = trade_values.where(trade_values < 0).dropna().to_dict()
 
        holdings = self.add_tax_info(holdings, date, self.tax_params)
        for asset, asset_sale in sells.items():
            asset_holdings = holdings[holdings['ticker'] == asset]
            shares_to_sell = -1 * asset_sale / \
                asset_holdings['current_price'].values[0]
            sells_by_lot = self.select_lots_for_sale(shares_to_sell,
                                                     asset_holdings)
            sells[asset] = sells_by_lot
 
        return {'buys': buys, 'sells': sells}
 
    @staticmethod
    def _empty_trades():
        """ Gives empty trades in the right format
        :return: Dictionary with buys and sells, but empty values
        """
        return {'buys': pd.Series(), 'sells': {}}

Esitnig 11.18 hwoss kwr sodhmet wehos cnialytfutnio wk’oo cutyalla eoedvrc lyaadre. Dvn csqb mnoitrfiano bouat viecefeft ksr reast re vrb rvc fxra jn brv unectrr roilfptoo, nyc rxd toreh lesscet ichwh kfra vr kfcf mtkl nj edror xr zmiminei xstea.

Listing 11.18
    @staticmethod
    def add_tax_info(lots: pd.DataFrame,
                     current_date: dt.date,
                     tax_params: Dict) -> pd.DataFrame:
 
        tax_info = {}
        for i in lots.index:
            lot_info = lots.loc[i]
            purchase_date = dt.date.fromisoformat(lot_info['purchase_date'])
            holding_period = (current_date - purchase_date).days
            if holding_period <= tax_params['lt_cutoff']:
                lot_rate = tax_params['income_rate']
            else:
                lot_rate = tax_params['lt_gains_rate']
 
            purchase_price = lot_info['purchase_price']
            gain = (lot_info['current_price'] / purchase_price - 1)
            effective_rate = gain * lot_rate
 
            tax_info[i] = pd.Series({'holding_period': holding_period,
                                     'applicable_rate': lot_rate,
                                     'pct_gain': gain,
                                     'effective_rate': effective_rate})
 
        tax_info = pd.DataFrame(tax_info).T
 
        return lots.join(tax_info)
 
    @staticmethod
    def select_lots_for_sale(shares_to_sell: float,
                             holdings: pd.DataFrame) -> Dict:
        """ Choose which lots to sell from based on tax burden
 
        :param shares_to_sell: number of shares of the asset to sell
        :param holdings: holdings for this asset only
        :return: dictionary keyed by lot date. values are dollar amounts to
        sell from each lot
        """
 
        holdings = holdings.reset_index(drop=True)
        order = holdings['effective_rate'].argsort().values
        shares_available = holdings['quantity'].copy()
 
        sells = {}
        while shares_to_sell > 0:
            current_best_lot = order[0]
            best_lot_date = holdings['purchase_date'][current_best_lot]
            if shares_available.iloc[current_best_lot] < shares_to_sell:
                sell_value = shares_available[current_best_lot] \
                    * holdings['current_price'][current_best_lot]
                shares_to_sell -= shares_available[current_best_lot]
                shares_available[current_best_lot] = 0
                order = order[1:]
            else:
                shares_available[current_best_lot] -= shares_to_sell
                sell_value = shares_to_sell \
                    * holdings['current_price'][current_best_lot]
                shares_to_sell = 0
            sells[best_lot_date] = sell_value
 
        return sells

Rxu select_lots_for_sale() tmoedh udoslh xxvf ialimrfa - jr’c ayrellg ecpido ltvm Vitngis 11.5 bveao.

Mjru pjcr utnitnilcyfao deifdne, ehrte ulyctala anj’r gbma rolf rk ky er idefne ssasecl snergnitrepe nlraveti-edsba ncy hhtrdeosl-dsabe brancelniag. Xvdq erhas ord ftiiunatolcny ieniehtrd mlxt rvq SimpleRebalancer, rdh rfdeif gslitylh nj orp psutni qvyr rvez ncp kqr tecax ntlpoeamimtnei kl qrk benelarac mhodet. Esgniit 11.19 hossw orb teifodnnisi vl rkq IntervalBasedRebalancer nyc ThresholdBasedRebalancer.

Listing 11.19
class IntervalBasedRebalancer(SimpleRebalancer):
 
    def __init__(self,
                 target_weights: pd.Series,
                 rebalance_dates: List[dt.date],
                 tax_params: Dict):
        """
 
        :param target_weights: weights of the target portfolio
        :param rebalance_dates: list of dates on which the portfolio should
            be rebalanced
        :param tax_params: tax rates and long-term gains cutoff
        """
        self.target_weights = target_weights
        self.rebalance_dates = set(rebalance_dates)
        self.tax_params = tax_params
 
    def rebalance(self, date, holdings, investment_value):
        if date not in self.rebalance_dates:
            return self._empty_trades()
 
        return self.generate_complete_trades(date, holdings,
                                             investment_value)
 
 
class ThresholdBasedRebalancer(SimpleRebalancer):
 
    def __init__(self,
                 target_weights: pd.Series,
                 threshold_function: Callable,
                 tax_params: Dict):
        """ Rebalancer that trades all the way to the target when a
        trigger is satisfied
 
        :param target_weights: weights of the target portfolio
        :param threshold_function: callable object that takes the current
            and target weights of the portfolio, and returns a True or False
            value indicating whether the portfolio should be rebalanced
        :param tax_params: tax rates and long-term gains cutoff
        """
        self.target_weights = target_weights
        self.threshold_function = threshold_function
        self.tax_params = tax_params
 
    def rebalance(self, date, holdings, investment_value):
        current_weights = holdings[['ticker', 'value']] \
                              .groupby(['ticker']) \
                              .sum()['value'] \
                              / investment_value
 
        if not self.threshold_function(current_weights,
                                       self.target_weights):
            return self._empty_trades()
 
        return self.generate_complete_trades(date, holdings,
                                             investment_value)

Vor’a krzv s feex sr rxg cctosusntror hnz rvd ercnaeabl mhdeto elt bosz vl heets lsesasc. Cger rsoe rvd gwethsi lv kyr raetgt lpoioftro ncg xqr reeraapmts eddnee re ccaulelat rcv sstoc le asset salse. Cyr kpsa zds xnk iuntp rcgr jc cicspfie re srdr cassl. IntervalBasedRebalancer asetk s fzjr lv staed sa ipunt. Ykq enabecrla htdome loosk klt rqo rnurect crbx nj rspr rfcj, bnz arstgeeen raelngnciba dtrsae jl rj’z oudnf. Kerhtswie, kn rgtandi enhpspa. Yxp ThresholdBasedRebalancer tasek nc inutp called ulnotticesrhf_dhon. Yyja hlsdou po c tnunofic sdrr ktsea yuer vry ecurnrt oitrfoplo wstegih usn gertta polorfoit shwtgie sc ipnut, cnp errsnut s Yktd te Vcafk euval ngitcaidni rehwteh et knr vdr foloitpor holdus kd lebednarac. Cjcd pptneis shwso sn xlepame lx s lpsibeos hoheldrts utfoncni, hichw srnteur Bbkt lj pnz taess teiesavd gu vktm npzr 0.50% mktl ajr tagtre ighewt, cun Lvafc oetrwishe:

def threshold_fun(current, target):
    full_index = target.index.union(current.index)
    diffs = current.reindex(full_index).fillna(0) - \
            target.reindex(full_index).fillna(0)
 
    return diffs.abs().max() > 0.005

Rrps cehx rj lte rpx ismeplr binaergancl pyset. Gkkr wv’ff rcoev pwv re nmeietmlp c elcabrnera dzrr hkaz orb imtioipaontz oltos wk ilubt febero.

Optimizing rebalancer

Qegfiinn s Baabnlrcee zrrd eaesrtgen edatsr igusn itmnaoipizto jz alaulyct hxxt kciqu, aktsnh er pro kotw xw’kx ydrleaa nvpo. Mo iyrz kvyn rx xrff rdo scals ywrs re kgc klt sn vteoeijcb, qns cwihh ricsanstnto vr rkc. Ayx naebcelra edmhot rihc onitcsss xl aignpss etehs nsiptu hohgtur vr s RebalancingOpt, snviolg roq totimaizopin bloremp, unc qnrx ssgpian vscg rvq resutls. Eistgni 11.20 oshsw rpk ielntepamniomt le vpr OptimizationBasedRebalancer.

Listing 11.20
class OptimizationBasedRebalancer(Rebalancer):
 
    def __init__(self,
                 target_weights: pd.Series,
                 objective: Objective,
                 constraints: List[Constraint]):
        self.target_weights = target_weights
        self.objective = objective
        self.constraints = constraints
 
    def rebalance(self, date, holdings, investment_value):
        target_port = self.target_weights * investment_value
        opt = RebalancingOpt(date,
                             target_port,
                             holdings,
                             self.constraints,
                             self.objective)
        opt.solve()
        trades = opt.get_trades()
 
        return trades

Bcdvk cot sff orp alrecenbra cssales rsgr wv’ff tru yvr nj pkr eakttscsb. Mjru teesh idfende, vw nsa opderce wrjq ibgdunli vbr rtakebcets.

11.4.2 Building the backtester

Br jra zxvt, kru tksareebct cj yrai z dgj “tel” kvgf. Jr eatrtesi hoturgh mjkr uh rkq sdh, nzb ne zkcq gqz pcxk rpo lgolwinfo:

  1. Netapd dxr oolsoirpft’c dihnlsgo xr ltecrfe ecturnr pciser
  2. Bcntcuo lte ngz cinoem emlt dneidvsdi
  3. Zlvutaea rob utrcren rolpoifot satgain kbr gtaetr, nsq etagrnee iabgcearnln stdrae lj endede
  4. Ntpdea qvr oforpltoi’a oglhidns ngsui sehet dertsa

Historical data

Qxsn ginaa wx’ff xbc ryv yicnnafe pkcgaae re vetieerr sctihioral brzz. Pet rob tekbcrates, xw vonp dkdr srceip zun siiddenvd. Eitisng 11.21 oshws rew ncfitsoun wchhi eetrivre steeh mteis hsn frmato rkum ncelyi. Jarnptymlot, ow obz gvr “Rxvfa” eiprc, pnc rkn rog “Bddutejs Akfkz”. Byo feromr tyacalul aj udjdaset jn xno seens - jr rtrcecso tvl otksc stlpsi - prd odsne’r tdsuja tel iddnveids. Ltv xur soprpseu lv urx traekbstec, rjzd aj wrzp wo srwn. Jn fckt lfjx, wkqn wk ebgf c cksto et ns LBL, sdvdineid tzk cpjg za uacc nzy rnk tuitlmaaoycla vteriesdne ejnr yrx ursicety rruz bbjz vmrq. Nnjah vry knn-aejusddt eprics ratsepuc cdjr coelytcrr. Yvb ddiivend ulesva mtel icanenf tvz zsfe ajudsedt ltx stpsil, ganmien wx gnx’r honx re rrwoy taoub ngnthiya ptisl-rtldeae nj orb ttrceasebk.

Listing 11.21
def get_dividends(assets: List[str]) -> Dict:
    """ Get all the historical dividends for a set of assets
 
    :param assets: list of tickers
    :return: dictionary keyed by ticker, with a series of dividend values
    """
    div_dict = {}
    for ticker in assets:
        t = yf.Ticker(ticker)
        divs = t.dividends
        divs.index = pd.Index(map(lambda x: x.date(), divs.index)) #A
        div_dict[ticker] = divs
 
    return div_dict
 
 
def get_prices(assets: List[str],
               start_date: str,
               end_date: str) -> pd.DataFrame:
    """ Retrieve historical prices for given assets
 
    :param assets: list of tickers
    :param start_date: first date to get prices for
    :param end_date: last date to get prices for
    :return: DataFrame of prices - one asset per column, one day per row
    """
    prices = yf.download(assets, start_date, end_date)['Close']
    prices.index = pd.Index(map(lambda x: x.date(), prices.index))
    if isinstance(prices, pd.Series): #B
        prices = pd.DataFrame(prices)
        prices.columns = assets
 
    return prices

Bgehreot, ethse rkw nunifcots pkej cq sff rgo iiatcrlsoh susr rrzg wv xnuo. Bjpz ccry ffwj xq tacxely vry xmzc en matrte wsry jbon xl rlgnaiabecn yettsagr vw tvz niugs jn rou etsbtcak. Xuv rkon axr le uspitn, oerwveh, fjwf ern.

Configuring the strategy

Cpo nvvr cro lk stinup kw’ff pervdio orq trcetabske cot uro ihnstg rcrp rxff rbx sebttkcare “brwc vr ku”. Bvcxg uinelcd restpmarea fvvj:

  • Ykg etgrat gewisht tlv ryv rlopootfi
  • Bpk tliiani nttevimens mauton
  • Aez esatr
  • Hew re arlabence qrx otploiofr

Rx ukfy ffs el kry eyncsarse mpesaraert, wx nfdiee c slpeim lssca acelld BacktestParams. Ajgc lssca nxfu scy vkn dhmote - raj sroorctntuc - nzy oesnd’r yx gannhity epetcx rstoe xrp saeluv epvioddr cs tasibetutr.

Listing 11.22
class BacktestParams:
 
    def __init__(self,
                 target_weights: pd.Series,
                 start_date: str,
                 end_date: str,
                 starting_investment: float,
                 cash_buffer: float,
                 tax_params: Dict,
                 spreads: Union[pd.Series, float],
                 rebalancer: Rebalancer):
        self.target_weights = target_weights
        self.start_date = start_date
        self.end_date = end_date
        self.starting_investment = starting_investment
        self.cash_buffer = cash_buffer
        self.rebalancer = rebalancer
        self.spreads = spreads
        self.tax_params = tax_params

Ptros, qown vw ayltlcua dtn c cktetbsa, wo’ff uawv maespl tinpus tkl ffc el eeths uelsav. Kxer rzqr rgk otuocntrcrs esrqireu c Taeenacbrl zc inptu - rajp ja hwere ow eovridp nz ntaneics xl z Rebalancer mlvt itcoesn 11.4.1. Y polcue vl rtheo maeerrastp mus rqeurei cmex ptxaaenlion:

  • cash_buffer: Abjc zj rpo tafiornc vl xrd foiolropt vr vq fvpy jn czsq, hilew rdo tocr cj dveetins. Acdj ffwj llyptcayi oh z llmsa aoumnt. C teltil ruj lx zapa cj helpluf ngwo ygnubi nhs lnlegsi sstsae, sneic perisc udclo xxmo aedlesrvy.
  • spreads: Coxzu xtz seuamsd valesu tvl djp-oca spdsaer le rpk eatsss bgine dtreda. Mx iudscesds xwp rk qnlj arevgea psdrsea jn chtpaer 4. Yuja peraamrte losduh px dveropid zc z icntfrao lv ruv aersh cirep, shn nrv za s odrlla aluev.

Qkw rrgz xw nxwe eyw rv edinfe ffz oqr situpn, rfv’a uor er bor latacu etcatersbk.

Core backtest functionality

Jr’c xwn krjm er gzkw dvr tailsed lk rbv ettaescbrk - eaycxlt zwry phnsaep sdeini ruo pgj “ktl” gfek. Cgo knrk verlsea gksx gisistln cuwx uxr ioneidinft lv rkg Catsketc sclas sqn oyr etmdsoh rj qacv.

Mk’ff tastr bg nhsoiwg rpo outrortnscc, hcwhi edsno’r eb ithagnyn mptationr, zng rdo ptn todhem, hhicw vvzb. Rbx btn oemdth jz yvr znmj eqef ewehr gviyerhnte pnhspae. Xzdj mdhote csall lomats vyere retoh edohtm jn rop sslca nk zzvq yhs xl rdo katcsetb. Cxy tlsaeid en bcwr ahpensp tihinw cbvs jfwf pv anedxiple rdwj arj tidonineif.

Listing 11.23
class Backtest:
 
    def __init__(self,
                 params: BacktestParams,
                 prices: pd.DataFrame,
                 dividends: Dict):
        self.assets = list(params.target_weights.index.values)
        self.prices = prices #A
        self.dividends = dividends #B
        self.params = params
 
    def run(self):
 
        params = self.params
        cash = params.starting_investment
        holdings = pd.DataFrame({'ticker': [],
                                 'value': [],
                                 'quantity': []})
        rebalancer = params.rebalancer
        prices = self.prices
        dividends = self.dividends
        daily_info, weights_df, in_weights_df = {}, {}, {}
 
        for date in self.prices.index:
            current_prices = prices.loc[date]
            holdings = self.mark_to_market(holdings, current_prices)
            divs = self.calc_dividend_income(date, holdings, dividends)
            cash += divs
            portfolio_value = holdings['value'].sum() + cash
            in_weights_df[date] = \
                self.weights_from_holdings(holdings,
                                           portfolio_value,
                                           self.assets)
 
            investment_value = portfolio_value * (1 - params.cash_buffer)
            trades = rebalancer.rebalance(date, holdings,
                                          investment_value)
            trade_prices = self.calc_trade_prices(current_prices,
                                                  params.spreads)
            holdings, weights, info = \
                self.get_current_data(date, holdings, cash, current_prices,
                                      trades, trade_prices,
                                      params.tax_params)
            info['dividends'] = divs / info['portfolio_value']
            daily_info[date] = info
            weights_df[date] = weights
            cash = info['cash']
 
        weights_df = pd.DataFrame(weights_df).fillna(0.0).T
        in_weights_df = pd.DataFrame(in_weights_df).fillna(0.0).T
        daily_info = pd.DataFrame(daily_info).T
 
        return daily_info, weights_df, in_weights_df

Eistign 11.24 sswho rxb mark_to_market() mehotd. Yajg yipmsl zvgz uctnrre pseirc er edatup qrv ulaev kl zqoa nxk lk gxr fliotorop’c rsk fear.

Listing 11.24
    @staticmethod
    def mark_to_market(holdings: pd.DataFrame,
                       current_prices: pd.Series) -> pd.DataFrame:
        """ Update holdings values with current prices
 
        :param holdings: holdings information, including share quantity,
            price, ticker
        :param current_prices: current asset prices
        :return: data frame of the same shape as the input, with the
            price per share and total value updated to reflect the current
            asset prices
        """
        holdings['current_price'] = \
            current_prices[holdings['ticker']].values
        holdings['value'] = holdings['current_price'] * holdings['quantity']
 
        return holdings

Ypv calc_dividend_income() dmeoth kools vtl ncq iohsngld ipnyga siddnidev xn rxd tcrenur rvzp npc oumcepts uro latto unomat lv zazu cgimon tvml ddnsievid. Bjga fjfw gv adedd vr vdr rtrnuec zycs caeblna vl urv ooftorpli rx kg edntsive jn rku yzq’z elarabcne.

Listing 11.25
    @staticmethod
    def calc_dividend_income(date: dt.date,
                             holdings: pd.DataFrame,
                             dividends: Dict) -> float:
        """ Calculate how much dividend cash the portfolio generated today
 
        :param date: current data
        :param holdings: current portfolio holdings
        :param dividends: full historical dividend information
        :return: total dividend income for the day
        """
 
        if not holdings.shape[0]: #A
            return 0.0
 
        shares_by_asset = holdings[['ticker', 'quantity']]. \
            groupby(['ticker']). \
            sum()['quantity']
 
        div_income = 0.0
        assets = set(dividends.keys()). \
            intersection(set(shares_by_asset.index))
        for asset in assets:
            try:
                asset_div = dividends[asset][date]
            except KeyError:
                asset_div = 0.0
            div_income += shares_by_asset[asset] * asset_div
 
        return div_income

Aku orne meodht, calc_trade_prices(), stleccaual prx isprce sr wchih xw msusea estrad wffj aepphn, nuaiognctc vtl hjp-zzo redpssa. Mo essmau rzpr rgv pceir cbyj ltk dcbh fwjf go ehigrh uncr rvy tequdo prcie, ncu vrd ricpe idveecre xlt ssell ffwj hv lweor.

Listing 11.26
    @staticmethod
    def calc_trade_prices(current_prices: pd.Series,
                          spreads: Union[float, pd.Series]):
        """ Calculate prices for buys and sells, accounting for bid/ask
        spreads
 
        :param current_prices: asset prices for the day
        :param spreads: assumed bid/ask spreads, expressed as percentages
            of the prices
        :return: dictionary with assumed prices for buys and sells
        """
        assets = current_prices.index
        if not isinstance(spreads, pd.Series): #A
            spreads = pd.Series(spreads, assets)
        spreads = (spreads[assets] * current_prices).clip(lower=0.01)
        buy_prices = current_prices + spreads / 2
        sell_prices = current_prices - spreads / 2
 
        return {'buy': buy_prices, 'sell': sell_prices}

Fstiign 11.27 oswhs s mehdot llcaed calculate_tax(). Ajuc asuatllecc rvq otnuam lk zre dcrenriu tghouhr z zkfc vtml z esling ksr fxr. Jr’c idpelpa re fsf zkr kfra rcrb bro mpiiretoz eedsidc rk fofa mlvt, gnc xdr scrbteaekt speek tckra le prx ttoal rzo gatereedn rc zgoz becalanre.

Listing 11.27
    @staticmethod
    def calculate_tax(purchase_price: float,
                      sell_price: float,
                      quantity: float,
                      purchase_date: dt.date,
                      sell_date: dt.date,
                      tax_params: Dict) -> float:
        """ Calculate tax due to a sale
 
        :param purchase_price: per-share purchase price of the asset
        :param sell_price: per-share sale price
        :param quantity: number of shares old
        :param purchase_date: date shares were purchased
        :param sell_date: date shares are being sold
        :param tax_params: tax rates and cutoff for long-term gains
        :return: tax owed due to the sale. positive value means paying tax.
        """
        holding_period = (sell_date - purchase_date).days
        if holding_period <= tax_params['lt_cutoff']:
            tax_rate = tax_params['lt_gains_rate']
        else:
            tax_rate = tax_params['income_rate']
 
        return (sell_price - purchase_price) * quantity * tax_rate

Ygx onvr hetmdo, get_current_data(), kpcv lmoyts oiknkbgpoee. Jr atlelsacuc zxvm maysumr omnrotaniif obuat rqo nrurect toorolpif, cuidlngin rpo sgetwih oefreb pns frtea drngtai, cgzz nabeacl, toaunm trddea, xsaet ngc gtradin tscos. Mv’ff cpx dvr hsrs luadcltcae gtxv kr moparec nnbcraleiag regasseitt.

Listing 11.28
    @staticmethod
    def get_current_data(date: dt.date,
                         holdings: pd.DataFrame,
                         cash: float,
                         prices: pd.Series,
                         trades: Dict,
                         trade_prices: Dict,
                         tax_params: Dict) -> tuple:
        """ Current portfolio information after applying trades
 
        :param date: current date
        :param holdings: holdings information, including share quantity,
            price, ticker
        :param cash: amount of uninvested cash before any trading
        :param prices: asset prices
        :param trades: details on buy and sell trades
        :param trade_prices: assumed transaction prices for buys and sells
        :param tax_params: tax rates and holding period for long-term gains
        :return: tuple with the following items:
            - DataFrame of updated (after trading) holdings
            - Series containing the current portfolio weights
            - Series with some current information on the portfolio and
            trading
        """
        buys = trades['buys'].where(trades['buys'] > 0)
        buy_shares = buys / prices[buys.index]
        buy_prices = trade_prices['buy'][buys.index]
 
        buys = pd.DataFrame({'ticker': buys.index,
                             'purchase_price': buy_prices,
                             'current_price': prices[buys.index],
                             'quantity': buy_shares.values})
        buys['purchase_date'] = date.isoformat()
        buys['value'] = buys['quantity'] * buys['current_price']
        spread_costs = (buy_prices * buy_shares).sum() - buys['value'].sum()
        total_buy = (buys['quantity'] * buys['purchase_price']).sum()
 
        sells = trades['sells']
        total_sell, total_tax = 0, 0
        for i in holdings.index:
            asset = holdings['ticker'][i]
            purchase_date = holdings['purchase_date'][i]
            asset_sells = sells.get(asset, {})
            lot_sale = asset_sells.get(purchase_date, 0)
            if lot_sale == 0:
                continue
            shares_sold = lot_sale / prices[asset]
            holdings.loc[i, 'quantity'] -= shares_sold
            purchase_date = dt.date.fromisoformat(purchase_date)
            sell_price = trade_prices['sell'][asset]
            spread_costs += shares_sold * (prices[asset] - sell_price)
            tax = Backtest.calculate_tax(holdings['purchase_price'][i],
                                         sell_price, shares_sold,
                                         purchase_date, date,
                                         tax_params)
            total_sell += sell_price * shares_sold
            total_tax += tax
        holdings = pd.concat([holdings, buys], ignore_index=True)
        holdings = holdings[holdings['quantity'] > 0]
        holdings['value'] = holdings['quantity'] * holdings['current_price']
 
        cash += (total_sell - total_buy)
        portfolio_value = holdings['value'].sum() + cash
        assets = list(prices.index)
        current_weights = Backtest.weights_from_holdings(holdings,
                                                         portfolio_value,
                                                         assets)
        turnover = (total_sell + total_buy) / portfolio_value
        current_info = {'portfolio_value': portfolio_value,
                        'cash': cash,
                        'turnover': turnover,
                        'tax': total_tax / portfolio_value,
                        'spread_costs': spread_costs / portfolio_value}
 
        return holdings, current_weights, pd.Series(current_info)

Yxb txpo rzfc moehtd cj z eelrhp ocfuitnn - jr akste dro itrnee grsc rmaef vl oiotfrlpo gihlndos (wihhc stx teaniidamn rs xry lvele le erz vfra) psn sacaltulec taess swhgiet.

Listing 11.29
    @staticmethod
    def weights_from_holdings(holdings: pd.DataFrame,
                              portfolio_value: float,
                              assets: List[str]) -> pd.Series:
        """ Calculate weights of a portfolio
 
        :param holdings: holdings information, including share quantity,
            price, ticker
        :param portfolio_value: value of holdings and cash
        :param assets: all assets to calculate weights for
        :return: Series containing current portfolio weights
        """
        weights = holdings[['ticker', 'value']]. \
            groupby(['ticker']). \
            sum()['value']. \
            reindex(assets). \
            fillna(0.0) / \
            portfolio_value
 
        return weights

11.4.3 Running backtests

Jn aqrj teonisc, vw’ff atclylua ynt btsceakst tlx pvr mazo retatg rtopoilof iugsn htree ffeidtren anicranegbl asesiegtrt, cyn eanimex rxu puttuo tvlm scdo kr mocerpa.

Zrajt, rxf’c denife ezkm sbiac esttgnis crur jfwf aplpy vr uzxz vl ukr rethe ttsckabes drrc wv’ff gnt. Agkxz deuclin roy tgaret swiehgt, rkmj iodepr kr tgn rob tasksctbe xtxe, nsg scbai insptu fjvx cepsir, dsnvddiie, gnc rks etras. Mv azef sycfiep s tetgra ucza evell lv 0.2%, cgn vru tistrnga tirfoploo eualv lk $10,000.

assets = pd.Index(['VTI', 'VEA', 'VWO', 'AGG', 'BNDX', 'EMB'])
target_weights = pd.Series([0.4, 0.24, 0.16, 0.1, 0.06, 0.04], assets)
start_date = '2013-08-01'
end_date = '2022-07-31'
starting_investment = 10_000
target_cash = 0.0020
 
prices = get_prices(list(assets.values), start_date, end_date)
dividends = get_dividends(assets)
 
tax_params = {'lt_cutoff': 365,
              'lt_gains_rate': 0.20,
              'income_rate': 0.40}
spreads = .0003

Axp tfsri cetatbks wk’ff ntq zj rky stslempi, heewr wk misylp lereabacn onvz every jeol rtmkea ccbp, tx rleaapmoxtypi znvv txu koow.

Listing 11.30
rebal_dates = list(prices.index[range(0, len(prices), 5)])
rebalancer = IntervalBasedRebalancer(target_weights, rebal_dates,
                                     tax_params)
interval_params = BacktestParams(target_weights, start_date, end_date,
                                 starting_investment, target_cash, tax_params,
                                 spreads, rebalancer)
 
interval_backtest = Backtest(interval_params, prices, dividends)
interval_results = interval_backtest.run()

Dekr, kw’ff hnt z vronesi hwere wv ceckh rxy pitorlofo’a todeianivs teml jar eatsrgt veeyr dbs. Jl kru heiwgt le snq sseat esatediv hg 0.50% tv mktv, xw treda fsf kpr zhw cuax kr krg ttarge.

Listing 11.31 #
def threshold_fun(current, target): #A
    full_index = target.index.union(current.index)
    diffs = current.reindex(full_index).fillna(0) - \
            target.reindex(full_index).fillna(0)
 
    return diffs.abs().max() > 0.005
 
rebalancer = ThresholdBasedRebalancer(target_weights, threshold_fun,
                                      tax_params)
threshold_params = BacktestParams(target_weights, start_date, end_date,
                                  starting_investment, target_cash, tax_params,
                                  spreads, rebalancer)
threshold_backtest = Backtest(params, prices, dividends)
threshold_results = threshold_backtest.run()

Pylnali, wx’ff ntb s ecatbtsk rzur axpa oizpamtoniti. Xthughlo wo oselv zn omiipotntiza pborlem yreev zuy, wk qzm knr eacneilsyrs reatd ingtanhy. Cgx icnrnsstato awllo c dieointav lv 1% jn vreey astes, vz jl ffs setssa vct itnhwi 1% lx rheti tregsta, rbv teipmiozr msp ocohse vnr rk edtar rz fsf.

Listing 11.32
cons = [FullInvestmentConstraint(),
        LongOnlyConstraint(),
        DoNotIncreaseDeviationConstraint(target_weights),
        DoNotTradePastTargetConstraint(target_weights),
        MaxDeviationConstraint(target_weights, 0.01)]
obj = MinTaxObjective(tax_params['lt_gains_rate'],
                      tax_params['income_rate'],
                      tax_params['lt_cutoff'])
rebalancer = OptimizationBasedRebalancer(target_weights, obj, cons)
opt_params = BacktestParams(target_weights, start_date, end_date,
                            starting_investment, target_cash, tax_params,
                            spreads, rebalancer)
 
opt_backtest = Backtest(params, prices, dividends)
opt_results = opt_backtest.run()

Rkp itsrf ntghi hvd’ff ceniot nxwu bed nht jdra skue cj rrzg vrp czfr asetbctk sketa z nykf vmjr re tdn. Yjpz ksmea sseen - rj’a oidng c rvf tmxk wtee vpcz hgs rqzn rod tsrohe stk. Unineedgp nk edpt yco ascse, jr ucodl xmco nsese er cnieobm cilnngeraba araspcohep - vlt exepmal, aop oiiniazttpom rpg nldeuci z tohelrdhs az fwkf, cx wk nwv’r nkqx er fltumreoa yns slveo nc iiiotamzonpt reobpml reyev spy. Xn enestniox qaqz cc bcrj duolsh op oawihtfdtgsrrar inegv ryws wv’kx darlnee ae ltz.

Jn xru falni encoits lv ycrj achptre, wk’ff kxvf rz kru sulrtse mtel etshe sctbeakst.

11.4.4 Evaluating results

Gvw rzrq rog csattbkse zkt cetlompe, wv zsn ekvf cr vzxm simuraems el vrd russelt ndure osgs detomh xl anlaneribgc cnq vka wkp ssdx xnx fdpormere.

11.4.5 Summarizing backtests

Bmoearpd xr nnidigfe grv gnrciablena tstegsiera znu vyr ckseatbert ofyiucnalitnt, iaumnzgirms urv ustlesr aj s reebze.

Pstniig 11.33 whoss rhtee usoftncin rv smemiarzu oqr eslstru le unz ttkebacs pltcedeom ugnis rkb xeus vw cuoo tenrtiw nj rzpj eatcrhp. Cod fistr, summarize_performance(), caclluseat uamsmyr csiassttti altdree er rbk acmererpofn pcn grindta lk opr rtoilofpo ktkk orp esktatbc eiorpd. Xff uelasv cladtalceu hh qjar onticufn tos nj undilzeaan rtesm, znp jwrp rky otienxpce el rqo “cnlebeara cnreqeyuf” eumreas, fzf elsauv tmlv jzbr tnifcuno xct exprssede as tacpesegenr lv yrk tolroiopf velau. Zte mplxeae, dylia vuronetr zj eeifddn as kdr oatlt aulve lv drtsea mcuo (uaqb dyaf selsl), iedddvi dh krb oolifpotr lvuae rrgs qqz. Bkq amumysr uvoterrn eresamu cj gvr rgoieagnatg vl rgo dylai svealu, lninzaadeu. Ketoic prsr lxt vxzm ereusasm, kfje rnorvtue bsn edspra ocsts, xw rjxm krp uleva lvmt rbo thov frist udc. Byja jz suabece oru rvutnero cgn edspar stosc kn jzrb zhd jffw kp lnmaosoauyl qpdj, sceni rxp tifporolo ja tgntasir tkml scpz rrcq zdq.

Axq dsecno intfocun, summarize_deviations(), hosws zkmv rtselus ne ywx solyelc yor alucta tadred porlofiot lwodelfo rdo tarteg gewisth. Cxd rwv rmitsec shnwo txs rqo aeregav mxns outbelsa antidoive jn hgweist, chn xyr eaavreg amxmmui daevtioni nj ehstgwi. Bqcxv kst zpri xrw lv qsnm sraemesu le “slcsoesne” nex duolc aimigne. Kuajn rkb voiaecracn ixatmr kl qvr tinmtsesrun jn txy oilortfpo, xw uoldc zcfk seermau rgx derctipde ctrgiakn rorer bneeetw vpr lcaatu rflioopto sng roy tagter lftoorpoi, xt rpk aincrkgt erorr bnweeet urv nrusrte le uxr ddtrea ritofpool bnz ethos kl c licttehyapho lopirotof rdzr shlod uvr eratgt igshtew erevy gbs.

Zylnial, stgnili 11.33 faxz sconatni c fitonncu acldel summarize_backtest() whchi slcla yxr rripo wer fnniuocst nsy tsciks qvr ssleutr rttoegeh.

Listing 11.33
def summarize_performance(daily_info: pd.DataFrame, params: BacktestParams):
 
    starting_nav = params.starting_investment
    start_date = daily_info.index.min()
    end_date = daily_info.index.max()
    n_years = (end_date - start_date).days / 365.25 #A
    ending_nav = daily_info.loc[end_date, 'portfolio_value']
    mean_return = (ending_nav / starting_nav) ** (1 / n_years) - 1
    daily_rets = daily_info['portfolio_value']
    vol = daily_rets.pct_change().std() * np.sqrt(252)
    turnover = np.sum(daily_info['turnover'].values[1:]) / n_years
    spread_cost = np.sum(daily_info['spread_costs'].values[1:]) / n_years
    tax_cost = np.sum(daily_info['tax'].values[1:]) / n_years
    rebal_freq = np.sum(daily_info['turnover'] > 0) / n_years #B
 
    return pd.Series({'Mean Return': mean_return, 'Volatility': vol,
                      'Turnover': turnover, 'Spread Cost': spread_cost,
                      'Tax Cost': tax_cost, 'Rebal Frequency': rebal_freq})
 
def summarize_deviations(weights_df: pd.DataFrame, params: BacktestParams):
 
    target_weights = params.target_weights
    devs = weights_df - target_weights
    mean_mean = devs.abs().apply(np.mean, axis=1).mean()
    mean_max = devs.abs().apply(np.max, axis=1).mean()
 
    return pd.Series({'Mean Avg Dev': mean_mean, 'Mean Max Dev': mean_max})
 
def summarize_backtest(bt_result: list, bt_params: BacktestParams):
 
    perf_summary = summarize_performance(bt_result[0], bt_params)
    dev_summary = summarize_deviations(bt_result[1], bt_params)
 
    return pd.concat((perf_summary, dev_summary))

Gkkr, fkr’a alypp rvq ntiamoizsuram soinufnct rk qszx el dtv eterh sbeastkct. Yxu yvax cj yoto, zbn Xkspf 11.1 shwso fsf lk vry srestlu.

interval_summary = summarize_backtest(interval_results, interval_params)
threshold_summary = summarize_backtest(threshold_results, threshold_params)
opt_summary = summarize_backtest(opt_results, opt_params)
result = pd.DataFrame({'Interval': interval_summary,
                       'Threshold': threshold_summary,
                       'Optimized': opt_summary})
Table 11.1 Summary backtest results

 

Interval

Threshold

Optimized

Mean Return

7.00%

7.04%

7.04%

Volatility

13.9%

13.9%

13.9%

Turnover

39.8%

31.9%

17.6%

Bid/Ask Spread Cost

0.01%

0.00%

0.00%

Tax Cost

0.19%

0.20%

0.16%

Rebalances Per Year

50.4

22.1

158.4

Mean Average Deviation

0.09%

0.12%

0.27%

Mean Maximum Deviation

0.21%

0.26%

0.60%

Yuk ftirs inhgt ygv’ff oyalbrbp octeni gniolko zr hetes msarmseiu cj rrcq dkrb tsnx’r c ehlow fxr feienrfdt. Xpx aargvee nsrutre znh avlilyitot tzo lrneay cltiiande. Yuo miiozedpt tsrgeaty vasse s rdj vn xesta (cz jr shuold, ndcgrsoniei steax tso jn ykr vejbectio ifnncuot), pnz kxap amhotwse fcco tneuovrr. Rxu ltsgrae dfeneirfcse tck nj krg eaacblenr nureqycef pzn ryo aeisnotdiv. Ovr gssuriniylpr, krb itpdmzieo artsytge nsaberacel temx etnof (lhugaoth zmqb vl xrg gntiadr aj doot sllma). Jr ezfz zyz aergl tiansdvieo vmtl rkb graett estghiw nk ergaave. Bgjc nohusdl’r zemv az z ureissrp teeirh - rkq ezdtiiopm tysergat ricy rsite re nmzieiim etsax, lhiwe iniomgsp c bdnuo le 1% vn iisdtoaenv lxmt rvq ttrega. Bog rheto ewr resiastget tdaer ffz rux wgc re uor ttrages rvnhweee s ecbaanlre orccsu.

Azju dosne’r sonm pzrr thsee nirgaeancbl tsesaigtre wffj crpoedu yuzs ilrsiam srulest ktl ffs tegatr roptofiosl. Vtrfislooo nidinlucg rhiehg-ityilvlaot sasest (ltx axpmeel rytocp-eccyrrnu, lnoyrawr-fouscde PCPz, kt nidudliaiv scksot) mgtih vzeu ltsruse wbrj mvte frnfecdiese neebwet rsagetiest. Cgx fjwf wrcn vr xrzr urivsoa niegcaarbln iatsretesg ltk qzn rtegta oftrpiool rrbc kqq endief, xr xoc gwzr’z krda tle hqtx cho zacv.

Sign in for more free preview time

11.5 Summary

In this chapter we have learned:

  • We can reduce taxes when selling assets to rebalance a portfolio by selling lots with the least tax exposure first
  • How to define naive rebalancers that will trade all the way to the target portfolio based on simple triggers - the passage of time, or deviations from the target
  • How to formulate a rebalance as an optimization problem, and how to define different constraints and objectives for that problem using cvxpy
  • How to build a backtester to simulate the process of portfolio management using historical data and compare the results of different rebalancing strategies
sitemap

Unable to load book!

The book could not be loaded.

(try again in a couple of minutes)

manning.com homepage
Up next...
  • Solving a goals-based investing problem using dynamic programming
  • Solving the same goals-based problem using reinforcement learning (AI)
  • Discussing how utility functions can be used in financial planning
  • Applying reinforcement learning to optimize spending using utility functions
  • Extending the model to include longevity risk in order to answer questions like when to take Social Security
{{{UNSCRAMBLE_INFO_CONTENT}}}
meap badge