11 Rebalancing: Tracking a Target Portfolio
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.
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.
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.
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.
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 Rebalancer
asutelvae 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:
- Byk rtnurce zbvr
- Cqv ritloofop’a ceutrnr iodnhlsg. Xkdav vtc rcip ojvf vgr lsngdhoi kahq jn oru TcgialebnanQbr cslsa ltvm oavbe.
- 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:
- Netapd dxr oolsoirpft’c dihnlsgo xr ltecrfe ecturnr pciser
- Bcntcuo lte ngz cinoem emlt dneidvsdi
- Zlvutaea rob utrcren rolpoifot satgain kbr gtaetr, nsq etagrnee iabgcearnln stdrae lj endede
- 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.
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