From ecf3b5382bccc04933e4fef649485ae79acb3c22 Mon Sep 17 00:00:00 2001 From: Caoyuan Deng Date: Sat, 7 Mar 2026 23:06:43 -0800 Subject: [PATCH 1/4] Let TimeHelper instance could be treated as Series. This fixed #156 --- src/Series.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Series.ts b/src/Series.ts index e3627d6..98187ed 100644 --- a/src/Series.ts +++ b/src/Series.ts @@ -1,5 +1,5 @@ export class Series { - constructor(public data: any[], public offset: number = 0) {} + constructor(public data: any[], public offset: number = 0) { } public get(index: number): any { const realIndex = this.data.length - 1 - (this.offset + index); @@ -27,6 +27,7 @@ export class Series { static from(source: any): Series { if (source instanceof Series) return source; if (Array.isArray(source)) return new Series(source); + if (typeof source === 'object' && '__value' in source && source.__value instanceof Series) return source.__value; return new Series([source]); // Treat scalar as single-element array? Or handle differently? // Ideally, scalar should be treated as a series where get(0) returns the value, and get(>0) might be undefined or NaN? // But for now, let's wrap in array. From ee973b43f106036648f615debd94315354ad02d6 Mon Sep 17 00:00:00 2001 From: alaa-eddine Date: Wed, 11 Mar 2026 21:07:04 +0100 Subject: [PATCH 2/4] fix $.get call for namespaces with .__value field --- src/Series.ts | 2 +- src/namespaces/Core.ts | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Series.ts b/src/Series.ts index 98187ed..d63e459 100644 --- a/src/Series.ts +++ b/src/Series.ts @@ -27,7 +27,7 @@ export class Series { static from(source: any): Series { if (source instanceof Series) return source; if (Array.isArray(source)) return new Series(source); - if (typeof source === 'object' && '__value' in source && source.__value instanceof Series) return source.__value; + if (source != null && typeof source === 'object' && '__value' in source && source.__value instanceof Series) return source.__value; return new Series([source]); // Treat scalar as single-element array? Or handle differently? // Ideally, scalar should be treated as a series where get(0) returns the value, and get(>0) might be undefined or NaN? // But for now, let's wrap in array. diff --git a/src/namespaces/Core.ts b/src/namespaces/Core.ts index 17de399..1f8dbb0 100644 --- a/src/namespaces/Core.ts +++ b/src/namespaces/Core.ts @@ -189,8 +189,31 @@ export class Core { } // Overload 1: timestamp(dateString) + // Parse in exchange timezone (not system local time) to match TradingView behaviour. if (parsed.dateString !== undefined) { - return new Date(parsed.dateString).getTime(); + const ds = parsed.dateString.trim(); + // If the string already carries explicit timezone info, honour it + if (/[Zz]$/.test(ds) || /[+-]\d{2}:?\d{2}$/.test(ds)) { + return new Date(ds).getTime(); + } + // Force UTC parse (normalize "YYYY-MM-DD HH:MM" → "YYYY-MM-DDTHH:MMZ") + // then extract UTC components and reinterpret in exchange timezone. + const isoStr = ds.includes('T') ? ds + 'Z' : ds.replace(/\s+/, 'T') + 'Z'; + const utcDate = new Date(isoStr); + if (!isNaN(utcDate.getTime())) { + const timezone = this.context.pine?.syminfo?.timezone || 'UTC'; + return this._timestampFromComponents( + timezone, + utcDate.getUTCFullYear(), + utcDate.getUTCMonth() + 1, + utcDate.getUTCDate(), + utcDate.getUTCHours(), + utcDate.getUTCMinutes(), + utcDate.getUTCSeconds(), + ); + } + // Fallback for other formats (RFC 2822, etc.) + return new Date(ds).getTime(); } return NaN; From 2c8e8057e5f2215b3db7c2f1b1e06275372f6925 Mon Sep 17 00:00:00 2001 From: alaa-eddine Date: Thu, 12 Mar 2026 12:22:45 +0100 Subject: [PATCH 3/4] Fix 1: closeTime normalization Fix 2: time_tradingday Fix 3: setTimezone display-only --- .github/images/sponsor.png | Bin 0 -> 66874 bytes README.md | 5 +- src/Context.class.ts | 12 +- src/PineTS.class.ts | 40 ++- .../Binance/BinanceProvider.class.ts | 133 ++++---- src/marketData/IProvider.ts | 12 + src/marketData/Mock/MockProvider.class.ts | 18 + src/namespaces/Core.ts | 5 +- src/namespaces/Log.ts | 67 ++-- src/namespaces/Time.ts | 4 +- src/namespaces/ta/methods/vwap.ts | 8 +- tests/core/time-components.test.ts | 10 +- tests/core/timezone-fixes.test.ts | 314 ++++++++++++++++++ tests/core/timezone.test.ts | 197 +++++++++++ 14 files changed, 727 insertions(+), 98 deletions(-) create mode 100644 .github/images/sponsor.png create mode 100644 tests/core/timezone-fixes.test.ts create mode 100644 tests/core/timezone.test.ts diff --git a/.github/images/sponsor.png b/.github/images/sponsor.png new file mode 100644 index 0000000000000000000000000000000000000000..d72d3188ec8f617b2278d3239051dc3b0f01f36c GIT binary patch literal 66874 zcmV)AK*Ya^P)EX>4Tx04R}tk-tmBKpe$iQ>7{u2Rn#3WT;MdsUnW06^me@v=v%)FuC*>G-*gu zTpR`0f`dO6s}3&Cx;nTDg5VDj{{V4PbdeIjmlRsWcyQc@clRE5?*O4yW2)H~160j2 zGRe4@FRY4zSNIWxfG7qfX6mWzVh*0;>mEM7-bHwp_qjjEuu?P`;1h{sOgAjz4dS^? zOXs{#9AXtoAwDObHt2%Hk6f2se&bwrSm2oLMOEZ1pHAc-ZUkcJ2u4OCHsg$S)0DJC+sANTN&JN`7eWO7x( z$gzMrR7j2={11M2YnEoBZc;D~^uO5l$0QKg1=>y9{yw(t_6gvB2ClTOzup4oKS^(P zweS%zvJG5ZcQttrxZDB8o^;8O9LY~pD3yTsGy0|iFnS9Nt+~B*_Hp_EH`c54-uCDgzx|W02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{03ZNKL_t(|+SI*ilwDVKF8a;ALeQfD<*XGvB`Zm}@3 zF|B(70h=Z<4iEw)&bxs;xCvay2n-mHG!O{a+zhx6LJaqs3n7dI+cCCBOlJ)4WNeJ( zMz$KO@2gd{=bSy?kKL`c_G(Yb@FIq=s_N7^d#}CLY`*!;Z_cgu%=pqT*Wf-zB2T`-neB_B+(hy{Olf{ce68>bj}- zR(VZ!PvX6})x{(QyY5#b< zmu?@0T&HO-sCzVc?`wWfy-xMBeeP9v$T;1M`aPlSrMdi3pBoJ8q`Obu^9fncx!R(T zy{S8!LzwH9u=d^~DtwIV zIk@bm&7LGlQaQ8Y>p8n;+gtl-^<@e{J!n=R);OU9)I9D>K`+okOk+AP8YbqqqWO?pH;)~0(RP0|4^XWEwt2@^|0lN0JtoP}u z+caF1@j%%QoATh*!{Fl_bsH}a`U>Zhzdj-_2PAIF+z`TY&9-~;3DB|>NXk4->&lKS z#J1smj4f-Pv3Jnn?Fh(-ije^^a|H-4qwj0v80>WjZTId%BYco1(`SSPBgO9v&MYJ& z)kTZ8vnU2U`I&KLjpR62CN$I)@U*ihY>vxho3zP8n?aw|krs%r-R`>H+qLeteQkDh z%Kc`Ds43v~&6?XmpUZtHCs@8gnxDN6OI}Rt?+PM)6KL&ySb_o3?c_nE`Wdz)!G@6d z1h}?!H?9d02weJO;k~0IyV$}}Qwgo8&LrGf`EN|NIPlUKmzf)i$^mH|QN_?{oV77f z@HB+hLzpi%n>TxL2t7a72eHv^0uKjl3ov~$c~X!QwbY|SRZLK`$zWKWy}EamXHlSJ zhG#J65(wpuY)&=!ygEvj zIz&o3BOaNw^sE3NN)s#g*azH(y$nH+`J!jhG5#4;06T8;;Zz{MLgh|4UK1D&K# z#u^eON={d{$e9n~;-MisDQ zOIF0k<dMeMo1;a6Uql-#2Ad&e zZ(9ksZClZ9@>=*lF8n3KaMn#gfCm>vI@<`?|CPNh1lUQEa!go=szgIxK^o>5jkX)T zGBOg75=@8&Gf9#X`ALLp&v|u~2iLwKR2>C8(3f^=ueQgng~>IBL`P`yiF$28T4jTA zm9rz&Pbq94xKN;q!%WhEnzROujF;fbYF>;WB@|`QX6gzGq=S^(Ef?7cuI-Xdb?8fx z{1{4SbIG{pYMXqWb&8Kk*hS2C1YV;z_o5mCW#uefs76YknIgf_$FQ)l$yEr^>d zB9a0tUC^S!k1ise%Xqj-Cn2yU03$)xHv@2G$foXtD2giN-_S$po2`e`mz_?}#e=aJ zW?K;KbMI32<3I_0Vcp8U3pisoPTO43Dpz^LcJCzq*RhXuJQecnaGDFhp`BnY#3q{d zpAYI}Bb54-<^V_mi^;Z`P+u$R9eBP&kxKO$YJ+p3YC#TQpyKaDoK5lRK15X?VtA;p zls`A)ijqDti9i75KZr8t&Tu+eS0vl6!Pb&#D80}KRLO|7q{=)>6d``d5Stpjv||We z9UB$+-IK*Gmm#t?Lg>hPX2aG_o!Qs88M}~#!$g4wGNeWs3Kcu(x6Q37-3t0=WD=ChzfZG10nY>oqw>Gv!<-Q+EqFY4a!|Du)gF& z@9rLQ>}v(P4e3&}a%U_h86U@}Yt{9!4|YBe8!H5^Bqbyf@j@V2h30&Wh2q1-ju&#< zmk>UR&wW;&2j$^Ll=^hd??#oZC}%z^htp=%+ch}8&?2!pk}iW2mYieJiK5gtbnR2@ zX$W11-i1bZWvx>Enku>hVu?iC?LuzE10sY*dZQcaK&dR2r)P*xPZn!MlQbG0w%ZLw zhC&0Q%JZ{4Yg6}u4>DmTh`0ItBs|#rUc=6f3ni_>5GeMiZ`-)bfI97y(#q=-gcAGh zmyke7(mtUud)d|>cD%Pnq9M3D&e6G{D~lK|QF>jj1!4*Wg99UwUwI7(wjI8RL@26e zI-4w>YciuQJ^^m1U$>&GmP9VoF%cek1%NQ80%}doEbi6bIq?wzl3rUW)j;Aum^dYc zWA-qFzMIY$jx-w8lB{8OPT9Rl-%re+FFxl&uY$vB(#F<9g$tGVk2P0%XkNeV~4d;%Dg4o4`u=Yx3KkZ2QXYZCah1spz_BO$io zbUA9<@3-A5pWAQN$C+Zn2>1n2FGiyV6o2e$GKne=oaG3MJ%fwT=(!BS*$R=_v_Iq^ z0LGu|f|D`;pt2P$&=C3%Lqk~qB@3J?u%eCvOMwPY(xG5~!=f!E=!n0buwMwnKGZSW z>dCg!WJqn;Zd^%P2_J#BQ1Lz{6pD#D;;U_M%4B7Yh*o|XN`PB=U-3N60i~&QctvDP zkwd#n+C}VKr8sS1taM&`>mI@03M@VsAA32M1W;ExlF-{go|bO^y4FPuE<-pgK8S|N z9*du83m9yB!SuB&acx(0PD&6o-KV4mDXLEoJTtxs9UFX=)29oGQh}IQ(SEkKw1p`5 zsag_ZW*y|nNPVwruRZB*NFyqhh_zFm+rjqvxs|trYE3`yzAyoL(jkJo?ymA}T$zkd zhLP7o$zhmP^d>aGaP2W&z1b(Das@`d3ZLaOBx8NrD27Eaww_i38ZO2ASo7BhnedHr zb=f2<=Po4M)Rp0E*J9eEp(Qw%AfE0sUCvs^`ytkUwn?Z>d*-d37u9lK2sbP!kx{PG zmROj&`a%OkTc;xjbOuSzU*mh!=aRo(R~FI{ZH?#Ofgb8!UzFwpyO~6!#CqOY_^*en z^XX41%M_D70AtF~gdnyaW z8T(f-_O6UG1g5lRL6&G!MM*eet;2WeLYO0e1&wKgn5PCi2!Wc5(GBb@0oaU_&iMNT zG@*f!t$V5x5Wudqs|_B=kOdi>g{qXOGmPQTML-r@Tsu3Rk|`@pl0<)I1k}dG4bbg< zGPF?p+9oHWV>o}VJ<<+p<7(;kRTplDkD?3Qe3A||VDeE`^_5*PeQGWhVcV~^R#unY zFqPMQ3H!cI+KAeKt%;^+)3knE;ccaWD>(z`kbt&=es~5qg*_7x+@h4Dq6)+%SHW~< zby=GB23#`z4swtS@~#e)gH)=}sjJ!s1hTFi1HVn?n^{{?Dp8Arg#gRrJyqc?lO6#P zf`;z-1ZO4etT)v1waB$b8#kccxfsTOlM)z_ozr1eRs|a}1~Y-xtL(yNV-+GR-ga;z zRa4mddf)b7-5Iq(*;Zr7n&5-vF+UZeoka*FRL;K4R=L8{@zK=q31UL#+T`b_qSk6@ zsrAjsRsNn$Pa~A0;3BqHQAG-}sX8*b4Gjw+t@|Xlgsf^)Sp-82=`N98WGw&!64I--;6+>gTy)i<(km&SOR&!>$l)!mzJi#! zk$`59`7pW$hB*hIiC+`Egq5WGVpya5{NodWWE-k+;bUZcwOULq=RUBoPsUksj4TJ? z`7}${Bv$S9E96g?G4oO}Wq85jHVuZVonR+<66!S+Gk?R{BYibqf^~4D_u$GkSB$NS zVzC(&Nj%C=`jV4ihkUxC*v$_)-E(i)q0(D+qFM&Co2hy`wz&6#|Sthp#xmKQqAP?vF+qIV>> zeQDmSGM3yilu7EWX1CM#LMU=niQHK)Q@PIUaCo7^v21XuNa7Q5X`dlhMk6O^wzwRI z3EtN6g4z{oYa1mIh?Oj5SuEG-JS% z+ep<}Oghpf1Kz?_p%aaLMr7y)^HG7Kl2>`&$^Ks3Y&LxvHm83+fWo*_fj z%0L!v)v|2{Rzgx8L;0MUzO{rWzdt$1)S<*sUqjw*HlFJYIni53rwMuPP4yW|_F8x| z8SU1uV>@=&XZB0z*KLaz3S5xffftP!8COP<9^keWH>{{#hZ8<_YW z5Z?#ss-1gjJQ#x-wh$&E>vVv%nhn;ViuCQTx}I^bxh@PZHi9}NXfGG#zD@OOhKbBu@A7faaKwiSum6fSlI$2pOTgHx|Q_WxChxUc=etiM3nAf zO5t~6Mem2InSLeE5(9goYrq=RyM*7sGCCQs-+Y1zUlhwm(likQZGjeGX|)YVZftwn zw=Cw`$D-C2dt9fntmI+Gbv_``YdZ)w1yYBbX$YvTTYesv;?s>tr*u_9P612oFwk^+Q@bY<~rA-VywVUl~EgfUqDsG>sT^yQp+tXuv2Je z!3Y{nudR2gB`O6CDh{3ceJWYXtd`l>oCW!8RB4QP=DeWF@tcgFa-GujVBsDMQ7k~!JGKNyCHuP?82PlKRe5DIYNUZf*h_)fjckdxwqArP2elkRw z8nA&%qNj&WgXx(`-7mt#X_Fyr%czk*isdtILyx5TUsp&=uWu>EghF_i(WFGluNM|=WX z(MH-jk!~>%%5&D)7y=e2ywN4+yHJ8~V6gsfAw}g|ww>XKN94KlLI3#{Zu6jl)1#!{onH;o` zEXfTG0^+CIPxjOHu&e%_p7Z1zz?sfNMF^2S0%MjF_mJdfF0;sdUl%%8Dbm5;V(X`C zth4W7@ijmuqN*)>cV16k5@LDxSr^cA?6sZ8pk{*QQH`Q%5I|IIjVma9oD_ehfQ12K zlGZ_uj7`v7naGRYv%1Fi8m%w^lg!fDUbA16=c*Imn=_oMfscJ*;^4`lQG|-rVn7CR zI+afSVPJcIzpu(?sKA@8HI_9b+^TA1hVFQj)+(*zfhd->Y-kwwU?XUn)J{Y8Oej%P zlF}zT>T2WYxzC6h9b?Z(fYp{|0R?9PSpmw{aPmkTT#-pTqdK&Eg6!oHTlr6HiijvO zBWMw6Q7s$#F(=}`$IPk{kWwbhYmj%<*F>&5qKolt3pk8$4nb-YRXs8{I(s$4;2X;{lPKq&VtJ|{_;6p#le zifhWscy3kBngkj3D3TQaDory=$CL|}cO58K@EVd0c(wfkGVF}@#rxVu5kmH@hEt_O zrF^q1tz(Y}h{!sJ$BE7uj2{V5zjYlF*6KUBP&2Pdz=m-TFGg_fJNL$hDi&mDP8G0W z>u@3c)N`9J(Af^3Jye|$#9k8aQs?-v)>suB|}wymV#yz z$z?62yk+a8YPa*KqPIetzzbzyn+H{-96l(xtjY=veKEz{xAL}EU?D) ziGbCKlE!(d_nUu4QgsHQQu^A7#B)Tk+C`^a;~v_1Zr;~JRl93KpiJE*gi_txNd9DK zr1b4A->gh`_aTw$kOrGQbDgQAK;X;kF)5D|QL2a<&4E zaK4YoqStd}Xv-q%oEamDy&&{MMu8PIYHv03fF#Lxa=O$in{^DD-iHl{r%4r}Op4_o zMaS1{8<{14o0NzpS89abD>KnyILS-@R-V-cXW8% zhiPBf$5kjcfc>5|876Ius1CEH9F<>}K!z6w>dj@+yziOzQTO#ie0`3I#Lbc0>+^0o z9hJd&7E0_}k17)?f~P+IC~X&}Y|5@@9nZ9QE*RT13DxyEq2uCBGPlNs)zvsj$$*s1 zg5bXqLweEKWB8j1S@CMe^04hb0ai$a?eolxB2KzlD;^nrh-7=Hp%3{z+x>=!9JRA= z=BjKN8hn|iTZiiONM%H_TUxCHgn9pvGK-1=0#?^BH{H00(16#qcD~bhO{lg`pwj0W zT7)cpt6N?iQ9{{$nZqT3@>$~@LRBA}8&x#X(LO=z1o!^k*iw}ZiK=AzC$W~ zpRJqt9u_2tC2E4CDc6=vehBL7C15G+$-EgtwIztC&ICm50;+TP!%rtg!@Rv>sN;(G8A*z!Qn6sS>Z zF)T3ovJ?YBRoJ<%nn0>uZku`B1ACnvSFqxxysU8wYFDFOXpy}3v6OU*CYSU`PEN26 z-FLPi)a$$xm!ehL=P$HCL)J<03Y6MB-?qThCGO}NjOv9bI&7WpK8qtqr9fqL7BZKllAQ^MAW4!+K%`nWS0Lc3g={kvxH=rCU`m| zb5-}3Z=hg1L%yg`$S8@*&m4mubzN|#&(rM<>pGCYFCcZyHkC@gS?ZqeYolg{=b%c{ z@2Tg|SNow|Vz%r3S@Ds&Ghvbq@CmrAh?+rkmo=1LG2bB# zhlSiik0!fzI!Zp0@TIc%)N^C}OqSh_@5mU{m&*2v%NT*&D2W}>*K?G_`--#%U(*KW ztH)ri^%oA&4y#vgj^Ui%Y1>>3|9Y-za9(A4sRfo-mHT|>^n?beBl6*6-LoKksHF+@Hk zG@tUG8;;jW>09oNpj8jn8C#tr@F{ut4Bj-3dl2Tou;(-MmNyS5kEt9JU6+(vwsD~b z5y&eT)$Ie9R*c-J5RS5Isv6~`Q$@~3DtDJ+kE|8)3`Ig+=%Ma3x0FoGbb; zkX3fYy{L}*uu0-OrmJN8%CnBgmePas(2|hN5LF0mwiY=8oJ2AgazXHy(${6Gx>5CE zn?f(VRC)Mw6w#^}TqOf#A)}P!`KG9q8&YDMJQ zZ1;7QOy)v*t^9tLdKEI}ijz^#mCxR3#}WHU>9a9_(QyhW7xp94+87raY}yMlMS;P@ zY3bCkhe9|MzFaTgJ)}#tOz0sIRT4I4wh-#MGKVWthCzBGdgQmCrRPnuuC>_RPKJO> zrz)EZyphQndZJJ+JK}?WhIFZhzSh5tUrl0|s+0xW`-$_A#6Ni}apaTTdOKhfj{x#` zcay}iHnUd5+Pqq79VFclN9a4;&WpY?J%f>16;2zMxCU3_3<7jn~%swhRm@3P*(hpZpB!MX6YH*yj~S{U5u zn8n~h;7c&SV3Rr6FNc(ez3}<_K6r&a)}lv<7Lm zfQAIVjBVYZ5b=qFe1k0zQ_ri5oE(}^`TSl8nhf!ou{fPMOfgejOz?Fw7)X$m5=c1;sxIRgU6Ikb^`L)3nC!L}}0 zv3A21iI0Gth}DjU&HQGw>Pp$E!oRlm1ql6BHN>ApAHF;^gNe9&lfiJ%^H0Sayx72BComvmyr2C zwlah3%!S^E>t_|xpFnhnHk_@ZfRLb9ZvdrY{U3t`fQrUj|ldNc`C zyAbWfT+?$69UNy`FK4;Qgyup~kYzo(B6c`QcEeGUnai^j2H2&gQ;Q9;>e~qAd1}-B zR5@HSskq|9ZCzZJ{cid?YIo*@@xZH@7ZHdeiStlIg`%U+S=5PrzR6q^OI=d>HZd}_ z1E#_`am;lfkX2v7<;W_TnrF>bsq6#^!}&NMmxe)s$jkvz&>6@VQ}oCrK-F$7f35~= zE@v|28bV2}y5P4|c7}vr5cG*)NJ&+zN-DnIV^~@!;$}mHd_)dIMOwS zB?3wkifFr@d4w174=K?#X=W!82~+M6KJwjX(3mdT_Ix^(d$AfGDua|_3^j#mON8hy z(^_Ew03ZNKL_t(ia6178=04}fH()7L6v240|AdM*`NGF@fk17lwV^Wdr{j-}Xl>=C zKu;dj)t^PkylNoJ?-MB#MLhx~m)?VIR?g`NE`4g~bE)O~+9=i0+gj(H`-uG$67PE> z_x-9!l&zFv>gsXXM>mwKB)yKIgrd%q>zTA`m()%vAE}NP(`Homy@-e@O0JZd;&jh!F_LSY1+UFc{=dQnn z2$Jw#w!g0I#fGA}TFF8Ki6CwW+-j`8im%4(%Y#p(X83bCd!c+IlOBim{8qB+au8Z^ zu5C!8_qCO*el=L}-CMd6f;DOnT;EUuyAYDSFRI~7zPC9s>my*l&+d0P7)b5?nFz(F zA&PG|g(JX8SPq4swdF$9$p;t_p8ntDsfbXeDUbq9KOBto~otb?2h z&fq?TKpApRKII>ivuSrGQ>f!Jp~F6kw5P*X+CIzG)n!T?+9k$^@LgNXU2HLy_znH(BF5VPtxa90GRf~@LU0YJHfQg zspx{@kO|?kp|i^lIOBE?HPaWfbfDu8Sy3ypuZxpd%z^8xL0@ybDK<^GTF8wyhRkJ= zxuSlyKv)b+wP>lKir_o4!L&bI9G!?A&c^qqO)|!;K~%JxL^&KbQYs1KseOWk!a!&B zJ5I_OAM7wBKKc`CtyCorsqKrVp=LgHWfu&olNI)bqP+r$HnL?C3N~k>CXB5&b{h$$ z0)5${Jygck3Rg9xWJSe%pyRf(!kogzmIZQ@*ye(ecq414Eeb_&w}(&GYDzFnb|55? z(4E`1+R-hJ#g*K-kebk-%m*Qzi@;z+krqAiCB7C2J#$#ukE?-yD4L`Vx)@&1O(#q5 z1Mh3WG|F0Ou~ti*k{{^wwYMIM$C5rnzmJvWrBJ;+Az|# zCif_Wc8~>|?eyWMSjo67A51%9z?FdAM`5|$;jE_TskOB}_CGUj5k&=B@1y79OtzI^PYX{L>>_1<;XLVn+L&O?$IS4pZ?*;vv;lAC081BU!ujLCqN=iirQ ztP4s)bVKBR@6vHeQtd)ViRkrh9W8n&*3X%^2IV;oR7R{NCUix0EbCo}4a-+!^`d21 z(l>zKjvn;Hb;J}Q%Ciju8c7qgtrkwrPGe+h90!Jv;pv0BabWBirjlk_>L>y^0+0Z0 zD8y1j!WrEI*tmQ()-G9v#XbG#t#_e2uA>&ksAXtW0!AZAFxzZm zVs;86)8jZieiF|d*n_>JM={x&MS`T{P!vauHvnHwkaEI#zv(^ zpX;5A{)V5XNqcBYp=n}}0HP zq(T?en-HUrSzEgEL@1}<+C~TXs@aAvysN_CN`O8P{3ZivGwiO3t2Q*pH%rrr>q+Myj7TWi+UCB94pLhV&c(IbbX+cRvAH_PVqM`D zPBeZ-(UAb}edslD+9Rn9K?Z!1IonkG(7?o3yfX;>4VXl2q9?fazt!G)OvZLiGY>(C zT}ZBYpGINFJTQ@*Bg&G6D=;BsgLlY0-Bagu(+kWM9Z^WraxP6SKq@-uOutd0wgVIC zJtI`wA&JXiphQ=)atFe?DO?-OD`REGimnB?`rNHJf5q7t;5v9@3d!gcl9^c`VZh`izqYX1^in`3lmxq0reQQ`8}Y49*of}o;tb*KYV5z_D`Mw>(p@>&hm~v6&1pf zq=f}>53bn!GF-fJ6V9mjf=8#(7@0saH4C&-a#rdr(tL?Hn~cl}a?M~SWMzrW!4M(o zj8W_FMs1)Mr=kXS3?0CIJ08Q1;X`O?_^+l~y)Uv+-X~cVGEGRJEy%MfylP5Q?KllJ zGuG`_)70ZoWW}ah>FcOU8{t3m$(lgq_Z}K3w23yEGX0QJ4m*w?X-dlBF|L0{h|Q5D zf|9RqK%0M^aFR|m7VMA-v6(BGTX!Z;$NRhP+k|lVezq-$vaOR(*=sHVnd$nqCBp(S z5z9BlX3&%)tOv7vfi3#0cqRfUNk1?b;-v~c>%MU^5LyW4N{_1aNlnZr&VL0iJYzjNCuYzXnm{zu1QH<5ENkgpSwf)_ zi9eE}AV~rcrF@bENijntA%Ht0#0&b+?5*MHlY4R3GY{c~$&*h=>b#ZQTOdg280GCR;Q~ZAf}l$ zy0epL0u$GLI5SWx;Fu~m4SJr1ZM4Jb7Bo}?y3n9(F3#HN@GRHkLefmZTdupLHWJtZ z6Bh#FD)Cj$3v*FCZ!3B52v}Sr8cYt|RF3gQEJYJHS1yt58LOX}BX^PyECLgyb{0jX zb?an$0Mhtt{iIm^!YNQU$k~K6%Nb))}m(fy+lw_pQEb3zB%#cUv&$Bg8 zkrc6vh2rO2Bg6}OG2P#RhYmi2yPkOvqsdfR4hbsJrte=*2(LQxTwJ^H5-e(VVD`i) zXtpJkR|rv#_v8+U8Br#`!vbO#QTpHfHB9Axh=fs7`LZ@+LT#W2$$}m{IkXSAJ$XM4 z&yI+sCu;|iS`M%Uq&5SQf1PON!G!QW<{Fr^8vxA}G4VmFd=L|mGWWzI^D}0y;_f1I z_m)qo>a_h~r3{L>%||*NL6NC+DcO6^(yz;bxypvVr3_zG%9v!bpTt8u`#?8sUc6`3 zM)(rRZKa$F=TgHz^BENqTFS{$*#c}=d_FB-zJf)RM4jC0k3I-d4>LH=81qa$NhAYa zUSRgyiJ-(L@;Zt*$4q|Bkr}cGY54>u#b*%}0aDS~!NSk7&(axEALC$(?zK=J5`ZIY z>RW~zF1!M(YXfK=9|cc0MmqFyh=bu@hkKZ}}a-27RC0fVFz!Nhd&NJUK zXpM-nYt0~BiV9RMH+>0`EU+ReaOAA?ldemv2x5pLw6aMLT#Ha&G!LhGW8AxQ8}5DK zF*IXxR;JW_XnpRi1Z4(t*<2Ju+bOR(YvvlHQYR%oSFt=-8OO%{FMcKjCcbdD4%N-4 zpyzVi711_!pJS8pb{Ts;2{-*vW;HDFFXd>1i2PoY04G;why@Qvwp9=YyRqIT3!kZ8 zpUOMXt{J&;Azc9F^WM6WhK-*TqAoj^Y3Ot1=Csv87>RNk+!i0*+2JS>-BoUwgqVo7 zjNDFMt^g^pwy^o3;f@yjJA&Zo#TQcfEIvbRwHM=c>n}#v$PAM4sq{0mQbs8-MFMdbXmWyyRs++GX-p=w zm~1sL+iIYhIphe0I@Qn}*U%exptshI?zoOR#i$h;0RSUOgG-^R)rtU`*M-)iF8t_) zU*XQ352Hbew34jJV3`>)16vlZ!|$B;8Z4NO(HxmT)Xd6BQT3rlS&6WT1XHaBCR?+Z zXwG1|HH&F(pqZo?PfQVN6r($;qbKe_Ppu2xQ5_vo4Ygu^bsG5OG~R?0fYR3DsHX$* z8T0V?$XB`7e z1h;yg{ASu(tP0-~dlD1tx+G zHm@-%!|=oa*N1~=Yj5@GPSNpPeB}aM$@^UPzlj*{vo0p2$%iysSt}7a_CT~w2XZ7{ zYb7wT1tq$`B*{>ZVuEf38dBnU@sNTAfJCS-D~sZfM5GyV+lP|@js!!aky@d=WPob> zv^paqG_sWf8zS^@j2q9n9G5OR8_nZcnImsq%E}cHMQN#mfd&$cHl}c7Y81z3$8mUO z6enlLG2WWNsbmJzNh3oFkOd|&dZIe|qb@9}_hCuB56e3HvAkyi7IyZblWK@TXc2)j zqH-%qGR0H^x+2ixempU<58ry?UW`#g4k}p&#%l@Tbt_+j*RR`(cxW<3eG*6Rt*t)qYliAyRfj{i!zJON#_;ek_U_$_0|yUcWOx`9Meef^s^>7r7H1P0B9(3h zdsJQX>IM6hRJ7+)GC9?nciEQ8Mij}`a%*g#Fi zN=mRJf%7>xIZv;hK+DIex=QV*GZ@#djeyRZssy3joOTi+qZ|kPOQyb`QTewRn5e`e zXJAsiQMK(%ho%5zIB4dNDOR&Cg308D3L|?5N5toybbLK09(W((@3;M9TF@&%G@@^dGsjA@yquwIC zWy8yH`SSBXCnvx&3Cdm|NM*elZenP50=p-UVdum#?4KUSk=7VyqKpF+MMZf}3J|k@ zBuN6a7*Ud7fV!}vV*qFMEW-NkB{;KZG5Twrs3As+lR`&@^BL%IgxdUWoa{>Qf1bD( zPn|jldA6><0rcGyA(LTSoM6-7CR~5rb-3h`OR#$NYRsQMADx|@lEX=o1k=;g7#$tO z-hKP<(1Q=+jyvwa^Upu;o$2`?CO&~y$Xp*=38_*R(Y#JH>?Z%Np8K+Eoyu}aQhtWo zh37-W*RBclI#4#^qK$))KWCkR4{?OP0i+4hkpny#;kK2%#P!i7bQNNzMnlC_9&4xl z*QUvmk6#f-Z}<)@^jYJAmY^Y~>yQxF^Y+!2@$t2;B*b^RS0U@YToe!bzSeUPGLgqu#du z6?oN>L11VSXeE-dkFt_ciy22}#;|kZD7KFu#Br3o)hF9nb5+SbvOL9=``qpE_JEVS2W7`eX?{ zj-H6He*JoU@IxQMt6%jh%%4Bs+49GVnA-60Fn;orpWusM{32d>;RUZm8B{er4tzlWNWveHKSBe7vBMXrXilT3_mnD@p)D{ zl7G3^%KhRSnApV8n)Mh_tyZiL!T)|ZcSaN;iejJ^VGOg_Gkp?=rbaN+YM?8wqpzbYE%0Q| z0ZB@-9H-Ks zbk!8wAWRie3f=4Rym|9J1@&f~WVzA^vSvynlLoyYnPZX_JRX8Z_m>6@b=E1B?3?U_tME zJau9(7{C=vHsK9xUJ49Pf|`OlXSUux5?k8ipOAN|-zv0%XhLtst?8mV?9F^j2v`s&`^UR-wBWtf?n!DEj- zCOKYpUsx-!rp!q&uzt=?{&-s%$Tlfh)U1)}^(ykAOJqp5up|dNq&Qw-w1G;DEdyx} z!f`R_J3|2?$FNSS?k7a#bz0^uL{?b?;d>VDg>AyFkCQLj|HTtc@IH^OJxf&OKx$D5 z*aOYb_02Px+J^3PgeBvD4ZK zj)J!(UH#YNCe#SoBtxt+zC}k{?KCh&)u|2$G7S^eFjTKiaCTA+l*)l(kze*fS;ibO zR{?hdBK^uXb|4WfC?+Yj_Ezg z(!ofMMgkS}a-u@E5>bFeylwipY5KVi5g|}@;;!Z2whY=Bph(Y^1nCIDcPgc&kU1_Q zV(uy*`py?J1~!`O6a5_tL9x%?*Ztd6^3w$;X#z@?HgwkR85c_9TWT_0r!g?`G4fsk zh2>DP3l+(;AD9VK&=}JyV!~P}eTJCTsqB0&m*vc`3^avIS@X9Pg-YLSuwx0{ zw&4}%8gGJDwoTic%o*p@f|2VN7&o+;N>a~n8ksKlc*uW;*MT)#~t7#8&{Yaisozs%jPe_EVr=p)G_~Zl2{Uuf>ElV z5~T5=eZ9T7`HNq~$}?AH<+*CTxz%dn__1TywQCoiefC-G*s%i#4<5wC!~}>4U0q$p znyc&C+1Y`!&RUCafBV~+o{d}O*~>|?j2PIA6m)0M)6fO|T!e)TBj0riPI<;55&T7@ z*LH7`zK11aCKo#Ey4QfD&vw4rnGUO#x!Hk`c%v6W@K*j$6Cknw^%{8`O2Fzpo!e9X z!MFw$W;V>DRXpC)R9bNjoV0!4r*h#MZmAQFvyq`|;nv=RV5GpNg(9Fp7A-jjrBl-x zc9Gbs%?bpGb(Uto_&E)U#ZUTLnidsOibe@!LAd;YM!L@xZ8#C;wPL*KtV_^8Mc`%= zNy6oUWMGzCczXCCelYYjc4HWEyty@ekPJ%b3qWed*6K)na8B%Q^RaDs>-*2?8yUop)u^W_-g%M z>_F0iCuP?`geW5X{=0u4gM))baKnNDWiUpDhw<~DKY)iGdI(QG`70bgd;~KyGwA53 zWBKysIRE_f@v2w83YT7X83qOhq>L&*ujia|E^fT>MttWx-#LBz#&hzrE+CWCI7kk3 zTHxkA8xt>CTK5o&stEH-U5mW-2u(Xuo!7Jv`ox{m{pP~>m{jkM0%(s2O_y*ow{4d6ursE$qSmx%Q?9yTu#c2 zl)5rSWFDuUp(SxbxaN%Wamm1X@Z^j{_>D4T;kmJ+xcB7K*o6~_;<`+pJWY<GtBcHkGY2SBx&kP*FD zftBoNq|!8{*EAC`4owW9FYdq@9rMr?b)>@p^ZnFH&>ht<&_5rKAK#5uM8SMR8&Q!o z8TrRP_A#7!=1T3GOifMV?z`^6%{Tvd{P@Q|!LegUF*!Mj+1Xi4Oibd)k)!zKFQ32@ zPdtGI0|QvUVFT**xM% zs4YRkuFPu8rY(q+xJ;5@nbeIJc<-vsFH%Q81mUzK6&9N%ib)!|NGi_=2o?u3ArC%f z8{a*ty$)r7rjEj3D(3_fnGHN$b5N%5*KfmaZMLzSZQHeJvu)e9ZQEOIZnJG;<5Qb$ zzjxo6-#b%(HS^3oUFSaMqjTL$C&Cy_$!}KpI()^#U@rar%5zL_co!SPvpjg!1e-g0 zze^AX_jn>J%&*OCQQo=?P?J0gCYJ4yBPf}gKU*eQ+A#PrPe+fX*nk5pt8v)0{{)xc z|2Z^gNR}^#qT2o1>9464W5uV>!C?LJL9bQvFJ4CE{|2;|Ic(LbQc{Yu7{B-bi~~ao z!AW#8bHnsuY{XQG9bMt0H{t!kbimb4oh*i3*=)CY;fPgsyVd@tT&4&qi$&>&ku7sxwW1g92dbl90x=aJ>4S;8(pjEE=OsDPlmQ_w-#a4p|-c#7|Ei zGiEGyCkKo?H`Bt)9iGj1w>mKKWT&=`8fu+hoOEp`Of>3ski5LSA86v>fWqZu+24At z$(&zqO@TH~tx82Ifs(?V?dtVkO~*k&DuL1AmZ1czidtB zcjK0gI6Ai#s`?YFtZ7T^i1v7f1_t0mv4leq{O_DGc)S^()8d!q%h76OH0U#?B0*R+ zUVGHFHtRrP+y|!HTfCql;PVANpS2+Z6mjyA=V&T(qNFxDIjo5xaQA;%fFedx$pk$+ zJ0#5}D?w9Ja@ic7@aOX`LPL;YK-Kr*EEC@5NLIE13X{X|rj?&rNNA%EIZ97lmGJ)V zPqhfAwNNlmmJH1lZO~2m4%j8mRV$c`(r~k^(_=<>MVrXkBx5#R5h-!MI5H~>JhT{v zH%n+7lCN9H_5VbtB`O_IFWeyY;IW|uI{O-sX?5;^(`?DwS$>AGkD>L$#S5eG(Qi`Ab(yX$Os02DzFzZE#B5MPJaAaM6iQTE5on$pJR3G}G-i!trt}9+Lr8 z{m9%S`lcXj#GR8F@i%&r?IE^lp33?3m$`8)9Gtj7!Ubx_biMcH_k_s%*Vsr}&7 zb>H@MFKW9UB9CIGIfN8!Cs72-4kW|Y;V3DeQh4H%C%%(?X+=qX5-A93^G7;^U{PA^ zErdG~%8~9VgD*l_YBH$@)e(fzd^P&co?gHK!aDrw(5vChe@yfp8j>~0ifK5oB*>lN z&k46=2JOn!_iYd9Tj6c6Lw06E##a07g2udblkFs|D6AmthTo1?90WLP;YBG;bqaY# z4>b@0TO7UggZBo;``}uwdc0ger7|;-k|7?MWc%Mn^m&--P*HCoMeH>{Z;8I6ev+FY z=t&2`{?6No^f{6?#bYLpfF7}et$f1R>3y{PZS6%L^@DrzO+&v>#2D5YxJrTJGi=@3 z0LdQ7y889gezFMDGP;If(6f&NA;QUKtV(6FLmI^#&T!rI$Oz@(YHM3tt;?x~)4h|_ zo}|g(NFwc$-K1bx%EI;x)v}@ExS+EU2h&9n{mst8Y z#J|4puTM<}R_hjxI4V^d!T$dK5%895xM}IxJy4igv0blgT>~k!^rCp;10uHeD9)zh z}kV-dvJU# zTDj0W#Q$dBeyx2jq?)gjh1PSiyIaI_nE5xN(EZx~FUpvPH#KgAp2F!JB}6aoJWFq)O7oj$CFSLgXx1WPjR(VZY|f7Bh#%xmMrikfhW3TZvo4%>X1 zOcG$(k4 z)Af<_KaNB}YO&CpyqicyGk3g|2{2jWvs!ttsCYe&=?*S+y*Ybr!|KLKHuu(%N-88( zEA1;)F4%9l@9$LVzEUk#Y0{HeX!+R49!=G$oAeflMqb3ndVP%IhuPFlBbHftwC(!b zN4Slbf9!=SHOXNy9Hm8Oq*Yv18=ZXZuR!OzTQLQ_p1*@35ONtI&Hu38>kWpzSebgm zukL(0Lb0bVVX6N$kx*EOR%(v=$x@}JN7w6w-tKY$6GTk-1}!Eo4i+J>q9^aKO#!9C ziOq^w4Lk>YH@f^m_Z|1%Web)8iKAo2;Ywc<0w8L`ss%ynw8<5HG|5jLDw}omQcR23?GU%Gd<$)4)1(bMKjm zg-N3Ykz2jjbnN5stLJKPtGn5n52P9XW1R*1e2 zeQqG!O*2HF^vjH;_RqfW_rAJLWQ?#`%=#>CTDWpSlZ7|puh;v)>H6}&UD3bS;boOx zR3wl%2RwR5xKtME43)m;${D^s!Gj+vsEqy#`XpN>yr!>eJ@!(==J0bHur%w^ZSkA_^IZYM$V$x~()p(!g+MPBo zH6(%^r+wQmD-t$+38HUiBc=*Pz%cx zB>@Ip(zElswtj)WohOLYYjLq@ay)8SWnlu5-IUetPDFCI$8DFlNSdm$yK2KUA$kc) z*u0noMIc6mN(Oy9^hluJ!wb94q){jRJ)tW$P2b`?(_f}z%wEizfI^={E9O}&$w4ZI z%NZ9{K6=I^QYp*1C#Fm`l5)p;M4mwuHE zDMC&w8bz<;H#Il2oVG_!b7OPE=K@Rsxw!l#rqryd9@FtNNRd?k3O6 zNN#&qLKPFU&Siw^mbI;Cru5?8)dm?k`DJeouiH`i>BlcX5ZDKYhUV&(hf@_Vxj*2! zJ?MO0jl{A5Nux@o5A*GLqUB*HvKWDjvKu)^9UC>gXqL$g3Yo6s2F2^_{H?lS0KA+v z0?ij`q;o5|#)}k#R*Qs}UW_c9Kwh4%9^Rcbg;au|y^L&xuC4^3n9=z0^gGoRYc1== z45^%kH%(kfB=L)~S0e2!2Qj;oL(kQRpi1N^Nu2F42duIT^E6k@X zBR0;U$EUL_Iu)1|-+aTxSI>@b)QdW9H8ja;wxhL}Nb;A#*60;UlR*KDuHMOXkbvpD z3y(o%;?WGTn&;#Cj9pO22*nxU>Zi}Y8!8Erk;O_&FZzdlcs!1v@`w|@IU%&A>bho| zCgChIx`SJKrWz0(Nq@mpz;OcBB5}W{7(l3xk*e$KQf)b?Ydj9NZ ztJYlxANJ&-p=<)edy`fJ6yZbnv;D(`{>ge9;uQZo4}s6FKF`Z^A7Eu-JdhY1xAXcK z7O&^j<&}1Ibq)KV%G#YgFrKV6V-gb=hJm1AF=)Cglq+k^X1zs@4qVzCY%I zrru7b8t&gNV}0=|RjVojv{taQa&mfM=}&rK+wS$8tl)sIpw?(X3QU~X$JgsU+7xde z@eT;~btlxbu6Li4zklYmd{Yw$0JI_}Xk*)y!QpX(1KMjIn|)iZ4ycwQCjVyh_Jt1SlaSF_ay8DhWBt) zwLB+PU~*3wq~7b(Ey`2>xP%66LxKo+nJ9~3J9+JRehN%~UL>t#Lyb)+f;Pjqj^w=0 zPyV$D)+WQW)15J$cqWQ4e+T+A%BVPw6m%OO4U`c_hW3M&FEhd=NNCm6{Vx#kB8J0u z1*McbB}aDHL~Zzb;*R%|{bhx!=*q1~(vl4#yF;iOEtHLhm70!Z4SpS}M{(r;;wGkq z+Z1ucn3BUAlFq6qn}lMsEG1EZvjVZ{wQ=@x~JMVIz^yVi0TGvS-H#fKa<6bH#Ka?OL>n~7m zrJ{bOCMI@MysjYT+(#OXs+4(|TYyX;85S10y?^2JW1iws%sCs2TBuYl?CK&E78VZP z&fxdu^SO8RVLx?2=CFT!njD|p+6Tmo&U@SUkS_#YNaC@0P{@V@>83k%V|0DCUE9r< zSE*fi+S=;@WzJ8r9vv~8zW3)K-S=Bv`=fcvaG0b5@*ooAR=GMRMWA`ApoSvi_f~69 zZFl<%r)8yqg-tK5g%XR$U;D+YU?6w^O8W(c&EoKPSTLK9Uy#Y^u^ zQTR@YEwYXnFS|LNIyh4KhfCPVXoSBRP;1lgDlf+S^pVhz&NIVG_suBmqN14_hl-|s zr8=?Hn~X%Jp+>@lq%=u!r{KJ-jM{<;!bn;80#G^l9Gheac+QT3{6R<;Iee&rt{#U_ z1m0`pN^pNm*7mq4{i$(@ZHDq3`03}V3-J@MFk(FOD8u&~tjQCNAPqKQ@+pN@4aUVg z+PRFy&G%zpeLX!eTem*k-&QNLzjWxfju>#$IAUt8R+wmXJMn6K z7Ao!LWZ3oBnyg#)QZz5RArRGa%`0N%doMZha!l7?P!u|T+cI$C43`inFf=u~se_O;yk+1AS?!^B?O+ zO|On-=6kPZH2e8AYtZtvp2~Y&0=`0%cym6+f&j9!2WJS36W@!<%dJlLTujjSPSlwrh0h<)@ij$2ILdn9JZlDuyd2T)v_`HA! zK^NP7*r6135_PMPs2ItL+5B~Ih1aJn>9FU~P(|Nakms7_6MFW|PkzyvF-T5T3dT=# zwV7NUfa9;-7j+i}L@ncaS36ZCF;=wRBNfamuvB<#dtm*V{>vm{_>ESs)uF4!-?^?k zIh2Tl8fHU9eNzFf2)lT)+-WtVbFYjQI;r{6I5U3)CT2Va#35JWg|%#Ig*77l*qnHQ z0$uB_SS$8PVazzp(Y^5<&OS^>k3u(61rjd}f6%G!>-DreoY^jk;Jn4oc+O*9d{m$T z)xKIC>m1)i?(6U{L+48|y`h5v_>z_*>tfAj9886BD0qZvdV$69Tl<@jmi+*F%kHey zMIXR@e6ImOWjGXr4S|Z)f)9f=6iYDh!S;7b`Cln5(miT!p6!FU&tLdHv)f2BKe%5r z<;ZR{CR7iM>D<-zK7w~Y?&ro%WU@;dr{U>;@+-~p`ML?vG)5MlAdL<><2QHl&8p28 zGB%sPVa=NlE>12kFqUy9=dhnKKCl<=+gW|CjAmoL9e{n}~s_T>Qy-~!T&)h#by=PetZUYU8b+adP1 z`~x4(T)QH5dG28Q?PZ{Mp+y+Ej)42lwgjy8(9N;e8NJFf^BNe_ynT;*^w2|c1t6_0(AF+Dx?37pL@x6lg zTeR&ZG9&YMLic<4$k;L4l4zMl-K#YmnM9WcU7<(vCv{F7YlJ`ISyXwFjW()hDCOk> zNG!}ykUI@B$rK>wnQ4`$+FL`ZJ^cJzIS=b%TH0rq1PO-hpx@Bgnv-lgt1F=z0n!sq z^TO$(u%V{n(@{Tz^LeEpU%n>^P~qXZa4cfFhfJB%uDD&KA;`rVP?_uL{Ee@+P2fEx z)0Z6b5}_5VmtA& z*keE4DIiCoBRe|0Th3%`drNu*y)FU?`vJO0lRGur0`BJ@RBO3J_xJX-?Puk{APq~5 z;mdRO<`C;rC!MHFP#&l6d2*$S{z8Q^hr=#(8a6<@EN9l%RGnzUL+mU(aPHqve0c8v z#w4}nadvUO|NFxK@~yh{$>Nt@IPz?>d8hppu#Ac&t0iMt-|9j8m_n2e{~{ZAI}1oa!}=Ld{JC|-k${+ z4GJxmj&Hf%09XOBr0eUAlNQ!1yOOuSYMLEP8kxFvP+M5cYdr87c2M7ijTUOddFfvM z{6LyN8@^MJXuASZMzBn0SyOV)aX?Qz6tbCEwFaaztOQNx&gsW;UZMRKY;jWDT@Vdc z8Bi9ct(zj*UEY$5ZdDWx zd>W13?TG)G&;TzJzfaAP>$Cu6nz#Y$)t>wbBny|XkCau!IT1ryC(U5Ss zt$X=3%eUu|1DHg$1_NJd)Y~C>y*nr?e)_9H1^#LXu-RzGX5aK;{;(>eqM{^nGZQr< z;eKF^Z2Zu0+YZKLVBw)DPdI7ZAPuG=z8+M$785cD=Xz1;xJbphnPz}K2>hr&_Gr!B(q-<8tyA&{D^+$n)v zQE)$V^Hfy&A-2`#8%aq?iG1aNn6U5crFzns-`~ydm^_ofpbS5VbI7-&<0@=}w6#GT;`LYBfVdy-vOiC^#TpW3`X^s+` zP%^o177bKf(qbZkfi2m5{y&f&mA&7ByRlh>%a}HTZot&j+jd&?XxFfdeZn&=G78iz zJ90!Q|FPwHZ|g`qORsIw#)D@Hdp#mrN*`1@r_v@%L^S4z!uCan>r$^I9=vzS_uC3N zRXwE#NwP4^W(h@VhNVZaX@1XxWrgeSz}i9f-eMctLx=cB5y8O36tH&Q0@1wg!~Ky@ zwE~VB;9!Fx@VLJn7JkrZHvLK?RIFTxaI|C!$Yitb73Y7F_c}2ooRf`CW)`5jyBR?h zF4ywzao5jB*VXCj6GteVu|nI?FJ{ztT0B;3y@B&nMA_XLFr`>wh!8P%a)O*XTh>1r zH0(K-k&)R3K#+M9a1wiyV>ilG(i)f;6)Ye%pY2M@Oi35c zm{DZ0Vh3ZKTHuL4Y$N)HNzr$p1qNn*ooQ-s&m^OT`FA@3)E?2t(4wwsM;VX0ytD^w zcxd2x|JuCif=l3iBhQin55*?4X}MRCEK%x99?&V}Q3{xK03Pw3$&d=@`~HoC7~J9( z=OY$4K0cmTS}MM?SX|7~I~X1KFca&eB?BfZtCSKaBQ3qPVwx+Oz)(P8Pnhx}@ssLF zd(h}}QqG#hyjD+8w8T*X@wn(}CoP)RmRJH;E|f{Kt7T@baN?+FSrsH%HGA9w*f-I_ zRez&^_BQ9gER>_R8Bj`VaZ$#2N`Iz~LZIE_Iu)L(;d6Y~dyrcx=ql=wMYl)S#D!jrrCo8{)Kjn>Zba8qshzd?#yh~1pQCW$Be#j zerOyLE;)YG)$QQas@45HfnV@@+&C+Xv3{vsmF;vAcE1!;O51iFhK>_q3Tygn?#Zd`+CteNRmpWwyYMGpDKzawJTn8_f~fU zEUANQ|8h$b+@WYLpgp|2%Ouj4zA2TMkD=o=4PQty8-=a>7^E5H2EU|;HKU%ySMEEB zppdbPyW#y#k`&WMz3UAh7?20=_cvsyUn0fQ+xXnOdU<60Km^`7qOLC?u|&a=b3#U% zq~2Vw_pUWdf&mrO^WAc{dxCD1Ewy4^r$>rPZj>;f#qa zvwtWaJB-=!1A$b85?tO`>ysRaCJ@E`r;)r%k<%=*T}jQ!MJZ!T(7L9A3bYZh4%!-QV@}dxGg}x5e)xkK<;8rH zGdJyMZD)jWrd_Cv-zLu_Z7_$jfT!1d{G`uM`HqZpNE4Wcq`(Jkc2`e&94I=FO3_;NdCT z@Vx8Puk_Qt-^eX@dUFF}y+!*?vF2}wJt`Wt7BKu@%VdKEqg8zLR`0 zFtBd^*@4a`LxOdFGhRf}ATjLkm1UMg>#1b27K9AG!QR$fIoXM&3a66=<5)aMGXl5c zG~4^`eqeKg$E(Fg^~zOB+WgF^PBg$t2Pfzs<5@vLsLUmB)xpKX z6wnunSo{}uOi->-0-FDMWy*d=l)mSmFnBjY2 zG*PuWH~YmDI6n#Q9U|Wvbkk}P+G_MYsM;cSUS4e!+jK@I9)&!JAm|+g-QRkj{!Qh^ z0xE$-ZXvcel*u5rX;wTLFQ-KuKLePe;vCk~dyD5a!=LIS52sVd%8GrP@;25f1*tkC zc)c+;iNV2MwHU@hEWCi)Z+b+rSiU5&aqW;kB)j*-eVA@=k8m;m%gX}YwSkgh)+5AM zG$V^fmMI$7^G7zff3=nGPwt1ZC;#04&DWq6)Ok)^ILbdl*w5*>U$v}RSb)-pA;d>v z#7p?Wxfx!ijUKEZulMZ&_|tZ>s!mUFs!o9MSc+j!08|>cD*~BpM%2Gc(9BQW*oq?y zT^aNoT8ax3{+9MGXC zTKL|k6G1wv3A0))^W+dr32ccrLILBDI*Fg98U7t`6HxPNX0BLYC-BQooRP&VmUvQt z=pNccv>RJP-}Bzr}!;4jmpKl)*)rDnN~|{a=~0LsW=;Z zTLS$Hq@j={^%*cAeWO^~KXqL`9k+mDxREBM#(flmv?c)~#Iw6;K06{=-bIQrGRmsy z?LCVx+E_h}*;N*rlrgumVMVSkOFV$px&;t?bk~_v$F`Si3n@~^DA2AxWgHRMOG&|g z;MmumVd?|s+TlluMH+;6LnU&mS< zqseL|f_-Bdj*|4>WOigav#G(=?noMsF_XzeR&Y}j+s6V8d~~$Or+qRCpcg${R1prA zEU^o!VaVq4MH|$Hg?v_hBKY)eq0Yg~(o*=36?d=2*Dqq*w$!VW9meb7ZRpPD1Ws znlYSlZyUP>o=ln>4t#Io9rK?9$7~tk8kZDqsXNU3Rg0|%YSr&z#ucKL5)3gt@#R4@ zLwkGwEbgq^f8!ku*U@bh7+(;fohB5{n^8tNxPC(W1@3@E5kj6+*NiiTA3Q8D9HDL3 z6oge9Z|DHIBk^>Q><9neFgd|&r!Pc+aG~qp*Vh+*`-a) z3?gPoW4RxGc!?dYtsZc2a3nfw*vut{8}05Co^?lfN+TlRfS}9a^A{Vvi3axc^uGXy zkbc&`87QznUIdk!^*HqH7Xk7DQm-rMsa4%K*mLU`(BQ5n-;++5VhDHxY+V1Y+(=}W z5u4+7nfT*sfIK4EUp$^3DnkECP}=VfBLlE{*_?O9c=sBLMxz0G>3!q2<+iWcV70+K zZX97s;+r^q8vmxQ;RoAVsW;WuXR!rO&})NyY3_PGrAWn(=T>1uM%+d|Yptj8-y2JL zHYH9@!L08GVlyAQ!x#Pe9shf)vL2O(lDKk<%N@G}bGh(ERz{7fVS>T%58?9dp&c94_%aV^9L_3 zst{h?|0I-xUbYFaJ6V>0=h^f)rp@AX{0!Lskxx=6cDDxoy0JER7O*W^ip}L7ZDa#Qk9qT(&86(sMFT$BhgX;K+sm6; zC6cCi4|B^!z4~{jdz!PX(=G{9MOHq=%m-;&^g*t%P@9RmJ(E|C(*@=fqFVCNuu z-kmLoi7ljrUE@_`^7Nr#@c9f4)mki6EFXIU35T6UP1}VMYp?b1BL~0@hP0YZ4+(c7 zK|mA|9eC!1Yt?3{q|aA!*j)+U?rh&xh!aBMC%J99AeOZ1xdc?u*|kPc!~xCN*5?}c zotJk()#MP9Sjb$rXcW$jRnEy_W=5zMBFG;f<@V+A+WvZi9h8E&5rDYyBKX(qdZOj= zU#2}^eQe+JUp;e8iLVGdrUU~me0y6zbzC|=z}n0XtI0ZW>*1{ZX>{!1C2%Sh_ya95`|94M+N z;Fx{6T2YC)_nh1sa7_J$Uk5s7k)nT;nyQ^QaQq(0(PK~@_ zY}%rLvf1bomvRXav;*83R<|Ebfo=@Zx)6n>=G-3}%cbhR6n3^kEhqoB3)_{PZ-i9}T{{ z+b>o}l2n3LvlR|V$!4y+z(v`EhV%y_r0{zipL1RLnyf(&_Mw%aj*ZnuOCT1=w(O$r z`ZEL+T5oh|zymmEYV{`3{00-_6sb}zm#^nTynx?RBA1Dbf>d%&}-E^X4BZUT)SQ9j(UZG?s1GTZ0}R$c#kL(NDW zl_yCpj6afkBo)rypiM^mggx|Agd;dYv+L~71QOS+d7bonM^Tvzg)I&cj|+*XScYJo z0y?)2DFX)q?oclLMIIuY$C5gjcCk*P1JvbghY~~pP!yDMk{Od z_9wM{xCdazGiVxp3DvKQ(i!x@J8z2Vw?CX>E^hAipknmsXc<09CNG(A=nm7Von%bi zULDMG`R6e?zCRcra!!$CqP(JFp{Fa7pc=U4;;$YcoMCq`{SEMX3aYCS=1S#;`omGZ zKegsX9!In>8cuESx}DSxHNa$TIr7@U1fzaShG#I!4rYMKLAFr^6H@z)ru*%C@6+w^ zB_Pg0%g9K5NAl&%9s0;5Ztq0W57PTSN;Nb$7b;QZ=jVS))po%?dw3XPbR2|}&}_Ay zC3?8KyVE;ftXgg5b~<1HBD0YIa?rm!;mkmY3B{pR;~?C8oOHb?ruT*^mCFqQU@h>i zOQh2Sgp_+oMSl3AOjz>c^ZVe*@mzkm?B8gR1+)~zzk=_r&&+(9&7qdzrRK?vV*o>5 zuSb7F-)U^9E5M#{lwi-FU}9ie?O>U}OBD)JNl{TIg)&VbNV|W{IxHzlafBh~$6+pD zh)ka^&2?&_n%O7&_iv!$9UBEr<=<|4VP^8+H%3MonO8ZLJZA%^q|XfF#!8V{utFdDT;~xY}&K zR}cw6Q^AY7hRmp+grb#YIb~g4|Is3Nf7E>s7LCNv_i|(=lQz3uD!GUeRL@jaRxYck zm__7(DyyvN``FR(+C}6G=;;wmk}{s3pU%Spb;ueOxCO6gWCU5$ zafjn$x9PpyVX3;Cj?`PuRUA~1bM&{yz#j4g7pz>HTtcyBlZdpo(>e=HSSl_zMFZGj ztLF=0|8`%fA$vPu3WMIB&xZhR_6Oylr+#cSv#ytxp)qa5I{(9^0n!Omm(#+C*tb90 z9Wi`$th97}Ci)tWX=zzKZL<#7cG z@x_*ukTo5EMQwdVXMm7PVPa@s zaCqJMgJ)KAzWdOyfT=3x+i{mq^!cVIeZ)WlXaJ;*LIzLCczPO_)miM8nhx%I&7d1- zJnaFx=oGyqu&JE~)3u0Tg4&zOBkN4OrMSOuxchPMVIkkywUrZ(Sjf zpc6kbc?9v3(kZF_ZEAX5jxg4ra}P6DV|VlIfyziBCn3|p_}d# zz6<{(@Yq*PLwzh5VLQx67gXbhl zE>j*4xmEYxWSGQFta37vc#H~j@Xeln?!Gx1V9~&8Gg*I^&E|^seH!H7oKC8VtE9H-~Gq$%c%2h@^gvvpq}A|KWTvH>5>Sg+z)((o33;KvTi%+F0Q zGJ1Cei7KkmBi#A;N;a#tk6a~z`z|sts$=z~V5pPQ$_(2yjx}J8d>qI=VF(tKIxk9Q z|GR|zoQ+wpUZ6zx1yb#v%h}24zu<<|+MqMw#D!P8L^lyqJ%`g4%L!xjGAEKqtPT3# zx$ry<6T2Od^UD{#=AV|Ho}T_^>wjd#O@Vti(ipX|eZ~7j1hkcUWtNH{Kjh*YcO3lIv1FUK{!F|TN-RkCTZg$Msa=BmF1Mwf z+;ay<&2JKjH5Q9NO6<#1leiA}WGJt&UzuOy@KM2&g{2847rx5#%PDpQC!q*tig@Zv?(l$bTdfd;hqhOE+qbxa@W-r zWco%96PZ~88aU&Pqto&fL{|_G7Wip~_A`DI!^YPK?I_4F#VY#BJKSGXhM8fq;#0D+ z01c;Q%;X%aa}v+<^V3C#XJ;%sfU%AiD(w%8sTm9h;aFH$9u%p!EXF^dW&?5V;nbxBq-4qgR>;-@?Wx!0By%+qB)ba%KP65rIi0 zqjZ7v5TH&SS$o)_+MFK@cBakxc{-zM9t{Bp~b?+ z_SqA%o?o!wEV9@DhFmVE|6{k{0Tc@YG9Ru#?Q+uHTDrK6354H(ecL!@k~D<$mMDimMKYN20UnZ+g*s98 z1J=Q@c1X-YGY0)60fP~tiFY}a!FD*RBookV2;P#{|0Kabp}>;FiJi%;aZ`tPwVsN! z*8I$3vZIs5;2Y;tL8O}?Bsz6Ol@keU5NZ#l>1Zf|?G^yP9En;k4>7mM6c<-1aHb1x zXKJ*{#5(zrMTmVSmWwB7O?R$>C@cEiI$Zs>DlN&f8-{bMK}3Q2WK)besL%-qefuhb zDqLU=>^+eWi*rQnFAF@>;a~m{|MR&x?cUn$dMu2>>6(cLY76w{J4?ADsDLEBbJ$feODb_z0!mu`cLD4NYbyAF7)}L@P2fv*kSp#?h*kse&kHL2{ zyf)!^y_w^1{1?UjC%(!6HKiL^sjGDyLIAMCSKzhLoGEnb0e}ctP+R-y(W{3(>qjvP za13@Y5B4V;jj$ix2e2z*>rJP)_A`F#ei(j#_H!Nk5kEm+6&*nZ5oPhXqdaPDXa%_q zWRd>%{zwRTkxXC6>kahud?MD@f%dPirt><_@w@;KClC%s$DmR3`%oQU9xltODn*s^ z!Nx`fe;jTuC6db;ymxvp*E*8xcpQr2aeBs;8Gl} zzIvA^3ZSbOc*Nj?0}CE<7G%K;E@vvwNP))Zm>*{u+pdXDHW*=)@D`<5-q9q()Q6U} z=eBT9Mq-BZx~W!ZPcU-eX)%&0(?@Y!)m zcVQ7#8#Hw8a=XTY&%>+5L^b9S_cCe$_tBaW0S#WP61L1dQp684`Ix`!ow!<6#cU*h zmNJr%P|hA0Lgbt2koI^iY1-7!1WnOD@UmchO6uqzd)&Bmpa)(h{>j$9G#=7=oHrm{ zbu+||iskki`p=#NHh@dhn2f2&rCJv8v>D5S^5;DEY}0=b2ZFLscxbVq{i6|NKJ5u23G7 zk%0qDAPC^Yge)y#EiCXMgw5Bm@bLP}6-(`I4&?FpUErFM_}rI>!HC7MGdZ%nen^-X zHZ@@YmEF^DI5*AHiTx<_NpEm^i)hlxW#{9o1A!^vQTRE$5Amk+t0{7bGj1?Esc7rC zIV5@HhYTVUX?OW%Y*|og{@XVpW>Tk532}iWwG9ou8=A!C5ewV9-9>-3%xM-ErN#7F z-m`$K<~eE_hx1_54XtAqpySQD#tmnMr7R%KUkxU6{7snGq{KM`a^=JfCA^oD*JFSt ziih%x9b-mHv;omYPe+;AfkJ@Dujh{$$Qon{VlQ>o^Rps(jPEb*R}%a^cNX3D?d7ew za%PbSN=Qx5Aoqb%t;%XRUByoS z?t(|s-VP&FHK!{vriiwz!V8acC*Ey651;@qInix7?{VN$WkBbl0h%=hC(|T;QsV+Bv_RU!G}w2~7bc z;(KxF_03Q)y{bG>Xet{XNJU>drtM+A{iHzCTVq4O+fpe(0T0{-fg=2=SBDiZqsC)=Msu zf=i`K=sjo_|LwYk_9Tj}m#>*>`D$*;lXa6dBqNmU{e*Xxo-19UTzA#@Lz!yAnj61y z4*Kw?NMRb0*RRO$y@tPJ4X_=ot1`&1^^tZWqU0OqB~+86N52);EfpPHlCCh2Dkp<^ z@nV4L3A?{Aif3voa|UdMrMWcrB=lD%-I7RO4#BK>{%k;sB4KBW!-vK@UB6sMI^?wM zt}=?9x@xuGU710M5h(-f{8TZULfoM@yc&WngGqec2uthJ^=TXP8{>KEqV@>XnL)%_ zo`GNLpjq{7R{}8(!ZV0 zoTgzW;!or1G3nmn1>-ifE4sYwh(z3v=@bD4#-Q-oL%f@+NEhVrb>C+jG6OnjXkp;h%;DZM?562V!|Lz5_8G|(*PRLfzvi`*v1gRpc6{b>& zphRW)BSpHq+yL9ht@#k=QSA_Mn%9rSz=?|sBiI%~nFuG!Bsm7mFa**#A$W+SdNVqRavCRRP&yI& zrFGY6)+;uq{HZEv)WnO7hdw#r@vFRw-Nlz!}+QVDE=${Vj0f zuTu;>)ISJjUT{(fQhK`|u{JC0G=V!@e=2D!%qjZ)UK&(%MAM3RN`Lqb$b~X%&Thy5 zYw4C8Ax6Zm^zK7TsVRA9le2$sp^)JX&P=;@_(Z)a{pcz-l@;otrX1i9E(ubfZrJTs z4#ErH(vc(!5l_N=iC#k!>FvgAD+Ddx{XLGyTH#~i=jh?XGnFeo-6;kSO{C?Cp!d*v zzS5zDl?@CP@rph;@U+I4$@>oyyDX_Y`8gy|c^=(SJf#lF~6RrsxJQM${C)&?A zRxMZ?gZxAkCa>!{#Xy0QRn|%0Ke0?+m_28dB?NNrggz7>Pf=<`0r0s;`v}&?^E@;Y#yc`T6d2 zm)|lIvPI{{+ohl@5y?yUh_F6C1~sfNWxIs3odTMk2iteJbHi-Wh>6ga5MW!>M~DAV z4Ml}KoaEV%GSlOVOmk_!rz9(yD@L(0pxKKZ`7FDm8|`-_*z(f|O*9)Sf_b6rf?RI0 z1YO;^6iR{$s0?t&QWfz+$|=8aTxgCr5K@xg5F2dnmN&3?;Dy;^!W?cpLm6#7ZBzG~ zqIw7Jm*h2iLHJQ0@^m4H;UNl{`ZC}2=Y%K>@xwS=l3W zQ;^z0;jeLtS3$>d5@MVs@A26cHCf@k5D6i -vLv+~?{x?^`7fB_snM4a?#|5r} zwi-df+WgbveOkp%vcuHgk%%fLqFld1{`}3^UY;o5boQ_)Ra^CJYvk9_W}-eDQaj;s zsw2|kISf75<-wFGiz*CC7S-y|`)*Ly>er`8{iG6{|r7g?fvAe#;&EzLpH-Lc!U3~rd<=~@F!1y;0(xEk_x0weU{Awj@he)Ni4kQc<6*H2zhi|25*Nri|kQfFmFH>Sq zs1hkGQH~4uUZ95*iBwHER>n4s%=+dwl+DYcev)MVA*+PC3tLI0Q*A^P01rgMXb%o& z8ed*!+k%4lSsMDgBZR5IinsV*#>)r{6Sx?`PI=jdM;*fe2BiWy8*8;oG2d)bb zlSR0w(k+&oOU9x>XqDev;Be<;A?~9dD3yq~n>L7gDti*R3|##jafC%55wVkK%g)A& zj0o}Zd)GTW6sfWN;lb}c`uh3+;X=9nHpv#-Gzh!7maki~k1NFdUsI9BcTsmE+7rZD zDAj1Rq1_Zh5VMt#1rbg0u3=#0JxRHd)vARP#jsM5t{=U_9|Ep0C^xddH-a-I%)esP ze8(UX(qJvj z2CcC~qMRSZvgk`4D{KySp4ALhcdVsAGG_M(hwDSQ&63UAg8VH0Rd|t?;Fz3|ISvQh z-64A-g((aFGTU-2mVlmS6@$~E7A_>DwYudV?S#+|4tJe$6JlsEDuZLo8pt5oXRJ1y zQqTZ-w=jw&)t~rMDe>c%@ll62%E9+f@2?WVu<=H#%TfwQ>slj{<(BEJC{v-B;^m@2 zY}OTFE7v>4@=+LAq{o;D)+PgJ-y~)RN7K90y!EbAj1Hy2t7}qY+VqQ!CuLuhMD6O) zN&B1Y92+wtl$Ps( z0aZ3yuq!q9TtqLHC-v9R`8&tUut?QOF?7boTO-{hS-I~t97G!n1k|MF$3uD`F*W0wamR& z4i++OKKP7H2VN?Wtp+K>uvuE>h<`EY(^l3oNG`QMqlm)9TOX2GkvX8~Pw@FCMSK&2 z7!GF=6|5vI-SP@Sq2(H$z3NZ|2+Lx{GI-4CZyEhT@epCmb%uf2tgEvMjs?TN)FF^5 zg}k8fI8Fa<2z7)?9_rI7P$BlN8^JcB5{gvm&O)>sK%k~f(Uv=WTan%^Dk*kD(d?u% zz*<{PG&H~%C1!cQgf7CGVTu*dENkrjyeBu*kQ`|Lb}D6wm_mI#T7;M+$_vB8I3Oz2 z6F7&-Fc=?vxS5wf4e9E}LjK+OzFVq&-3P6af-Ey0QK0QrVmfzk4e`IKenTb zK2ju=iJK`Jn?i1*DF%LXUu(XCY!NUkt0_5Hwd zsJ_ghxzG~F(Z3xJ9?_Rir9Xbq((OF4$n9!YZCJ6a2PR+E_|u5L&B#7EwXje$R$HW( zq^vT-zG=X{WQXm)`rMxom06vH`F3a^OJ!r0*=ezDZee9YC>O1# zg&EUBV(!x^;kBTB1P8baPK63z zg_kDJxRe(Q9}=ICoJd^o^^`h8v!3SgD$R-tUiP29_cvJ8{$Ssp93Tv83}RC{wt$z8 z%eA;L3bj=-dsqYH3+MOf<7YchF-aIMv`V)TmO}2MHF6}l2WTR0zwg1c%JG4B0HI@L zCv1Qa1-O1|#i`r*kd%qFGLse8&w}5nTFEB$;&f%QV5MV9PpwDk=!u_}#Cx6lY zZ-@n~Y|~mu^Vy~U*&yE$9p@gyuUw%U+|0}QxHo_n@Q7? zhZdg{lzHTiSF;i#t)*WuCq3dlzGNUOuSGrPKIlxm&9`g7mXCbSQ{00tm*)SMg(FKj zHPZW*C?v7)QUtGJWEWpIV23sjE{v@i(UNbdx;)<`jpq1ox@j5f(k1qH7-LXjYyh|* znhR1+e`Kdw@%P*BF{rZx`K( z;62(k#=~TLQmUW0fk}VTEGoWcpU&WVp7J7>J)Vo6g&MMh)R~6Q$7HE)|BoWCeP#nu z1r>q~k)Hs3dGHUURhN~a$r&dcK{UYdbA0&mB@!8d59embdB=Q1gSl*wQYfL? z?^cYD#B4TS8}=7`xhO?L7TOXLki&4cK&uw1Kl^envtch35WM+=jg#0zNx!tg`D!f~ z&KGpFTxq);E+@9RhwX+Gg%p!pRyK;qOK-T9kn;qx?Rs_j_|cO0rJyZraLzmE=|X@t z8yh=&9xx6Dl5Kndp6P$?ecz_h<;^E9yA}mIGH9hNwL1m6f)YX>IO|98MNqSMH2ie0 zr8(SQ5QS`IBu|(W#hk}!*+(HIx+XZaf<%j#fQbMl7Uz3Y>_;F9wS|uMNwF8)$#k8w zn4svG0E*x{&Mp*u-L}-IpJvfb-Ob8&U}1d*I?!eynvF*#eceEj=oq;06V+&;n+7TG zWYRaa8>3oqGY_M<0EJl79=ogv@3m5JxX?mdlXFN4{)_tK+)j8XlR8M{bLK*)=dU`d z)EcwV6?@AW!dxFb&{iuc&yZn&gGDoD)eWIwit5c?PZ-*K)b%FIo z-{=bu9LXI6FLZdo70GQN8{l=uBF#DSxrNeMu zskjx>&Eo=z(#mGH&b!!XZHu=KwbPBMx=3FzthwBC=%>rjmu%{j zxmI4!2PI8_aa9!-Bve{Vn7A6(2 zC8Vw$zHe45iz>e*vU!xo0~8cxxNy&#tI29(1mm}UD%U5F(<=1-JpzKC)+N%5tb$6M zwMY?l#RED5X0`e;bS=dgqLR$k-_GNT1iy>cP+54sDt-s)CB=$K3xvsj_JfI1t+oF> zFq3!p$3CacFS7?t?{Fyu67n;(a<^A3l7vDXE%p?hMAo#PcAdlM!H|L{*LY9K;U&S6uNMFrs=Jn1R!9) z9wEL1$6O0uz|Gg$sTb1xy`6ubaf%IdBtbDn(Wki)Zo1>MKjE}X|0XM2TK?gSZ~jiU zx4!hbK&(6r?lN0oVk7`lY;``L_3@}z1}4>Inj{rjuL$~iL5BR<6ik2hPZ6A(o!!LD zd4K}U$nOc38+Qr)*Yva!%q?i9YwyaFZyIy<^1svn203bJ_x(Uhqg)^p4}!sx7Ak3n z;*1Wg7;LOYGfE{$fO9FLJ{_cDy_BBOtctq8rI*mA!n%?SYXjGWT(V#}l;)(C9n^@^ zErHT}>K{-QsvajR$_Yk$`1%uKQI|JB)(h4le-V;q(Y&}Wkwhw%Po(+hG+Dd(m6E!D zo4@tgzPSjxuw{OnJ=A=Ch`vJ}pFFixA%+shEVL@-gw=k`J6y&y zig8~z_;9Ae-Q#z)ozeqT^JQK1;?1DbD~00XNyVcVDqX%?GMA1VGDd-fG-+AuwC2(LF`lk zpUxb@FiDT8>&ke!F)8WalFC$8sh|1CNa_E}C$G8s@Hb)`N@v!&?Wic(i;71lA;nD0 zyczsRme?EpokS^RNunZ3svrd=<=(lyJmzqkVjp+Xfl_caF1ejOG%^%R;WG>kOK_?x zrqAn5$tzpN8F8tIGfXcvBpQ%Nw#|Mfi=C+HL7@kV!WvLEy@FaQ`H5jD#EB*yaq;ur zkZs>ek*5=3M=Z+2R<1$RRJ_s^it~>r&r^7STZ6T}X#$6FE+3{hw>j$%KIg*I@9kTM zkSPYEyd38l4;R!qawRaHCU0|i0%Y9-@i|amhd|5_1U%33+rYdQZ%rDJ}Ps zZUxlt2S$_Stj>`90*{y{V^?auQA@vj#a-lxswdGrxP86+Z>tdMK{bqPb{c|dw|lj)3}098{`6G^+32R%R$FA-K*d?sWY zVG1B4Kp)9CKc(b1oIXi=RaI>Iv1uggj{R2PXraSknZRS1F92`gX3;OD;K=R!$;Wen z*!XcJ8P*fqbyU^{93c6(HhTg-Hpx%V&!mBZ%F39<@;T9;Sj}=PE9V6$1C zhy>?F&{*MjpbAq|uLud>Lh4tN1FeLrwXuLTDmj`}1{KVilPgjJR!0qjlqARYzZLRK zWO-*5SqzzUDg+K3Iq6!-!wj_!s|nfg_SP`t1W<=vJNIbeYHqAso;4%BD7zZlU-8d- zdGjNV{o=X;7h8{XQ`hq(E=ZXGCEz|?aC*u!8t;LmCZd&lf;^Glw0wG%{I_KYJZb`e{20xHYHU?YVhx;{= zNCAbPfe&m{`I8xR!+|{&GPo+|jcU2t*3)s-&`ylNS-zee58xB|4Io+jK08O{mA(C8 z31CceIcMEtj9rl;A*emIj!Tu_LIa%T1QGO`8%_@oN$j=;aP|8KWiv8)Kua4=fz_+K zf82kUmC*uc5}Q;u%lOL<4%A0GtU(IQS zaKt0*MdK1qCd8Fw%5Qjgycuy#!cSCSGiA*JLr4$xs^oDXL@fWh;V>p91)Ww{( zZOVhZk$UN4a)7tg>V3JWbeGYPJlPK|a<>`}#-j>addo~dE0>XK0}atlQi7=D?$d~> zo|wj}zPG&So5xn>koJJG$;%F7IH6cuk0B*h6J)^ZS%+j<>(p#~Y4wMWP2>(q&<#rI z4lWC0D8l^GYJ&fS-W80yJbzvmA3@E1&7`7;mkSC?I8UJ))%yYx2^l#csc>RpBo;|~ z`2*zqPvw2n7-QtU^3pvN2KgoG=*a3LujF<$LPfrBzy09wa#JGU>@hcvXU&izGA=9K z?hm>acke8GnV2yN9YSw;e|t7;Bi~c(88P@$rPY7{*dHZ?YX+KuyOP05BQc!pG@L(_ z<0D}pn%hEKWpBNuGCP+(yRw({P{x1I%m!~4(W9{5OywscA|N4^QEj*bllj#P|9ye@ zHBSB~rBM3N2)4RToF=+<&Km*=Z2pw?T80N<;X|`xp&$}&ZMwJ&3{jSuo2Fk&Zo$=G z?-d3b(o!t3&va|m(ijlRFC3?)`?MQ&M{a>R=tFc8pHMO2WDdxfoR+}@AS{^0H7FP^ zCCRYUKt8GuXq39KXj2k%rcof6OgDx@LOUkDBmPvEF)sPLj$n^s%v+mC_w_dlrq(OpKrluk;OJ^f) zRA80m&Swz@J3s1@dhQ$AgPTL;k4$%9a4?MLKh_*^Q#^5<{OBTf)BE$bLyrPbY!eI$0um$Dyb8~n^ zM1ugjiU~b5yU#}NQjBq|>I2lc%#QGZz16GaDcHvo`oAM380K8?>$jSO#U$3Sc*`)r zKhCM^50qDR2B$Au`K&0#A(DbA0NK#Ti4Q52;quL4xKETw1vK8@zdJRakB;A$YKyi> zMuS_@m>|3uT8e^MoP>EVd2|UM+@m;{rQOuv%>pUlO-Gjbne&c{^H+Epo2_+9J?xY3 zBR#6vmc_W_im|V`=hm1qqagon;*++Lx_f`c;TPUh>RFz6M*XD}nqkZGN7%afgonY& z86x3=6GlzjV2k+xioJ76rUebX5W1ax+V3bE@>lIus_u+`riYnaF8Zb)S@HbBrt;WE z#B(EG80MZH7zFNiK-k68lFP{{{~6jb*RQi43^WLHH?V4k%v`D8Hd_VPm4nmEhuup; zu{XJ&o3;&e)uRpimRu^%*CLG8{igoXn_ZizubQD#L8O|BYL^?$ZkA9R{5*xc8cxQl z$58c^kA*hdjFWi#{J}BD7s;MoO1%>nCjJ6SEP?D;qwU%e@%zTh?S{efjU~T>4l)98 zZG8Z|S10*!P>Y_#?DgUC*7uw&PZfUb~Ypfl^MC zVV|%h5d#Rv0#P{FY|ksHmwlhNk{Ff#0%=k)de`{k)>eGZbvN?Ljypb$<_j{JR3=jU zZ!XXW0Fv#H`w9kR)-`$W%7*|DBGz&zI&@ZX^SZFCAvPa<@9^qyk?JfBo+_m#z z;`f0BLC&<_yGp;veswiJ?4MHOpi;5?C7js)8!g&|7Y_|ro_esL6yr=X8B`j7oQShA z!zSk_o+v0Hjdd25$G2JVY1kXg@2xnL5Val6XD~pdx2SP+H=UPe;#B*59RTOm<%BM z$pnGp2N%mYyVK});GatoX=S-ZZlk^xU%a$=#^01khFtw=6TRL0jwJhhDJn11a+K5~ zo)A{C*u^qZiWPQeo?m`6YmjUjgOKwJe5{;O*g#LP>%}H1pch2N)vceD>LjTrBTpGK z{P!nqvC$@@OCMl;DBM<7HqPo97qfQlNzZ|^hXm*vF^Cs1AcqYO4h}2-0ALU_^kQJv z!059#2E5zme*G?;3J6=-&f+)z`f-f;_*FM;c=L4LHB_|>VN+1T1i1dp*cz9ACFTFQ zA~D5bM-*uK4^dTJy#PSko&NplVPDV*koP)Lc00%mv44NPSGOgvl5s{W{f(k@g>{!C zmX<&aiJa$5!GGJ>a-AO}Oax?geG{iUs;2Ve64j;vjmU-F`<)%XVpfua(parMa9a#V z92{OYR|_9Rm}+1?IC%UbUM=@j$waBL>RIH5MezyXm&F~*jwu~j8Y(;$JuCiw;=x?ZQ{xG!=+rT`({cV_qYZjC3|aeDh!9iToss{P1VRwi~sHHyYFB zJ+)!e_<5ot#+=Hab~(UOZl@%2q1c@NI- zb*$rIM&MN$NFkL6f6jb?mLvh@G26s%Tf`2xZB;I@1eR!#2&9GCN;d#Ye!0i!^C?B& zo4fNOh%r}%RuwHN*Qq4*$6x!BQjADsb5D{m-#R`%>J*!jBJoMCpK1&-8MdwP0w%O83?_hwHbc}b_|5<>oxcV*=8v^rAb!N}K3Wk0M&)G#`j;-TywIM$x zLr+FJZGUS=glKEy{wPAla3lsxuZ&-CNlzYWN&l!@$Pnn#Eg;zQCK6^J#36C5aF<`( zfL7k~wE$$k(*yc@SkTuDuUDuxS1{JGhKHM4_SE=t*r@}Y)ki_raf!KYui5Huo9m4z zy1@o;-B&c1T%j#XE~J9M-+Uo8l}@m7+T>>%_Ir_U(oaoEy;EBOms(`&uXFR+4#yN? zYRj5Fs;cax{ej*{jW)(^ZW`JXjrFjF-0CJ#Msz zLU%1ar~D+QDdtfrrA3Pligl%afJdim3~O0NvmLc#9=~3GbEQ9<5=DT7>gSKg1mr8^ zSz_!3^h|qcp6OW14$uq$zWlFA?2cEpss9_AwY4?5Pg!|6kcmmKeUP+5B&$q<6ceD} zIWgE(vrLg8h+u1N4v8Qhy`slZ+EKDtys<*R`P`WFehCHFe}6)|eL z5K9^d6Rp2&-XK z*sfYAz&{XtWinyTK3oo3XkD4IMD>RT+lnTs6?ut&?Mv-JemGh2MrFO)K~vl1N!k1< z=RV?oRZwM&NSxuyUTKjId3FXXb=XMK{`?0oj${3VFSw-Db(0#|H&8XMN@+PD$CQ_8 z=AP!M1s^fMMZWsLt1Ho<#5J%v5ZVyPh?{d%WMZqVVv!UAtu_Y-2N(Ks(gb_8r_`gg zI~zp2=K`>LLB{X`Ju}&=oSST%oW>*56t?-~*jMhGrJ{OcN^(Rs{=!T~PA{jhg?}MlKkuNL*_hDWa;B!^t1!clFBp!;E(-GCKlr5LtV!733KDpl|c$s9xp?>M;l`$*PnBpGZn-tbe#zl+<D#y;XzUw7UQC~M3Pc>+$@nFxgLd7F^Y>0(Mm>8RHIPk8u zK!!9gY!PLO2duc^7j)(u`DD=1?fn7=_=##4OLPf0OBZQVIJF>*!pgA+UhRFqq8rnEr#P z29xvaIKEE@8e{6$^c@b_JnpbCvYqk4hF)}(Fo}wY3_&Ta$Rrl8nOWV}z>yz4i=HxR zF{FgPO4-9Q`@5aC@n=Evu7!AQZjLuXPfn5M9xi>g^g0~2D1(~pzO|)wUr0TInH%!( zb2K|)1U!!2zaR7DC$Hn)7*`M*Q08JcJnYsUS64HEukvkLwA8eE`=K}1l)ppGJ9mb? z)*>d3S=VIABvnXkMp$x-&-yC1lsn^_okg+9O~?R z8@(T)&)0?lF7v_71E~;G&$kXKiWqZo-zpTkG}26F^uNZS+Rn8qS6A2n1Z++dd?8tF zE=Is~2S@=nyIqo1w7mTUOt)?)i!q6zh4Nj|h*ckNtXQULJbR;~#UN zwq!7LO|0H7kIf6))%>p%cd$s@UEJ8-#X&*LHo#U7wpISby=rOaklmrsrw5ZHG1pAx zcA^Bv?Lq9;^PAT>@4Rx^U&B7W*{hv*zz~rK5;-}!@oXe}+i*tgu1Pt})onc*4jz&_ z(j!XSr)ESc7UU!Ap{VZ!kWbW@@qY$>DXl(*aZu@;<1xr6{nLhL?V?#<7ZGe;F|8PU!35_8c78nC*n>8J`*0t%5V=ee*Lq=BpKqvZv64ZxrbgaU6x1p7Vc}#olrN@RG_?$ z=NGdGhgjWLJ>m%}1q2vzjqTr^#u^KcICrDA3Jg_3{SIH!nklf{51=2<({8HOYHu-f zEqbakW;3g>fx9EI08x>Wl%>}}W;DRDZwKG6`QEQx@XNT0H54ZD!;XH7_weullpj?R z^Nl4|2=*kDdvY($K(V5>sGGR|_tpLm172RXK=T}Tn2o2qrm4<%uwjH{_zX_FkC07$ zJsZ$N*2I0RC%ut}kEEeSl%m%PQDw8Z*+!3v&i{JI!)!SXlFb;l_5j*q5W3ueT2G*c zM6^cZX2>%nB;=#p4a(8n#D>qN{kgX2x0 zf72D)+p%(^e@Ftl*59kplDn}}^!>`pAV9|^^3I;B8oi9`z)b1m0R8W8R><)}MbDb+ zM%2en_(Qo2VA?gzjBFNIZyZ7>R&c*2_da^f7N-9aLR;`%MMGr*7`XL!OURD@DHLaKW@m7;CMHXTu+ozEW5L#h zWX8qA<%kDCowtt-7ChVqM?e`G8|a03m*32cK`-& zp=&Qt@N8_ea_5v&^=Coq|D7~?W=%AxgO7Yef*bz-9TPUE)ky$?nPqiLclD4 zfX@4J(Z}WZz;vY=q;L`U;bpwY0G^`;Jz~E~OZ@~S|Mtu-?T#csDzZQv9UTKAfut#G z>QB?jA6brgQk8Hto**=?9~UGaqCB0ShefIUK;`f6L?s}_f&O7z2i&UQQRfMJ zaf&UxadGyhqaYhUx1<-R3H}hkC$swW+_YT=%q(YVlp)Q{P0!I7S_k|2X_j_Y;YgVP zPsGPOQlsN1JCHNg-&_IK3cqd(d-JuEoe0%KOrM3p$PN%7>mE6hpN@1B)`L`p! zzeG;acBM05(2Y3iK%j-F^wYEqz3RkwO3quOmslq!fikWW1Sxjp{M}|_W%n27LuWXr zV!?xSu$(j@e#Ioz^%csnoIM+cGRjE{ymYSjX&s742b%;5YSB@B9Xx>x`7sSQ?|ka} z7b7JmBOqA^GO#TG5d#frIFIEjtFPJgjEAYSn5b*@9wA)#EyiCCt{Yp{$>G?)e3A?7 zVv{Mg!=-RV$smk$(XIUc*G%UYQRrl%SbB6|sl07qxX34#pv^@YaGB?(Dx-X7Z&k7a zQ%wsSo2c0OjtgAP1w${WrZ07_6GSgVJ}399><{3p<(O1uBxa#S);@RSfr|bq~(ZoiH&UzDTH*y&EV)GU&DA0nU5mUF6kW zDGdn707y5l8A_nh6Td`O677mCMqS?>8Ymp=k4$6DiZdlkVLjFfdA!~aU~O&@4*^dU zru?2Jq^X{tEormb3=X)V`+)ms^;7zsM(cMPO4-z3RJnRAyADiP6lTs4CV=1PKOg(Y zj!6G$I)!X!P*dTD@m3KQjAlRvqg~>``!5}3X}c6qSzfSJcYQsnWxdAz;Z{HV4E=A+ zFC&Q9>&1SU%AViwsiNEXDPQ&77x_*+S_n&aE)<^NLru8V23h8%n|x0g z%ox5Qans|^TL}8OST>0<5VId8t_(W+S$JtpTS^kSKyBilb<+j`;nl*DeYPIP!n)3E z%Rq^J^jSzqDmJz{)HfgM>(eFB@Ws^GN+jTQta+2iRrYodEh`af$ZWs1yVvY+XZq`i zD439;tw$8<1pxxa6n^D7db^a>ZVT;JXtyH#kvOqIiG?Edq-neW-t$+Ljd!%k0pHw| zTak@#EU$!xN<4YSPleKxjV{y}-YaS%?}x2W7ic;{{0&vL0u;EgtYwH3SO+!!PfviNpBstK}U3%u;yq)j&85=HFnP`&SF zatvRuk1s3BEBnK-Qw)J)dn$)BK0ZEy0sPkRA08+Qw{0uh`vIO9VO8++wo9+!{c8Xe zX#=DZbiy**@h+6GEtizJWY5|-U;5CFJ}XX3TkaK-;-nACN<$k5~BRefF{g?C2Xlc9gZ^K-irM{%4wRY(F@w-ZJ}H`PvkcIV$l4aFxr`NeVwh z5rXB*Q0E*DgkQ2w{Wz3$vsjto(mR6}7yPs@8d=75Dk+1IHB%mk!_Q_%QJLNh6c>lk zCqa@@ps1L7>@jiN6ZR|X+HD2mxrl2@M8FiutW*bEm=ZolKr2bYXF>f%*3MG%Z7p8B zEoG_45wVdxSA`9RsR0D9FZVtI(XhmxlR`|A60lh#ggmpIsu3^&NKHz?@hsT=3WRMz z5c1Zwr{nv;|K2+E{S@LCh?1IZ9M5x)`v=UQ;4ywlwgks8 z4#6?Y%UY5&%Iy5TyK2Pjm*-JXHLepX;vV8f?A^}sW^fR4nOxC1uP5dY_Yy=sqpp?x zy%9J@>#HrPd>$N>a+#sY$!fEl^W^4xtH~OZN0*=pz8|Om+2r}}xnF?Qj1xjQUX)Ip zmm)TYG8(J0l2FB2P2TGYxfcS9K1H|?gAta}&g2yv>16#T@rte_I^pNP>l$Ge+Qkd1 zXhiG|l>K9dfv)t2jB}X`^OHK1B2`EdK*)-s{T*4{& zi_vhTN@a~G-Rh(c>juRnl^VwK2VLJFJDfU#9V7RjT#OZPF2{@AQEW4C^Nd_e3iYnJ0G^p~j!ig^^jZ`4I zxGN*j=HprwaEaSs0Qo;I&t0aE6s$9F;sq9I2nt~H@{3Y3{@tfgB6*}VhLTxW7BL!J z_|%|RjTs|kk<$vvd>$!3h43uJP^-8~Ye>gL{h}{GrQuJ&lwC)?f?;$0FW*Lw zz-&AnP(9!pjtt-$pRKxgs`?5HR?u^j!V$k7NL=m^zvBTv^^#U*1r%q|hRP9-pRBwWKfrd!M*k}pdIjmMSC7xo6G-k%)HQLvl-_!lQ zLPr4}NrURLviYkG@yhp%#R_3%G68PzQt>qLO%`I}57bh(?4ET2HFD8M5^h6`*v#V4 z(R&YQ-g0R+(}eCi&qMus{I~+2PuV+0*RY*w|551SbNQ0Ch0-w-UmsEBX~8uC9bTLn zhW99jMzY|p47veSD7L*bLrn$r?em{@JpU;UB2cMNkN<7jjHWp6L1N4GES~uL2O4Zl zw~RvrO)ll>VtMJ+?d*i;W)C!+n5f{!&$Q!*aR1XG7$Y?q7$JlUqKroD{0hcau1m+ zF;#5&#DUsL7%d4aAFcmFBmQo(eiu{@@}l?>KNY!$VHz}2Hvn580|WhxD6#+=V^Oq@ z^g9Vh8q8G^sy&QdKHXHy4eRDfV< z*9Lr|)|^;1JQv4mP)ghV_TJko-gsMOn-qkJU_Mk`8ZjB!V6;PM3=EY$twCUQ14Joy zDYzNG%LyJ(lH-HWKdmFep+>R{3^e@wgfh7t2mvBlSeigsgM`#T^QpNjZeb5ozG!_u zJg9?Ol6Mt3TxVRqNEOSnG%)|ARI&!s9WM9Ge~>@Ec$+ToP-GhVQ39G&+yc8FBE^@z!G=k=|o<$bL5EBHlF zEZUJBokH!lcxfJf*(_H^T2HZHmZ-leD8Zeq*Bt9@Ep^~mzVIi5s$o{E;qJN{a^pu< z8c8^FUAJYTue^r?P~~@bf{>3G`5MQ#gVm}-OqR~&qi<|vmmK)zky}?f{MtGCL^ctr2fXjzf^;((|vs-B3Pjcs=84CfdD>WguX=y%Qahz zmK|WPcYcc)9=k?XY_e`ec6bfmKI4jp)|WSV*t*buCU?Bk{Jn3;RftiGrwV-~Cts^C#JL$!4g1iy1IrZ%l;Ls|S ztP|9$fe%UF0fqKjjF(gHl7Na(i{4A^HZ3FOGj~Cy2xcSEGyY2!pUKJ9g}FZ810y7_ zENx~3RO5jMZi|tH7QYd814!Taz_qE6#$bnLj{(?=V@QI_-muTrQbrXrwnkEeyEjE8 z8psce@zxHCx1x)my*6uaxb6F+a}fNPhAGXF&@WZq?-9D(U)a z^^DAddFWaw`SaKVU*g7)Cm{>%3C-(NbP#I(ZqQE;zKZ;DyrccWG3QViWn>jlOPJls ztjjtdT3T#1gg6WuUSK~wIk}=T^S@5^1!)Lx>YsJs^0=NyH+zLtH62}wkdoP#s-TFM zb~v$saL`4QXcAA%Vp!Ihy{{5-`aP=J78LL4$)Yfk%w5+>JLXqVoI|bKOy*I&Xyp8v!HwA&$w>`1LQk&t)(9L`K zbW^<43TFq8?>{}BOU5e8c`r1d*WY57t(KDMwZEWoaJ(TgNB2{NOJB_bfSKU^a^U0SU)Y0s`)#!0G2X{3; z$i56+`vjFZpj-0rta3<-{l3dke`L0{NQt-Wy_0_2kt~pD;J*%pWdC>l!@Z5`IG$uw zy683THH%v#I$FAXBePsx2&{OJb@5$4pLZ-L;TI?1Tz3c6^Dxd^D<5W%P49tGT zrs<<)vP(_|UlRg-3z!{qH7+4Aga!-Rt~M)LO5JV>ESJC0*amjgu@8~nJMOq0NbU{? z#Gj6yTe$w<#&FRm&#gwoLeBll3Ze>mf4`sl^ewl)fr<8KfQ@Tal*50Khgz0B!+Wq%Q1 ze5J@P{^r51J~KhYo}769)hN`QrTjR_k*$At&0G#^t4w?QFT>xBF(}q_R@f}}97mlz z^;1nOP*{93?xeCqD|Tj}tybjM-j!wy@xf zg@LGE2O@6A+8bKUG&F{+PTx5){=Rr*?DWRGKg8@Tc%LrVZ}*42yna&>Y4J54z>Omf zL#7ultNMeGM&~dp4;c!l|8|w)@OWkFmuvxg6af_Gz(8Ad-jux3hJ)b41n72at*`tb z5+Q%yzqz*l#{uw+!$U6Ed{+Ox48nEsvaf3(*9wc@uqZB<%H7)~A(5y>@k`FBetr3u zI6v)8nEFFgxtegEO+JW~V0jU#gAE~xh`1QrN!z^hejl`Eh04m3NJnSw0;5mCD(!~# z$LgmC>3Kn!UV{;U^FtgWfnM=h$iN+*q zzhmRS+)lYX7~AMK?bIBUcUOzZ#5?qLus=iS^k{|by6DX5jqw_~H^fx)qH}#CbXfP? zc7E93c#pYo)xSrXjKF2qW#qdKVvGk{?MIQ<;|r0`enxn^*kjyTSB$X zC5T{%HoY^Atu?xOT~GBxuB{_%6Cewt8TpR{^*GtDqWFx&N+O&--vmC%Xw4I|ISwwv8^*G9|v})0|;8?sIH3h1KZB4(O6k-6<qEhe1^Ro{s@%zKjrIWzD(+7%}$gd&|5A-&mdcWRisGMz+A*abVF68p9k=TubbVE0{gDA%$kV6F=dB)E zw9oyvO6gx)B=c@33dJp)GErANPKd<@cYh`Z)F+^8U1~#F>!VX)sAKuzJF*k0TeDL45E@3YxTd0^6 z?KH!a`w4Z<);bXct}BTLHr(bu$uR2n^xSG+Uw=yrl_ileX#Zi4tZ>=b<&#~#R`;yWFT=`f1$UOh?Kkj7ev z)jawVpQ&V6W0ILBPO=aB7cc*$S#ufs0Lt25%PwsLRPP5dz|#?R6WPv<&$jK<>0$o{Eyr7?t{6C}1C7>qm+V2cH^S%uCxn1d8Q>Cs)sXKAS+JEH@Ki4c)DLg?SC(h|~cO~27KoDy6t11(jSw1ccc?uryJ z!k8AH+EPq>s#v$J+9cWd50GvYFbE);ssdRKwe0W^3+Mzs_r~JWnGwURdEkuJnk7)p zD#QP#uS;In_3pHd{`oK9Xb{s-h1ERK_fKG@56am80-8T%z!#+W9chzr83*|E#V7F6 zttE=F!1+{C~Ax4(_=`mX!&w?FU! zY|O*1;GWmrqXeZmYmSu9mWZ(1O}P8+yYaf$y$(-5{j^B1&=d~ai_uki zrbb?aO<7NjFuwJzr||DS_U~}l-FM+{e((3rvh;9tSO}-@yc2iK3a<;#$UPuM%jMRs zTX^iT$MCU_eas~ZyNfe`={xNT}SlWaL{9F&f_`H5YBn{8Yu` zj{t+mvs!73E&G*wsokVd39jH%3@zl6=&)N6RS#UYEn0obPQuapTuftG3~_uYinjiK zaw2W8JI46)8_(g~i**1G#y|e&B!>gEf-)c{;?qD4A=K zAX#z&f_l|@)*{*W)BOEtd~BpT?Zm8nb;`$IkyOspWcxk0C)P`%>XNzugh&74FYtf- z=Kl$zg!g{`dvR>LRmcB65lNVGoUK_6?{!z zSU&%RV&qgL8Q8Ra+``bTo$dA*{_qo@z^hK3!bd*x5q#f!-ZPsn?ysrv$QM0{q^j+T z^CF(*!ET4gAO9-;?|<`e@cGYwUaJA4Wt|Ww8EPtr9i%v-1(;UX;#8cEdLeu^B5lz^ zXhz#0@e~<_XaXMsC5UdXpOmaiOBU&QDV-6H_i{=8;Dpv2Fwe3lcO?p)u>^ zgxf8Y++QaB9QR76lPmn^U8Ceyl39i$UCSl;qDwyfV1ECLin7L z@LVYmMjrNm{K-GUrAwFavp@S!@Iw#$5bnC`?rh+fKtlM6XcCB);xE$)Yed<)bqX1BcvNsXRsr`IYM$dv7wSyXncPHWZb;5IZy+R`GH($DRp zLcZ%t(>wHmP(?2lXI~`BtoaJ!WB+w^TdB^p!v!aKLG#GwrQmB#vG@U>~0(06YeEXP>-PM~&HxKZ>*W8acf8b~Ehfh9?|M=`*Vn>YczU^-O)VqEd z@4WpCb}wDR?#2PqoPZDu@Rk|Zd53RYdI67I_y)dk`6*mK22eV7sFux8WkU1uO1jnO zJv;_GW0Lb4n0Q5$Xb1MZmZk~QZii{w&54Q_H(kWND245Sk(R8i_N`#pqUJd5c1X&| zlgqzNRuqku%>8oUPrRR`IoR#G!K&Izi=_Ru!)}^z>()&i+`0v#O_i)~HXD5Iv!BJc zzV%J~^oM^MANQ^7fPyEAwh#&bo ze+S?5J>P>@zxqxbJAPal>G_BK{$ROw?HZna_E~)HbDzV%|M&j^4?p~c><83*KHUx+ znV5o5@iioao2siA3Wx;KDfZSNCoO@s5&s*v6$8zcX@S|RUE6u2CEwr$kOW?hs&4QM zN4$WhnV-bmDF%U6UUUp#TMlt9<5gED96Wb^Zny0h z95Dkt?WL#*IKR7wFTeZ@KKt#j;fdW#pc7l99>H@^t0er{xpGvB#Uja7MH!VTp`|6E zd~BdE^m1_LZ;fJ#dWX#cp@dMd8MRA1mI3L_WU!*kgF+nP;>jDl?{u zaplUD>V99ndKI7k^ruw-TxjaMX@_T@eWv?dZrr$m2OoSea~<}YCHwZuZa3i@U;p~v ztaDQn$rB|UKfb{W&%c2G`CtA^{OOhw!dnVf?W`%3l}cnQ=j@&CM*{b$o+Wc zxpU`2PDYfJlu8!Zq%EF(_8I(d|LgyPKm5ce@c#F|AMbd_+wsOXz7hA{dj_XZpT^0P zC-bQDG)=g9^9HV7y@r=wdI`@w`z*fp#1nYrkw@@_haTD=0^9A7Rz)w~kl#^s^?|at zB{z`X2onwt=n=k1lmOS9!axSEb69iGl8=(wXb_}2IM||eG#3ADGH2r7f{H3Z#ZFQp zr@lPjyWTG=U+*Ezbl-jVF${~i)l+k`A%m7^rAl3C+Q1z8R5%BDE9+7O+JW9;F&`wi z)e_!liof6@7mBsrIbc(5T*%YhA6&eQjv^Z5nsc=U;h6VY4cl9cci(;n?>qfQy#3BI zxFg-R??(!QH>N#d6$#khb^^5BV0Yy@=wSaEdq_W(&%%rwiSezg=kUdspTg%ad>t3I z2cYfdP~*}gBs(=VmPi~tzDuqQuF~CipD-rf5{fd-3R?98lQ-!jt7&Pf*{yEHwf6e^ z%lpZ({k{64&~A}|7K@Xyex2QJClU(bBQ+m3({5+X^ZcNd<>hHlyk4$-;yr=`AQM2V z>k-d)ni#u-TR3&ct8nid&fpDicmwXf`!1X~aRLD2=FMC9_P1Zc3oo3-bI(18ix)3q zv)L2^DAlXbZ4C<4Hs3e#v&toR@#`5NK+6KD{tWvBagpPH{XO^K^>26s?!EV3+;PVp zICgB0^c)-<;Of<@c=5#-arW$4oIQIMmo8nxcDt>nZ@Qzj0s0ZOI_x@9sEDvc=$$j( zv=|R2<1L5Z$lJv|D8*r{$y22EbMr%*_2izV3rC1_-?b;B-$L!eRu$QncHJ?Mq}-)Y zK)}&#@krJZV(HA8Gwj{O5$D4Z5~)f61}&gN-bCQ++iN|M;G)!@Yku&%X@Agk+EVV%UCaNh7lhU@H z>&Vv^zT1fAhOJ!Pb(6JSl4;Auv9!%u7+b>;`s{HBnu$qDB@q@ai4mV0FA?c?0RUIL zu)y@C=H}V$*qW}I_WN>fK&siUoPebQ(GH(C><^Nv4#qyom7~gX5^y0#AxjtdIxq5l zN)fkq%69=6w)-=LLhX?e+h`6ZZ9tN{u!8KvWqcqv3|u0kTqK9LpUzV}%k&NVL+AGq_4c-tNK z;#4}e=cW+NlMlQf@0^y=%SEtfe?Z>k9lmwtC4A|^(|GvOH*hZ9KsvE4k`Rak<37`8 zUz9EoMxp?sUC67nR6dHMIICik!B7@`h73h>g@RZtt(9muy*gG%QOEi%q%N6+ z#PiysB_Ubi9P1o&x-B_KBCTRDYhlhveHpTtGHvbpU1oJydSSWc^aE3 z&56i95T;C&WF!E)#JIG(fv;S629Lb_6uxrfdF+nuy-WHZ4CsARb)d6vf~ZWt92iFR z%UMjCiS?H6C1K#Rvo8lP4$?K`uwHUPqoO&3NrA2A+gyk`_#L+S*u4)_+8PY2vI?c~ zqLCJMms1|}?M!?15)}rz1mCre>uVCkaLkkA^|IEG(p5_yn37w6y@6&F^Py4v{^?)* z-}E7E&L9W@IT7=-{tg$!wbmo@4Gd$nih-0?@1b013*=iB4y+L!*|sV0l;0xGmo1+^ z%6RKi?6>lW=z2GIoWA3i@*FIU|Df0JvSw&fY-tbCN1h9jRBd5MB~VzA?lUiL_TReI zG>y*!FAubq(&u6z?kzIvI46?`{??y>{7qWm&bj(eH1z!3XC>Puy`N7B7jXk$yK)v6 zZeBxT;MDdcPHc`LWzCvG4!w~kJay$9K6~!3@c(}MaeNaOfMc7PE0DUMS$QxGwnM_t zpUi!g1wk^}B$h;Mt2almjqEcgLM=zeqFq4Yc;}kKq6Pg^$6|gNa~*h-5dG-A$C?AL z_jM9teoHIHFEFX4Ex7t~tVslV!dlTG8u1CL!Xu?af96&Dr4jQ-61P^%MM}BDTfdYy zQ4ljd6V5Uk8x@)D&tCc;NgBn9q(wQ~6Dbau8a&9;p`~3Mu1QB+wD^doLnT~N5)J&rG2`zEd*XQb_R-mIfq zw_}M`9|{$BRtCLM7fE*p=hI##y&r)e zOQ>4z&)xSYsymRKrNz{4v5mNck0ydG1h)6wT7?X?mZ@qQ^bw7y#nEj0I&rhNE~lPXt6j9B4N3CY+$7JZ!u)5W!6l3}K?7@E==!v#p~B~I=DstmU~Mxwvj#Q`AvqVF8;3F}CGc*FGniz` z{Mj{;nCc$o5n${JwoE4LZxDzThh*ZIKqLrMxj&g|aV|Hgi&jswPrVSI1et2+=NbW? zrwN-|6W)394BmF?9^7~PYj7-WaQ?;>{MF^>@#y7m;nH?O+HAD#(+a2NLsf^~ytWhF z>sm<=PK>@nCODEwzESZ#MY~W=FcHWjq@giBIEap{UB_ij8Hshrl3@Znf%EXv-_Zd$ z(prdAID2ANW!=-EMW@YN|0`BqOpz2 ziF{)rD}yYgn3oO#I}oRnlX?f4=!VzIyFBymoUtj-@SLKDdI*bPIHBiewrDAgQzU-4mqJpq=uuL#x;+O;fA!b*UM_({NdRJU6OrchYA@LzNv;}*O{Cd^ ze2*P+EU)JtH79RLoKJy9*S4q9HX3kT&mFSn=CbOi%ob*2w-ur^UN8eAG3o5oO7Gb* zM^L5cx@6iUB8i@u)o^G}GoxMvI+>FjEiYOQ7DoR2<{aR>^0ft$WUHps7L(&8nsWnAY0CCbeqy`0r<&2y+6urqfW>;s zKNXB}SJPo?N3rX>)2#R#$d@Giy7zxcs3y_B9&DjbrqXMxKl+mdJQ|oq0YGAWk$dO_ zS&}$+4->B~)3Wqv%BL?yOD}PQ;T_Bn>(bY^w{{ZAwx`KcVp6+c&}2uwnxbp+(U9>1 z!+l*LnZ<-|vDO>CR>^W>REALVEEJDKoP7-5n@z4n=HZyojv{(h(k~D(&pEs}18dsF zGX1EVfqitAhlEGqB}s!%zM#9d#QB?(#tpk`Q*W(oXW z1vl>6nO04;S+zrzs8VUSs0}4N(ljKk$AKq#c)G5jzDH6hFtr2@L)aq-hg890Im_utSL1a!8Aq zAa#tqs!k<~R38qs0>K+e5GB!)O*rznVyf6!;vBO;^YqjO%$RUSf?4CQB~I^^YPm9w zgpd4^psdU3HwUor(U8M0&O$n!F{2|CJQlrDvDPh)z4LB4RTz;euFI`yC4WcQu`INk zm}JY(wfxkRLRlW6uc0%dBjfKgWPTyb!a=;=7T_6@f^g6sGw$nupZ4~!{V8t6&Uu|r zv`fXNySnEk+8!hm~%E8HF6Dvh&J4k;4OCK z7UK5WFxr4h+@DFT50&JQNJ{p6D6;K0ifJyYH_kN*VU>Vo>xtyT0++RgHDibcj6)*h zTxeat1(%CRO&T;wrtFK)Ngah!an8vyQJ7GSUzfl<$hU)Isf9F&B!PtK*%9{!2f7t@hKmUwIF47O@{DbVjqfkoo>lVT6iFlB-b_fDonfW$=_vUBn>C5EDFa9j^i6r6D zHPug3KLc%sx>6@3(Whr9HZMC>AB!FwC2hN`bSaihdSdkuDDNFnUoLPPkssVlP>B>4 zv!R=4A(0?Zs?6R|qAkT8=g;pI8B+CMTB?`(hblm+Ltv>0jg9!)QNY6mX_z%2)OV>I z2+MR5nIfjl&y~Q?)OwIidQm;wmPnT;5(HK&QSS=n$w{705*a^(Ah`32xrrC_ScqK7ZC_9+nq$j&U%l7Xbs=r?7g(2H zB*s+PBqWgv$cSR_sG*%guc6K1zz$+G5D-Qu#_G?+N5dGzL<)`A$4glWDfQ8qNE$8h zy|0gu#$_DIzk7Sf6HQx+D7$9O=NUmtdhW?2+h)#drBVp7tNv<}j_#PEIAD}SbPznF zzVF0b^)SQ2NMx`RW;fF1T?H&$1`Y~j>=C^fzA2NUu8@z3g_~Ot%v`(Y%Ao}+_~eOU zlB7Aqjim~h^TU+vVK0ucWIbXiJQitS^U~=;8pY~k5tzjG#-SFN7kVSr7NwPO5UCi1+9AlIkXo5DXk|2Fu#44 z#od)vqT-`3JcMwERvzBVP7ZR?IQYjr5IIH`x(VVSLEIbFWh0W|5t15b>ol|LA?Kxaj-$$Dq4I6aHmK$QF?UAk3(#~k!SDC2D_7oMb5L-#M zUavVp)X>OLNi(>w?=YaDbeMZpP1RVFw+QJY)MzW|HbFPxZisDFLWg#-1Y~XRSL9Z{ zpbQyN=aSU5{M@|5ze;*KsKVtWY{b+XIeNETw@`*&m4f`Z1IzZheKQ6p+-ntbWJQzwg>3wMvxjLRpP^) z>Xvn)I!^*Ds84y#pvsmgVg)(0H?GpjaS*j3`*%jS&A^T}dqH*B0&a@CVB!0rLlu%= zUeL|z!BS`RcmOT&5lqu&VOxl>GqOLnLnBnVdtW+)QR^wwLyLMXp{PKxX+~$bPalOo5PDL(o$qXw8IE z#-BCEM4-}MV-od{Ks0f5QUCxT07*naRCFzCgc_sQiT+(1pEi<+;n5(jtF(63Swdn; zsghCP!3?v4k(g^Zmg|IqosCy(nMp4Ivx%vrY&mjpR0mZr6%Vp=gHUk81r##*kWQ`Q z9+Q(?DK3m6w5&*|SeURaE|g;WYBD|ZWz=y}>*bItrjCA#e}7CyWMNsG?aJ|MoA+{= zqG3j85ZB77nEf@;>*cyu7nUWD0flF|ooGDyv;|BQ;Pbk((*NA9Bt8GD(Ln7Ty6^-M zx$LjpCN`X4`MS1XTg#L2VD26wvj(whzAy7E;5BwXWmT@w5BYVjpJSD9jedR=)(0og zWV{(H343SYQisq&7?-i6W0y(J-f)#)owYM6glUu3(27Dn!&IjEMI}ZVfUhsxFditoI=%BiTLB zZt;>-2IrA}7OuL@b_JX^2nmJR4wD|$c~JJ7eNIeGjk+o^$ZO3UIBXEHDDh!u+@E@f z)50T3RD~*@SQ7#xp*fDP4~I1V8+97?4ua9d(yLA*@wW&K*M@Ejf@yp$0i#BXp=qs} zr=rR0t;2a-ftQ0VI_2p;%eQq~4s-k`+n$m~%RAgsUa9TUOu# z?z1f}CO@$$_m+!@mV8QzMQ%{3i8Ua@>o&N}SBuC`+qT`JZ9^eMqJ_=`%V^bTc^)*- z;cJd83T{zOat3~moFqK2g-Vs%sX7e@%!TGznGqTCuieAt53-z!G&72HfYwFHZ5eWm zHbFxNJ~fQZM%pzaC`wZkVsJoV^#4O1K|B)_g@SAgs_zjitDjwx1UABP!99t752Hs_ zVbK10OV4AgBAxw7IVZk$Pxo4>he}=o&$js6$=Rl?5)cxFNpEVXXNwU%N8c+a-%J7p zZjLSU9hGG>7b#n&tQkV6i8f>T&G;`nP!Wl%U@ z?<}vqe5-G6G+zr(bE|_&n`!bTvTemOv6KbY3u+mww!8-BNzE1H@kjHl*tCJNDVLKS zv-iwdtHP1FztVe2J!yIG+>Kc(b2ea89`9pcqoTbjpo|_zQIu-m=VV@P9m9PSCaH2t(_6%YUF_O*3#N)d|2U|Yc$g1 z9Ymko8?s|`Olg;_rY-s?>9)Y?o%U8CL_rGJRhHf$ z*Fwg%7#BBG2}3%bKFMa&fxyl}DyvX5Yh^G}33YmAJ%1+k9;6S@NHENkSZ)R6dOcCB zFwBDqR3q@pbz-?Yk+W^pBW_8xN6MvhbW2FZG|pH$cn#cw(vfN-B2exPV}{$qEdDUo zUJFEKY%won5x808X>=$T>>l5s*Sn9Z?llI1L+iisDT{tq+|J`KB|^FU4Gl(%cU(91xp?@ZebSp+*AHV76xyw!^)qycdNL!x<%kIXFp9 zpd?cME`XC!ibck5Kh;4z(BVyxGYU+k#@+~{UL@jEaFxLf_ZGvqn7BeYdTS&?^JF`u zg#h-R|6U=O-RU(~p?Bq4j?5v~jGvAa79-DM^mSVBZ8iQWTa$rhLeCgbk@kYbmOBP) zp47XoIAP4@)O9(?3c#z9IN!PH&4hl+E6}7@rCd%W^|K7x= zo?Q=vyAj>!IT{ZK0;*w1s|(Z0?4zoB^x2IN|HvU1d-td%M%24kA(9Zh2SK|Vw<@33 zJ&K;2UAu?T44^=ZsJG8sXin?d_x4OH!RWRM8rM6rclGu6(WKsW4aEo^U#lZYY4koR zCDlIejfJ-2g?G60)=O&-_sn09g5HcK=QY{}l{DmbYb1BjI^bn1Ko~yS`J>*x?Vq;> z!(+dts9tY!L?<1esA|0=;gMvK`}xdYe3g37C~B>bwjlT8!e|S>jH(bi;CPTv_bTQ= zC>gc)PU|7lfS!ogrjeprk!DFmmXcP3)hMty z=IcH3bYzXNwi;^()G>lroREt|%5?KQmBXlVTC3>UI(#hIPVeop=f36;XrVE^vB8n` zN2@1ar~+1A5xtKfc12SyBW;y6oRiUnXAMf}CAQ%jZ}^3+dp_ge8~Xh;u&NCur1`g1 znaw0^uV{b}nzPR|P)a9fy{}G0r5_kHBEMWi)XodZw4@8@S}%y*_P_G88*Dd@CYr5@ zFaV%4XU?omXPc7+wYmcxAElnD?*du zm#`WKT@V#G?w2*QMO&B4&plpQuYo(|`<3fzD-(`#fJRZTwN;c_H&m;J_5$8mubs^j zBrUeJLhQ=pLdlxSKI0hmDQl(ooJSE6iI}lzSyW53DX-s0zSbZ#RgX~pJgU8Gf6kJD zy`Pcsz&EiD9cxsE(A zqOX$(;N9fYIGh$#>pFTqOg$8?kY5`xo&N&d&z2TWkRV#-3>C4vrbnkEGOrh19PP*c z652PR3TzbLKvLX>Cw)palbO9YxYE*|G1kXT>59s8eDGCe<4DRRbRN!)@oR6N9>44BRK3cO&k)EY4&9ks;h7KX#DW9R` zD=q^GNK|J=fmW2)q8TlYB2=}~F_DM!P~VU{8PXyVk^pRKC@ZBTyCeB`YSghjaX*l+ ztsWc$J{C5<1>qZDH)BqSz@fD8=tAoG2>1yhV=~5LDzI09u%AqfLc(5Ra3rhxhSho& z1Dy80Ky6X7!~I%KBx%iriPvj8##3(0orf!m}Cgd$gAS za&po|wv>TRIb#ylZBVuUbY<%KEtQe>I?H$^+FeoG#h$~`L%{v*6agY?$RNq@8+ghWl5E0aYGsBJ2}D5n+G^|R5EAM5>#xJL9A%M7NdQ0C@{@nN)Fs)V1~hbTL1 zf;z=SK^wVwF4`s5B4 z4IW&hR_UXqJ{x)WHKVXc z$;u<=$fG1Yqns2~0Svo6lXz;ihD)W9L+y$oav_Z?be*VVCEl#0%VA4x)L#6RLXaY4 zB`e#qbl|4)9M~yJAbRHuF@sE%YS0!{)eR)MYu41*TIWCoy3#JM5{-RZBZ(p}UN_W9 zEJM!O%E5)+R7EBX3%?g>8_aRKL$7PBAz6mD}y9vAZcv*k!6hyMB5ZXpL3O1}3jxrA1OJow0SpYfR^#fxsb6Q)O zDM-5Lc#8GQxoGAJQCNd~}dSuB-CC42?^}ar-?aY`g{`=^dV< zf;xvyirCuJV{q+>l7QLju$1RGq9{A!4bcl8-biZ=OdQS^W#pcY2x8gSOKEOlCCB_w z?A_AV+D{df8d(0HN|2u0M@z$3wMSLrLIq~dtqYyRXx|n6cNX76LE-?S%Hv11NrcBs zvWSPUTp-gfX&qjNQEFX%Q1!k?c&2#kFkCx?toXdPwca}T+J>1N?iZO+ju17g0^IdW zVukR?Hk#LYuv1RGy;~ z4_x>VEC*EV{6Ut=0(Aw_BIjD=JF5t~2xDb_0JldNTNSb!P!@Qat>IC{dp;b6Iqu`aZ$1 zUA}h`Cm|6h*O`@17cUw?3~e#K zL;l2KGb0B>g(rw~=^=us#Oq0?wZSU%gK0dgmdo?nzpFQ$3_%b6Z%ry5h12Do;(XYY z^+Va`r{&tKzTonzL|`=&TgrC9n+OOJg`TcKocz@Za6DPn-}pRGy^J6-9JD(WSBSOULF-HYK&F5n450fm zx3KfU-RBdx7rFaf*$MiswvA12Tdg1YzU@F=CV!Uly|->D@E&RYYu~3f!9F3bU|O+Hd{M25Cv=d9CB-`^E_IItGf@W_A@$6>uxlrL{$!f=HjbN zG)jI1m<8X6L;aHx#Rp1drZ_Onj%feHy-=Q^;2><;HVzc_afr*W^V(k97|x z$m|z>)IbHNX%A_-^HSGo2Yptp)ZawtHy|+F*%6O;{M+3RzV76$B z+~r%6;oSf=qP`kjr>Y!1NKE{m1j@1xzLRMUc{(tht4e+7+Uw$4UqCSUd73o5Cm84*YDbDarpb0bv4KRguXzW(f}>EcS?N{+77}QOI}k@jm5U0eKm661YCoja&Tg(ZHCAum>2D3YKLiPV#E$d;|g z)$6X$F&b)t4Hl-oH7cKfYYj)SZ^Sl*f+@6um-F08d zz8jL7*&|-BaUuB-kNIGD3#jFD(t|(?+#bVJJ>Et|yfgoID@Iv&lK%IJ^@t|V>VMND zTTRd?m3_8y+{`HSl&qTT(wrU7h6Fo1UM`We*`RO#922yAhyW~0pI8(87Qru?56Yri zr>Z1dNsLa(c!9@r#+HauawZTYtiH-=b<%|1R>Cxb7J5n98ow5Zd(0ydcTNr{C2&UP z608Pso z{GM}gwPeLra|gXG$jKVl@nZe4=T@vhm=byTtIu8QBpHP9>6Y;GFfdK~m>==qqEX{& z|Bj+GD`KA~80aPB8DW*S9_)z!(7abFl_d+k;l#*{UWdxEA_g|o!=|b1fr~Cwy^i5XAKd}SvH5+ znN27TGwzATqHMuvBLvp9f0ykL+1B#1o$p;cN-|}y4M|o#L^Q4Jg z8JGbF3!M#l7$pf6P)I!7CkV3*n1vwQIaHCx+{zz)Z==?IX0*8vvXUXE6)LqpI8aID z!76feQml7vZu`s>GCYthsI|;G3FN3}Ym|AFW>IQ$d;BUSjMk?P?@v_D3 zbuH}AVzgGF8mbjDKc8y-@akl{PEh(%o+PXetQca1tV8+jTW_G}QNvKX9D>8-^krw6QcVrr#C-MVO~)=FvmRxt0` z$misj#?TMjN-BTeBYs3lT|*05iYiV^pp6c1638(zhVMamK)|$TCxJ?LP=_^6#Z_W7 z5Z|oBy3+aLx)5%mCZqfth=Mvrq;(J|>Ccsqu)zAmwq4h@-$oP9wZ%07!wRUQ=hWH} z;^7x@(_sL;i|r#336@Ad7bU-ksCIdzBJY0#-`!UT)FA0x|?nUDlH#X3r5Ly zQZzQ@_2kzs;LJ1O0cqn-VpQFuK~Xkp$#N$>Hkg_FifpDGJ2}z*Hw@aM^pqUi=N_Kh zI((Js8Pqdujs1E$K0RJTv>r#HPewhyEGL!h8nzPDYzWTetkIlEHpUdYqo<5o7>jjO zBts$#H75quM8}fvT4b@e-*t1jH2iE*GBS$a(zx&@&g8oElFr^9uiHgbt<^ly*r<}d z!HBg)+oCEuFael7XS4t(nL&*PHq?w=Zp=@q-^SvB1Ks1s$)b_`R?bn!&eiyEqPE~B z>q)m(65XBr8~l8uJfjr6a`_=U7B+BDMT=mW`tW{sG6YNs^Al5XrW7p&*j9m4S=#=; z7eu0)7fD=)J|w&s7Gl!vq}Cy14=L_c-O(OMq~)ff!*Nw&H1NJTn9js2H`|m|h1j58 z@1GY<6nU1-{pCrF@#cz2wRLn-U4LHERRa0zURFT+3&e*(_LwAr(NWY@RS<`WR5%3!9NNc-jGA=xI@9bLo@P{cLG4JQWXm&1?KW`mIs0m{~g;eibDaY>YF~)FW6di_Y zI?|E8X#R{h`b*28DLcuDKj~*RrjBBWx}230Vo1ai?@64kYZBfFoQT%^XdKzP2N%t7 zy4SmIR4T2}qX^1oW~Ff;tMzH=MbFN;Qgm&}Yzh6~0via)3APk-ESAsM^O+FtE3PTROsy|0g$QPY{TYMIPIdD=}{h)h-(azoCjCsW<0&2aft?mLsH zkSdXpVkB2pGu6YOYAOh1t9hzkFO!EwvhKHJpnP|{prcz#dhL-zQgsalSz9-F;(&^S zp;|nK+?lHL!q?Jqtnb+EAy5hBS(O%7FXqNtI+DY~xz)<<_-rdF8G-g|=VsEn88QjZYQJJ4 z%52%(>WATlaDj1iLbV3@GuPERgk4fBi-A6ZIr{tNk6nbiMKDiPg|)>Gta@I{+93AZ z;Ii162N)#);Lh_>wXqn^fn|Rk{hj|(EXVGN^DI} z&+F%B&q(Ig^1VH#`m!LxpvF?XP0O`&!*3zJN1e?VB!5ei$x>sYmDxB_)AIQ&5NJk% zxUOmx@ikd-KYyBxgdmi1HC?6?Wc9>#?^|mRAXGqZt*bjZPo={vfQ*5UzBLTM3nV z7TF^$Rnq+<2utycaZr^JcVn^#Bo#iDh_AW5hPGnrq4aLVdX$q=3MHrps5~8?V$dgj zlDJttt#6m~knWK-q8yoq^8A6}EEV(GQNh4w3(d@Ydp0eAN2X^L1FV2(7Y)DhKwZ-whG6hCHDW3%PJrup*V5P(D0JL6zQBPjZ6G;xBpK z%YEPpyip~ruw%u!)lSsWv}vF6NtP+q4HEQ$s~y}aVk*P+|fs( zL@KFd+2%-O5)aA~HJ%@7SGiYCfh`B7R5N8@;wiV65|Pkfn_+qh!tIfpY2rO2Ap88V zcEo1V+nliHToKozg%GWutb{CoreXfoOg@3`q>frsjR3i}qF(l&O3+xZkX)otYHG14 zQDYR39X732t8`r@V$1X7K7_6fP-9&gNY~+keZ98&9R2>0LZs^##^`F|*mmPlcoV1) zth!ZNon66swkVxjrD9f!IXHA7x0c+#Xp^|HcyZF6(XELm)mE-1fk+?n_TE(Y2_=!? zilm+HQ~&@1vPnciRR2|4JTy;MN<@`uKSjH5$8$Z95XA<9%ZHcWvyBcv_LMsj6GA=c z%eyhr4eD(wuYc~nzPjN=72Lmv%~_F~Jd?E1%B7Vcbu}J330Rb*xned;VB{#Xwu2vpG>}9-7sL8-2ol&LN1k{O#CztC{CvOaRPPqJQ(5L@fPF=u{4yocJ8TnkY{+? z5+c=MhSGjg=n+-i6iXS(?;-D#L@?NAp~yb^8p3b3glHug!d?R%kC@hE;b9KTR=c`K z&X)Hha~Q-JkMy+BLsuy$^YjWOv2snf?%h&6xbKy0(IQr0BKP%M=h4UnR_m;Ygx2Jm z$|x%b=m)j;d^q?Z<6|Na%j8V7q83GLT_S=f22m&v z0%n=^?Ov+-)I_@Xr++5azh8b%n(RfRNX>Cf@m}(4DJ5g1r5g2m7S0}wj#_M_-2r1v z*MjxJexUicHJdXO^5S-Hv}`{Pq4?_OEZQ?#joE6H{GNM($CrZ#QfGrQg26ttu1=#~ zPdq^0I1nO}8%uNV1aZ}%#>=(jL$Vx&z575-Xxq=8+KcoQoAD(HzR!*mcqj%D~s<}DxOE-`s5fT?&%)MT}2%(K;AzB zF=!M(CxiX9`Dzx=)D${)MJ?znSiAQB!%R5Q<;Vo u+C-Gcxy9CIb literal 0 HcmV?d00001 diff --git a/README.md b/README.md index d1a7322..4fb3e3f 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,9 @@ PineTS enables algorithmic traders, quant developers and platforms to integrate ---

- Sponsored by
- PineTS + Sponsors
+ LuxAlgo   + Sponsor

## What is PineTS? diff --git a/src/Context.class.ts b/src/Context.class.ts index 44c3bf2..5c3061f 100644 --- a/src/Context.class.ts +++ b/src/Context.class.ts @@ -46,6 +46,7 @@ export class Context { public cache: any = {}; public taState: any = {}; // State for incremental TA calculations public isSecondaryContext: boolean = false; // Flag to prevent infinite recursion in request.security + public chartTimezone: string | null = null; // Chart display timezone (affects log timestamps only, not computation) public dataVersion: number = 0; // Incremented when market data changes (streaming mode) public NA: any = NaN; @@ -198,12 +199,13 @@ export class Context { return new Date().getTime(); }, get time_tradingday() { - //FIXME : this is a temporary solution to get the time_tradingday value, - //we need to implement a better way to handle realtime states based on provider's data when available - const currentTime = Series.from(_this.data.openTime).get(0); - if (isNaN(currentTime)) return NaN; + // TradingView returns 00:00 UTC of the trading day the bar belongs to. + // For daily+ timeframes on 24/7 markets, this equals the bar's close date + // (i.e. the date the bar settles / completes). + const closeTime = Series.from(_this.data.closeTime).get(0); + if (isNaN(closeTime)) return NaN; const timezone = _this.pine?.syminfo?.timezone || 'UTC'; - const parts = getDatePartsInTimezone(currentTime, timezone); + const parts = getDatePartsInTimezone(closeTime, timezone); return Date.UTC(parts.year, parts.month - 1, parts.day, 0, 0, 0); }, get inputs() { diff --git a/src/PineTS.class.ts b/src/PineTS.class.ts index e30bd9b..3301c07 100644 --- a/src/PineTS.class.ts +++ b/src/PineTS.class.ts @@ -6,6 +6,20 @@ import { Context } from './Context.class'; import { Series } from './Series'; import { Indicator } from './Indicator'; +// ── Timeframe duration utility ────────────────────────────────────── +//prettier-ignore +const TIMEFRAME_DURATION_MS: Record = { + '1': 60_000, '3': 180_000, '5': 300_000, '15': 900_000, '30': 1_800_000, + '60': 3_600_000, '120': 7_200_000, '180': 10_800_000, '240': 14_400_000, + '4H': 14_400_000, '1D': 86_400_000, 'D': 86_400_000, + '1W': 604_800_000, 'W': 604_800_000, + '1M': 30 * 86_400_000, 'M': 30 * 86_400_000, +}; +function getTimeframeDurationMs(timeframe: string | undefined): number { + if (!timeframe) return 86_400_000; // default to 1D when timeframe is unknown + return TIMEFRAME_DURATION_MS[timeframe] ?? TIMEFRAME_DURATION_MS[timeframe.toUpperCase()] ?? 86_400_000; +} + /** * This class is a wrapper for the Pine Script language, it allows to run Pine Script code in a JavaScript environment */ @@ -55,6 +69,18 @@ export class PineTS { } private _syminfo: ISymbolInfo; + private _chartTimezone: string | null = null; + + /** + * Set the chart display timezone (like TradingView's timezone picker). + * This only affects log timestamp formatting — it does NOT change the timezone + * used by computation functions (timestamp(), dayofmonth, hour, etc.), which + * always use the exchange timezone from syminfo.timezone. + * @param timezone IANA timezone name (e.g. 'America/New_York'), UTC offset ('UTC+5'), or 'UTC' + */ + public setTimezone(timezone: string) { + this._chartTimezone = timezone; + } constructor( private source: IProvider | any[], @@ -81,7 +107,13 @@ export class PineTS { const _ohlc4 = marketData.map((d) => (d.high + d.low + d.open + d.close) / 4); const _hlcc4 = marketData.map((d) => (d.high + d.low + d.close + d.close) / 4); const _openTime = marketData.map((d) => d.openTime); - const _closeTime = marketData.map((d) => d.closeTime); + // Providers should supply closeTime in TV convention (= next bar open). + // Safety-net for array-based data or providers that omit closeTime: + // estimate as openTime + timeframe duration. + const tfDurationMs = getTimeframeDurationMs(this.timeframe); + const _closeTime = marketData.map((d) => + d.closeTime != null ? d.closeTime : d.openTime + tfDurationMs + ); this.open = _open; this.close = _close; @@ -609,6 +641,12 @@ export class PineTS { }); context.pine.syminfo = this._syminfo; + // Chart timezone only affects display formatting (log timestamps). + // It does NOT override syminfo.timezone, which drives computation + // (timestamp(), hour, dayofmonth, time_tradingday, etc.). + if (this._chartTimezone) { + context.chartTimezone = this._chartTimezone; + } context.pineTSCode = pineTSCode; context.isSecondaryContext = isSecondary; // Set secondary context flag diff --git a/src/marketData/Binance/BinanceProvider.class.ts b/src/marketData/Binance/BinanceProvider.class.ts index c50c641..24ca5d9 100644 --- a/src/marketData/Binance/BinanceProvider.class.ts +++ b/src/marketData/Binance/BinanceProvider.class.ts @@ -92,6 +92,21 @@ export class BinanceProvider implements IProvider { this.cacheManager = new CacheManager(5 * 60 * 1000); // 5 minutes cache duration } + /** + * Normalize closeTime to TradingView convention: closeTime = next bar's openTime. + * Binance raw API returns closeTime as (nextBarOpen - 1ms). For all bars except the + * last, we use the next bar's actual openTime (exact). For the last bar, we add 1ms + * to the raw value. + */ + private _normalizeCloseTime(data: any[]): void { + for (let i = 0; i < data.length - 1; i++) { + data[i].closeTime = data[i + 1].openTime; + } + if (data.length > 0) { + data[data.length - 1].closeTime = data[data.length - 1].closeTime + 1; + } + } + /** * Resolves the working Binance API endpoint. * Tries default first, then falls back to US endpoint. @@ -135,6 +150,52 @@ export class BinanceProvider implements IProvider { return BINANCE_API_URL_DEFAULT; } + /** + * Fetch a single chunk of raw kline data from the Binance API (no closeTime normalization). + * Used internally by pagination methods that assemble chunks before normalizing. + */ + private async _fetchRawChunk(tickerId: string, timeframe: string, limit?: number, sDate?: number, eDate?: number): Promise { + const interval = timeframe_to_binance[timeframe.toUpperCase()]; + if (!interval) { + console.error(`Unsupported timeframe: ${timeframe}`); + return []; + } + + const baseUrl = await this.getBaseUrl(); + let url = `${baseUrl}/klines?symbol=${tickerId}&interval=${interval}`; + + if (limit) { + url += `&limit=${Math.min(limit, 1000)}`; + } + if (sDate) { + url += `&startTime=${sDate}`; + } + if (eDate) { + url += `&endTime=${eDate}`; + } + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const result = await response.json(); + + return result.map((item) => ({ + openTime: parseInt(item[0]), + open: parseFloat(item[1]), + high: parseFloat(item[2]), + low: parseFloat(item[3]), + close: parseFloat(item[4]), + volume: parseFloat(item[5]), + closeTime: parseInt(item[6]), + quoteAssetVolume: parseFloat(item[7]), + numberOfTrades: parseInt(item[8]), + takerBuyBaseAssetVolume: parseFloat(item[9]), + takerBuyQuoteAssetVolume: parseFloat(item[10]), + ignore: item[11], + })); + } + async getMarketDataInterval(tickerId: string, timeframe: string, sDate: number, eDate: number): Promise { try { const interval = timeframe_to_binance[timeframe.toUpperCase()]; @@ -170,25 +231,18 @@ export class BinanceProvider implements IProvider { while (currentStart < endTime) { const chunkEnd = Math.min(currentStart + 1000 * intervalDuration, endTime); - const data = await this.getMarketData( - tickerId, - timeframe, - 1000, // Max allowed by Binance - currentStart, - chunkEnd, - ); + const data = await this._fetchRawChunk(tickerId, timeframe, 1000, currentStart, chunkEnd); if (data.length === 0) break; allData = allData.concat(data); - // CORRECTED LINE: Remove *1000 since closeTime is already in milliseconds + // Raw closeTime is (nextBarOpen - 1ms), so +1 gives the correct pagination cursor currentStart = data[data.length - 1].closeTime + 1; - - // Keep this safety check to exit when we get less than full page - //if (data.length < 1000) break; } + // Normalize closeTime on the fully assembled data + this._normalizeCloseTime(allData); return allData; } catch (error) { console.error('Error in getMarketDataInterval:', error); @@ -209,8 +263,8 @@ export class BinanceProvider implements IProvider { iterations++; const fetchSize = Math.min(remaining, 1000); - // Fetch batch - const data = await this.getMarketData(tickerId, timeframe, fetchSize, undefined, currentEndTime); + // Fetch raw batch (no normalization yet) + const data = await this._fetchRawChunk(tickerId, timeframe, fetchSize, undefined, currentEndTime); if (data.length === 0) break; @@ -219,15 +273,15 @@ export class BinanceProvider implements IProvider { remaining -= data.length; // Update end time for next batch to be just before the oldest candle we got - // data[0] is the oldest candle in the batch currentEndTime = data[0].openTime - 1; if (data.length < fetchSize) { - // We got less than requested, meaning we reached the beginning of available data break; } } + // Normalize closeTime on the fully assembled data + this._normalizeCloseTime(allData); return allData; } @@ -241,7 +295,6 @@ export class BinanceProvider implements IProvider { if (shouldCache) { const cachedData = this.cacheManager.get(cacheParams); if (cachedData) { - //console.log('cache hit', tickerId, timeframe, limit, sDate, eDate); return cachedData; } } @@ -257,63 +310,25 @@ export class BinanceProvider implements IProvider { if (needsPagination) { if (sDate && eDate) { - // Forward pagination: Fetch all data using interval pagination, then apply limit + // Forward pagination — already normalized by getMarketDataInterval const allData = await this.getMarketDataInterval(tickerId, timeframe, sDate, eDate); const result = limit ? allData.slice(0, limit) : allData; - // Cache the results with original params this.cacheManager.set(cacheParams, result); return result; } else if (limit && limit > 1000) { - // Backward pagination: Fetch 'limit' candles backwards from eDate (or now) + // Backward pagination — already normalized by getMarketDataBackwards const result = await this.getMarketDataBackwards(tickerId, timeframe, limit, eDate); - // Cache the results this.cacheManager.set(cacheParams, result); return result; } } - // Single request for <= 1000 candles - const baseUrl = await this.getBaseUrl(); - let url = `${baseUrl}/klines?symbol=${tickerId}&interval=${interval}`; - - //example https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1m&limit=1000 - if (limit) { - url += `&limit=${Math.min(limit, 1000)}`; // Cap at 1000 for single request - } - - if (sDate) { - url += `&startTime=${sDate}`; - } - if (eDate) { - url += `&endTime=${eDate}`; - } - - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const result = await response.json(); + // Single chunk — fetch raw, then normalize + const data = await this._fetchRawChunk(tickerId, timeframe, limit, sDate, eDate); + this._normalizeCloseTime(data); - const data = result.map((item) => { - return { - openTime: parseInt(item[0]), - open: parseFloat(item[1]), - high: parseFloat(item[2]), - low: parseFloat(item[3]), - close: parseFloat(item[4]), - volume: parseFloat(item[5]), - closeTime: parseInt(item[6]), - quoteAssetVolume: parseFloat(item[7]), - numberOfTrades: parseInt(item[8]), - takerBuyBaseAssetVolume: parseFloat(item[9]), - takerBuyQuoteAssetVolume: parseFloat(item[10]), - ignore: item[11], - }; - }); - - // Cache the results if (shouldCache) { this.cacheManager.set(cacheParams, data); } diff --git a/src/marketData/IProvider.ts b/src/marketData/IProvider.ts index 840c070..cf95d7b 100644 --- a/src/marketData/IProvider.ts +++ b/src/marketData/IProvider.ts @@ -55,6 +55,18 @@ export type ISymbolInfo = { target_price_low: number; target_price_median: number; }; +/** + * Market data provider interface. + * + * ## closeTime convention + * Providers MUST return `closeTime` following the TradingView convention: + * `closeTime` = the timestamp of the **start of the next bar** (not the last + * millisecond of the current bar). For example, a weekly bar opening on + * Monday 2019-01-07T00:00Z should have `closeTime = 2019-01-14T00:00Z`. + * + * If a provider's raw data uses a different convention (e.g., Binance returns + * `nextBarOpen - 1ms`), the provider must normalize before returning. + */ export interface IProvider { getMarketData(tickerId: string, timeframe: string, limit?: number, sDate?: number, eDate?: number): Promise; getSymbolInfo(tickerId: string): Promise; diff --git a/src/marketData/Mock/MockProvider.class.ts b/src/marketData/Mock/MockProvider.class.ts index bfa891a..5a466ea 100644 --- a/src/marketData/Mock/MockProvider.class.ts +++ b/src/marketData/Mock/MockProvider.class.ts @@ -230,6 +230,9 @@ export class MockProvider implements IProvider { // Filter and limit data const filteredData = this.filterData(allData, sDate, eDate, limit); + // Normalize closeTime to TV convention (nextBar.openTime) + this._normalizeCloseTime(filteredData); + return filteredData; } catch (error) { console.error(`Error in MockProvider.getMarketData:`, error); @@ -389,6 +392,21 @@ export class MockProvider implements IProvider { } } + /** + * Normalize closeTime to TradingView convention: closeTime = next bar's openTime. + * Mock data files contain raw Binance data where closeTime = (nextBarOpen - 1ms). + * For all bars except the last, we use the next bar's actual openTime. For the + * last bar, we add 1ms to the raw value. + */ + private _normalizeCloseTime(data: Kline[]): void { + for (let i = 0; i < data.length - 1; i++) { + data[i].closeTime = data[i + 1].openTime; + } + if (data.length > 0) { + data[data.length - 1].closeTime = data[data.length - 1].closeTime + 1; + } + } + /** * Clears the data cache */ diff --git a/src/namespaces/Core.ts b/src/namespaces/Core.ts index 1f8dbb0..c4e4bca 100644 --- a/src/namespaces/Core.ts +++ b/src/namespaces/Core.ts @@ -213,6 +213,9 @@ export class Core { ); } // Fallback for other formats (RFC 2822, etc.) + // RFC 2822 strings always include a timezone offset (e.g. "+0000"), + // so they are normally caught by the explicit-TZ check above. + // Any remaining string that reaches here is non-standard; parse as-is. return new Date(ds).getTime(); } @@ -240,7 +243,7 @@ export class Core { // For plain UTC, return directly const tzNorm = timezone.trim(); - if (tzNorm === 'UTC' || tzNorm === 'GMT') { + if (tzNorm === 'UTC' || tzNorm === 'GMT' || tzNorm === 'Etc/UTC') { return utcDate.getTime(); } diff --git a/src/namespaces/Log.ts b/src/namespaces/Log.ts index 387d11c..e5d56c0 100644 --- a/src/namespaces/Log.ts +++ b/src/namespaces/Log.ts @@ -2,15 +2,40 @@ import { Series } from '../Series'; import { Context } from '..'; +import { getDatePartsInTimezone } from './Time'; -function formatWithTimezone(date = new Date(), offset?: number) { - const _offset = offset ?? -date.getTimezoneOffset(); - const sign = _offset >= 0 ? '+' : '-'; - const pad = (n) => String(Math.floor(Math.abs(n))).padStart(2, '0'); +/** + * Compute the UTC offset (in minutes) for a given timestamp in a given timezone. + * Returns 0 for UTC/GMT/Etc/UTC, and the correct offset for IANA/offset strings. + */ +function getTimezoneOffsetMinutes(timestamp: number, timezone: string): number { + const tz = timezone.trim(); + if (tz === 'UTC' || tz === 'GMT' || tz === 'Etc/UTC') return 0; - const tz = sign + pad(_offset / 60) + ':' + pad(_offset % 60); + // UTC/GMT offset notation: "UTC+5", "GMT-03:30" + const offsetMatch = tz.match(/^(?:UTC|GMT)([+-])(\d{1,2})(?::(\d{2}))?$/i); + if (offsetMatch) { + const sign = offsetMatch[1] === '+' ? 1 : -1; + const hours = parseInt(offsetMatch[2], 10); + const minutes = parseInt(offsetMatch[3] || '0', 10); + return sign * (hours * 60 + minutes); + } + + // IANA timezone — compute offset by comparing UTC parts with timezone parts + const parts = getDatePartsInTimezone(timestamp, timezone); + const tzDate = new Date(Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second)); + return Math.round((tzDate.getTime() - timestamp) / 60000); +} - return `[${date.toISOString().slice(0, -1)}${tz}]`; +function formatWithTimezone(timestamp: number, offsetMinutes: number) { + const sign = offsetMinutes >= 0 ? '+' : '-'; + const pad = (n: number) => String(Math.floor(Math.abs(n))).padStart(2, '0'); + + const tz = sign + pad(offsetMinutes / 60) + ':' + pad(offsetMinutes % 60); + + // Build ISO-like string adjusted to the target timezone + const adjusted = new Date(timestamp + offsetMinutes * 60000); + return `[${adjusted.toISOString().slice(0, -1)}${tz}]`; } export class Log { @@ -23,33 +48,33 @@ export class Log { param(source: any, index: number = 0, name?: string) { return Series.from(source).get(index); } + + private _formatTimestamp(): string { + const timestamp = this.context.data['openTime'].data[this.context.idx]; + // Use chart timezone for display (like TradingView's timezone picker), + // falling back to the exchange timezone from syminfo. + const timezone = this.context.chartTimezone + || this.context.pine?.syminfo?.timezone + || 'UTC'; + const offset = getTimezoneOffsetMinutes(timestamp, timezone); + return formatWithTimezone(timestamp, offset); + } + warning(message: string, ...args: any[]) { // Suppress log output in secondary contexts (created by request.security) // to match TradingView behavior — only the main chart context produces logs. if (this.context.isSecondaryContext) return; - const _timestamp = this.context.data['openTime'].data[this.context.idx]; - //FIXME : we are forcing UTC for now, we need to handle the timezone properly - const _time = formatWithTimezone(new Date(_timestamp), 0); - - console.warn(`${_time} ${this.logFormat(message, ...args)}`); + console.warn(`${this._formatTimestamp()} ${this.logFormat(message, ...args)}`); } error(message: string, ...args: any[]) { if (this.context.isSecondaryContext) return; - const _timestamp = this.context.data['openTime'].data[this.context.idx]; - //FIXME : we are forcing UTC for now, we need to handle the timezone properly - const _time = formatWithTimezone(new Date(_timestamp), 0); - - console.error(`${_time} ${this.logFormat(message, ...args)}`); + console.error(`${this._formatTimestamp()} ${this.logFormat(message, ...args)}`); } info(message: string, ...args: any[]) { if (this.context.isSecondaryContext) return; - const _timestamp = this.context.data['openTime'].data[this.context.idx]; - //FIXME : we are forcing UTC for now, we need to handle the timezone properly - const _time = formatWithTimezone(new Date(_timestamp), 0); - - console.log(`${_time} ${this.logFormat(message, ...args)}`); + console.log(`${this._formatTimestamp()} ${this.logFormat(message, ...args)}`); } } diff --git a/src/namespaces/Time.ts b/src/namespaces/Time.ts index 15577f5..f6343f8 100644 --- a/src/namespaces/Time.ts +++ b/src/namespaces/Time.ts @@ -22,8 +22,8 @@ interface DateParts { export function getDatePartsInTimezone(timestamp: number, timezone: string): DateParts { const tzNorm = timezone.trim(); - // Fast path: plain UTC / GMT - if (tzNorm === 'UTC' || tzNorm === 'GMT') { + // Fast path: plain UTC / GMT / Etc/UTC + if (tzNorm === 'UTC' || tzNorm === 'GMT' || tzNorm === 'Etc/UTC') { const d = new Date(timestamp); return { year: d.getUTCFullYear(), diff --git a/src/namespaces/ta/methods/vwap.ts b/src/namespaces/ta/methods/vwap.ts index ca05f7c..f2415c4 100644 --- a/src/namespaces/ta/methods/vwap.ts +++ b/src/namespaces/ta/methods/vwap.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { Series } from '../../../Series'; +import { getDatePartsInTimezone } from '../../Time'; /** * VWAP - Volume Weighted Average Price @@ -54,9 +55,10 @@ export function vwap(context: any) { // Get current bar's open time to detect session changes const currentOpenTime = Series.from(context.data.openTime).get(0); - // Detect new session (new trading day) - const currentDate = new Date(currentOpenTime); - const currentSessionDate = currentDate.toISOString().slice(0, 10); // YYYY-MM-DD + // Detect new session (new trading day) using exchange timezone + const timezone = context.pine?.syminfo?.timezone || 'UTC'; + const parts = getDatePartsInTimezone(currentOpenTime, timezone); + const currentSessionDate = `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`; // Use committed state let cumulativePV = state.prevCumulativePV; diff --git a/tests/core/time-components.test.ts b/tests/core/time-components.test.ts index fddc2ab..1d2b242 100644 --- a/tests/core/time-components.test.ts +++ b/tests/core/time-components.test.ts @@ -374,14 +374,15 @@ plot(y, "year") }); describe('time_tradingday', () => { - it('returns midnight UTC of trading day', async () => { + it('returns midnight UTC of the close date (trading day the bar settles)', async () => { const { result } = await pineTS.run(($) => { const { time_tradingday } = $.pine; let td = time_tradingday; return { td }; }); - // 2019-01-07 00:00:00 UTC → midnight = 1546819200000 - expect(result.td[0]).toBe(Date.UTC(2019, 0, 7, 0, 0, 0)); + // Weekly bar opens 2019-01-07, closes (= next bar open) 2019-01-14. + // TradingView returns 00:00 UTC of the close date → 2019-01-14 + expect(result.td[0]).toBe(Date.UTC(2019, 0, 14, 0, 0, 0)); }); it('Pine Script string syntax', async () => { @@ -393,6 +394,7 @@ plot(td, "td") `; const { plots } = await pineTS.run(code); expect(plots['td']).toBeDefined(); - expect(plots['td'].data[0].value).toBe(Date.UTC(2019, 0, 7, 0, 0, 0)); + // Weekly bar opens 2019-01-07, closeTime = 2019-01-14 → time_tradingday = midnight 2019-01-14 + expect(plots['td'].data[0].value).toBe(Date.UTC(2019, 0, 14, 0, 0, 0)); }); }); diff --git a/tests/core/timezone-fixes.test.ts b/tests/core/timezone-fixes.test.ts new file mode 100644 index 0000000..5268d94 --- /dev/null +++ b/tests/core/timezone-fixes.test.ts @@ -0,0 +1,314 @@ +/** + * Tests for timezone-related fixes: + * - Fix 1: closeTime normalization (provider-level + PineTS safety-net) + * - Fix 2: time_tradingday uses closeTime instead of openTime + * - Fix 3: setTimezone() is display-only (does not affect computation) + */ +import { describe, it, expect } from 'vitest'; +import { PineTS } from '../../src/PineTS.class'; +import { Provider } from '../../src/marketData/Provider.class'; + +// ── Fix 1: closeTime normalization ────────────────────────────────────── + +describe('closeTime normalization — provider-level (MockProvider)', () => { + // MockProvider normalizes raw Binance closeTime (nextBarOpen - 1ms) to TV convention (nextBarOpen). + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, + new Date('2019-01-01').getTime(), new Date('2019-03-01').getTime()); + + it('time_close matches TV convention (next bar openTime)', async () => { + const sourceCode = ` +//@version=6 +indicator("CloseTime Test") +plot(time, "time") +plot(time_close, "time_close") +`; + const { plots } = await pineTS.run(sourceCode); + const _time = plots['time']?.data; + const _tc = plots['time_close']?.data; + + // For each bar (except last), time_close should equal next bar's time + for (let i = 0; i < _time.length - 1; i++) { + expect(_tc[i].value).toBe(_time[i + 1].value); + } + }); + + it('time_close is exactly 7 days after time for weekly bars', async () => { + const sourceCode = ` +//@version=6 +indicator("CloseTime Delta Test") +plot(time, "time") +plot(time_close, "time_close") +`; + const { plots } = await pineTS.run(sourceCode); + const _time = plots['time']?.data; + const _tc = plots['time_close']?.data; + const oneWeekMs = 7 * 24 * 60 * 60 * 1000; + + // For weekly crypto bars, closeTime - openTime = exactly 1 week + for (let i = 0; i < _time.length; i++) { + expect(_tc[i].value - _time[i].value).toBe(oneWeekMs); + } + }); + + it('first bar closeTime matches TV value exactly', async () => { + const sourceCode = ` +//@version=6 +indicator("CloseTime Exact Test") +plot(time_close, "time_close") +`; + const { plots } = await pineTS.run(sourceCode); + // First weekly bar: opens 2019-01-07, closes 2019-01-14 00:00 UTC + expect(plots['time_close'].data[0].value).toBe(new Date('2019-01-14T00:00:00Z').getTime()); + }); +}); + +describe('closeTime normalization — PineTS safety-net (array-based data)', () => { + it('computes closeTime from openTime + timeframe when not provided', async () => { + // Array-based data without closeTime + const arrayData = [ + { openTime: 1546819200000, open: 3800, high: 3900, low: 3700, close: 3850, volume: 100 }, + { openTime: 1547424000000, open: 3850, high: 3950, low: 3750, close: 3900, volume: 110 }, + { openTime: 1548028800000, open: 3900, high: 4000, low: 3800, close: 3950, volume: 120 }, + ]; + + const pineTS = new PineTS(arrayData, 'TEST', 'W'); + const { result } = await pineTS.run(($) => { + const { time_close } = $.pine; + let tc = time_close; + return { tc }; + }); + + // Safety-net: closeTime = openTime + 1W (604800000ms) + const oneWeekMs = 604_800_000; + expect(result.tc[0]).toBe(1546819200000 + oneWeekMs); + expect(result.tc[1]).toBe(1547424000000 + oneWeekMs); + }); + + it('uses provider closeTime when available (no override)', async () => { + // Array-based data WITH closeTime already set + const arrayData = [ + { openTime: 1546819200000, open: 3800, high: 3900, low: 3700, close: 3850, volume: 100, closeTime: 1547424000000 }, + { openTime: 1547424000000, open: 3850, high: 3950, low: 3750, close: 3900, volume: 110, closeTime: 1548028800000 }, + ]; + + const pineTS = new PineTS(arrayData, 'TEST', 'W'); + const { result } = await pineTS.run(($) => { + const { time_close } = $.pine; + let tc = time_close; + return { tc }; + }); + + // Should use provider-supplied closeTime, not compute from timeframe + expect(result.tc[0]).toBe(1547424000000); + expect(result.tc[1]).toBe(1548028800000); + }); + + it('defaults to 1D duration when timeframe is undefined', async () => { + const arrayData = [ + { openTime: 1546819200000, open: 100, high: 110, low: 90, close: 105, volume: 50 }, + ]; + + // No timeframe specified + const pineTS = new PineTS(arrayData); + const { result } = await pineTS.run(($) => { + const { time_close } = $.pine; + let tc = time_close; + return { tc }; + }); + + // Default: 1D = 86400000ms + expect(result.tc[0]).toBe(1546819200000 + 86_400_000); + }); +}); + +// ── Fix 2: time_tradingday uses closeTime ────────────────────────────── + +describe('time_tradingday — uses close date (TV-compatible)', () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, + new Date('2019-01-01').getTime(), new Date('2019-03-01').getTime()); + + it('returns midnight UTC of the close date, not open date', async () => { + const sourceCode = ` +//@version=6 +indicator("TradingDay Test") +plot(time, "time") +plot(time_tradingday, "td") +`; + const { plots } = await pineTS.run(sourceCode); + const _time = plots['time']?.data; + const _td = plots['td']?.data; + + // First bar opens 2019-01-07, closes 2019-01-14 + // time_tradingday should be midnight of 2019-01-14 + expect(_td[0].value).toBe(Date.UTC(2019, 0, 14, 0, 0, 0)); + // NOT the open date + expect(_td[0].value).not.toBe(_time[0].value); + }); + + it('time_tradingday equals midnight of closeTime date for each bar', async () => { + const sourceCode = ` +//@version=6 +indicator("TradingDay All Bars") +plot(time_close, "tc") +plot(time_tradingday, "td") +`; + const { plots } = await pineTS.run(sourceCode); + const _tc = plots['tc']?.data; + const _td = plots['td']?.data; + + for (let i = 0; i < _tc.length; i++) { + // closeTime → get date → midnight of that date + const closeDate = new Date(_tc[i].value); + const expectedTD = Date.UTC(closeDate.getUTCFullYear(), closeDate.getUTCMonth(), closeDate.getUTCDate(), 0, 0, 0); + expect(_td[i].value).toBe(expectedTD); + } + }); + + it('matches TV values for multiple weeks (validated against TradingView)', async () => { + const sourceCode = ` +//@version=6 +indicator("TD TV Comparison") +plot(time_tradingday, "td") +`; + const { plots } = await pineTS.run(sourceCode); + const tdData = plots['td']?.data; + + // TV expected values for BTCUSDC Weekly, chart timezone UTC: + // Bar opens 2019-01-07 → time_tradingday = 2019-01-14 00:00 UTC = 1547424000000 + // Bar opens 2019-01-14 → time_tradingday = 2019-01-21 00:00 UTC = 1548028800000 + // Bar opens 2019-01-21 → time_tradingday = 2019-01-28 00:00 UTC = 1548633600000 + // Bar opens 2019-01-28 → time_tradingday = 2019-02-04 00:00 UTC = 1549238400000 + expect(tdData[0].value).toBe(1547424000000); + expect(tdData[1].value).toBe(1548028800000); + expect(tdData[2].value).toBe(1548633600000); + expect(tdData[3].value).toBe(1549238400000); + }); +}); + +// ── Fix 3: setTimezone() is display-only ──────────────────────────────── + +describe('setTimezone() — display-only behavior (does not change computation)', () => { + + it('hour and dayofmonth are unchanged by setTimezone()', async () => { + // Run without setTimezone + const pineTSDefault = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, + new Date('2019-01-01').getTime(), new Date('2019-02-01').getTime()); + + // Run with setTimezone + const pineTSWithTZ = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, + new Date('2019-01-01').getTime(), new Date('2019-02-01').getTime()); + pineTSWithTZ.setTimezone('America/New_York'); + + const sourceCode = ` +//@version=6 +indicator("TZ Test") +plot(hour, "hour") +plot(dayofmonth, "dom") +`; + const { plots: plotsDefault } = await pineTSDefault.run(sourceCode); + const { plots: plotsWithTZ } = await pineTSWithTZ.run(sourceCode); + + // All computation values should be identical + for (let i = 0; i < plotsDefault['hour'].data.length; i++) { + expect(plotsWithTZ['hour'].data[i].value).toBe(plotsDefault['hour'].data[i].value); + expect(plotsWithTZ['dom'].data[i].value).toBe(plotsDefault['dom'].data[i].value); + } + }); + + it('timestamp() is unchanged by setTimezone()', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, + new Date('2019-01-01').getTime(), new Date('2019-02-01').getTime()); + pineTS.setTimezone('UTC-5'); + + const sourceCode = ` +//@version=6 +indicator("TZ Test") +ts = timestamp("2019-06-10 00:00") +plot(ts, "ts") +`; + const { plots } = await pineTS.run(sourceCode); + // Should resolve in exchange timezone (UTC), NOT chart timezone (UTC-5) + expect(plots['ts'].data[0].value).toBe(1560124800000); // 2019-06-10 00:00 UTC + }); + + it('time_tradingday is unchanged by setTimezone()', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, + new Date('2019-01-01').getTime(), new Date('2019-02-01').getTime()); + pineTS.setTimezone('UTC+8'); + + const sourceCode = ` +//@version=6 +indicator("TZ Test") +plot(time_tradingday, "td") +`; + const { plots } = await pineTS.run(sourceCode); + // time_tradingday uses syminfo.timezone (UTC), not chart timezone + // First bar opens 2019-01-07, closes 2019-01-14 → TD = midnight Jan 14 + expect(plots['td'].data[0].value).toBe(Date.UTC(2019, 0, 14, 0, 0, 0)); + }); + + it('weekofyear, month, year unchanged by setTimezone()', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, + new Date('2019-01-01').getTime(), new Date('2019-02-01').getTime()); + pineTS.setTimezone('Asia/Tokyo'); // UTC+9 + + const sourceCode = ` +//@version=6 +indicator("TZ Test") +plot(weekofyear, "woy") +plot(month, "month") +plot(year, "year") +`; + const { plots } = await pineTS.run(sourceCode); + // Bar opens 2019-01-07 00:00 UTC — computation uses exchange TZ (UTC) + expect(plots['woy'].data[0].value).toBe(2); + expect(plots['month'].data[0].value).toBe(1); + expect(plots['year'].data[0].value).toBe(2019); + }); +}); + +// ── closeTime normalization end-to-end (TV values) ────────────────────── + +describe('closeTime + time_close — TV-validated values', () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, + new Date('2019-01-01').getTime(), new Date('2019-04-01').getTime()); + + it('time, time_close, time_tradingday match TradingView exactly', async () => { + const sourceCode = ` +//@version=6 +indicator("TV Comparison") +plot(time, "time") +plot(time_close, "tc") +plot(time_tradingday, "td") +`; + const { plots } = await pineTS.run(sourceCode); + const _time = plots['time']?.data; + const _tc = plots['tc']?.data; + const _td = plots['td']?.data; + + // TV-validated values (BTCUSDC Weekly, UTC): + // Bar 2019-01-07: time=1546819200000, time_close=1547424000000, time_tradingday=1547424000000 + // Bar 2019-01-14: time=1547424000000, time_close=1548028800000, time_tradingday=1548028800000 + // Bar 2019-01-21: time=1548028800000, time_close=1548633600000, time_tradingday=1548633600000 + const expectedBars = [ + { time: 1546819200000, tc: 1547424000000, td: 1547424000000 }, + { time: 1547424000000, tc: 1548028800000, td: 1548028800000 }, + { time: 1548028800000, tc: 1548633600000, td: 1548633600000 }, + { time: 1548633600000, tc: 1549238400000, td: 1549238400000 }, + { time: 1549238400000, tc: 1549843200000, td: 1549843200000 }, + { time: 1549843200000, tc: 1550448000000, td: 1550448000000 }, + { time: 1550448000000, tc: 1551052800000, td: 1551052800000 }, + { time: 1551052800000, tc: 1551657600000, td: 1551657600000 }, + { time: 1551657600000, tc: 1552262400000, td: 1552262400000 }, + { time: 1552262400000, tc: 1552867200000, td: 1552867200000 }, + { time: 1552867200000, tc: 1553472000000, td: 1553472000000 }, + { time: 1553472000000, tc: 1554076800000, td: 1554076800000 }, + ]; + + for (let i = 0; i < expectedBars.length; i++) { + expect(_time[i].value).toBe(expectedBars[i].time); + expect(_tc[i].value).toBe(expectedBars[i].tc); + expect(_td[i].value).toBe(expectedBars[i].td); + } + }); +}); diff --git a/tests/core/timezone.test.ts b/tests/core/timezone.test.ts new file mode 100644 index 0000000..1c3aa41 --- /dev/null +++ b/tests/core/timezone.test.ts @@ -0,0 +1,197 @@ +import { describe, it, expect } from 'vitest'; +import { PineTS } from '../../src/PineTS.class'; +import { Provider } from '../../src/marketData/Provider.class'; + +// BTCUSDC Weekly — data range covers 2018-12-10 to 2019-06-30 +// Weekly bars start on Mondays. First bar in range: 2018-12-10 (Mon) + +describe('Timezone — timestamp(dateString) in UTC', () => { + // TV expected values (chart timezone: UTC, BTCUSDC Weekly): + // timestamp("2019-06-10 00:00") = 1560124800000 (UTC midnight) + // timestamp(2019, 6, 10, 0, 0, 0) = 1560124800000 + // timestamp("America/New_York", 2019, 6, 10, 0, 0, 0) = 1560139200000 (EDT midnight = UTC+4h) + + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2018-12-10').getTime(), new Date('2019-06-30').getTime()); + + it('timestamp(dateString) resolves to UTC when chart timezone is UTC', async () => { + const sourceCode = ` +//@version=6 +indicator("Timezone Test") +ts = timestamp("2019-06-10 00:00") +plot(ts, "ts") +`; + const { plots } = await pineTS.run(sourceCode); + const tsData = plots['ts']?.data; + // All bars should produce the same constant value + expect(tsData[0].value).toBe(1560124800000); + }); + + it('timestamp(year, month, day) matches timestamp(dateString) in UTC', async () => { + const sourceCode = ` +//@version=6 +indicator("Timezone Test") +ts1 = timestamp("2019-06-10 00:00") +ts2 = timestamp(2019, 6, 10, 0, 0, 0) +plot(ts1, "ts1") +plot(ts2, "ts2") +`; + const { plots } = await pineTS.run(sourceCode); + expect(plots['ts1'].data[0].value).toBe(plots['ts2'].data[0].value); + expect(plots['ts1'].data[0].value).toBe(1560124800000); + }); + + it('timestamp with explicit IANA timezone offsets correctly', async () => { + const sourceCode = ` +//@version=6 +indicator("Timezone Test") +ts = timestamp("America/New_York", 2019, 6, 10, 0, 0, 0) +plot(ts, "ts") +`; + const { plots } = await pineTS.run(sourceCode); + // Midnight in America/New_York (EDT, UTC-4 in June) = 04:00 UTC + expect(plots['ts'].data[0].value).toBe(1560139200000); + }); +}); + +describe('Timezone — time component functions in UTC (validated against TradingView)', () => { + // TV data: BTCUSDC Weekly, chart timezone UTC + // Bar 2019-01-07: hour=0, dayofmonth=7, dayofweek=2(Mon), month=1, year=2019, weekofyear=2 + // Bar 2019-02-04: hour=0, dayofmonth=4, dayofweek=2(Mon), month=2, year=2019, weekofyear=6 + // Bar 2019-03-04: hour=0, dayofmonth=4, dayofweek=2(Mon), month=3, year=2019, weekofyear=10 + + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2018-12-10').getTime(), new Date('2019-04-01').getTime()); + + it('hour, dayofmonth, dayofweek, month, year, weekofyear match TradingView', async () => { + const sourceCode = ` +//@version=6 +indicator("Time Components Test") +plot(hour, "hour") +plot(dayofmonth, "dom") +plot(dayofweek, "dow") +plot(month, "month") +plot(year, "year") +plot(weekofyear, "woy") +`; + const { plots } = await pineTS.run(sourceCode); + const _hour = plots['hour']?.data; + const _dom = plots['dom']?.data; + const _dow = plots['dow']?.data; + const _month = plots['month']?.data; + const _year = plots['year']?.data; + const _woy = plots['woy']?.data; + + const startDate = new Date('2019-01-07').getTime(); + const endDate = new Date('2019-03-25').getTime(); + + let plotdata_str = ''; + for (let i = 0; i < _hour.length; i++) { + const time = _hour[i].time; + if (time < startDate || time > endDate) continue; + const str_time = new Date(time).toISOString().slice(0, -1) + '-00:00'; + plotdata_str += `[${str_time}]: ${_hour[i].value} ${_dom[i].value} ${_dow[i].value} ${_month[i].value} ${_year[i].value} ${_woy[i].value}\n`; + } + + // Expected from TradingView (BTCUSDC Weekly, UTC timezone) + // Format: hour dayofmonth dayofweek month year weekofyear + const expected_plot = `[2019-01-07T00:00:00.000-00:00]: 0 7 2 1 2019 2 +[2019-01-14T00:00:00.000-00:00]: 0 14 2 1 2019 3 +[2019-01-21T00:00:00.000-00:00]: 0 21 2 1 2019 4 +[2019-01-28T00:00:00.000-00:00]: 0 28 2 1 2019 5 +[2019-02-04T00:00:00.000-00:00]: 0 4 2 2 2019 6 +[2019-02-11T00:00:00.000-00:00]: 0 11 2 2 2019 7 +[2019-02-18T00:00:00.000-00:00]: 0 18 2 2 2019 8 +[2019-02-25T00:00:00.000-00:00]: 0 25 2 2 2019 9 +[2019-03-04T00:00:00.000-00:00]: 0 4 2 3 2019 10 +[2019-03-11T00:00:00.000-00:00]: 0 11 2 3 2019 11 +[2019-03-18T00:00:00.000-00:00]: 0 18 2 3 2019 12 +[2019-03-25T00:00:00.000-00:00]: 0 25 2 3 2019 13`; + + expect(plotdata_str.trim()).toEqual(expected_plot.trim()); + }); +}); + +describe('Timezone — setTimezone() does NOT change computation (display-only)', () => { + // TradingView behavior: changing chart timezone on crypto (BTCUSDC) has zero effect + // on Pine Script computation functions (hour, dayofmonth, timestamp, etc.). + // All functions use syminfo.timezone (= Etc/UTC for crypto). + // setTimezone() only changes log timestamp display formatting. + + it('hour and dayofmonth stay in exchange timezone (UTC) when chart TZ is UTC+5', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2019-01-01').getTime(), new Date('2019-02-01').getTime()); + pineTS.setTimezone('UTC+5'); + + const sourceCode = ` +//@version=6 +indicator("TZ Test") +plot(hour, "hour") +plot(dayofmonth, "dom") +`; + const { plots } = await pineTS.run(sourceCode); + // Bar at 2019-01-07 00:00 UTC — computation uses exchange TZ (UTC), NOT chart TZ + expect(plots['hour'].data[0].value).toBe(0); + expect(plots['dom'].data[0].value).toBe(7); + }); + + it('hour and dayofmonth stay in exchange timezone (UTC) when chart TZ is UTC-5', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2019-01-01').getTime(), new Date('2019-02-01').getTime()); + pineTS.setTimezone('UTC-5'); + + const sourceCode = ` +//@version=6 +indicator("TZ Test") +plot(hour, "hour") +plot(dayofmonth, "dom") +`; + const { plots } = await pineTS.run(sourceCode); + // Computation still uses exchange TZ (UTC), chart TZ is display-only + expect(plots['hour'].data[0].value).toBe(0); + expect(plots['dom'].data[0].value).toBe(7); + }); + + it('IANA chart timezone does not affect computation', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2019-01-01').getTime(), new Date('2019-04-01').getTime()); + pineTS.setTimezone('America/New_York'); + + const sourceCode = ` +//@version=6 +indicator("TZ Test") +plot(hour, "hour") +`; + const { plots } = await pineTS.run(sourceCode); + // Jan 7: exchange is UTC → hour = 0 regardless of chart TZ + expect(plots['hour'].data[0].value).toBe(0); + }); +}); + +describe('Timezone — timestamp(dateString) with non-UTC chart timezone', () => { + it('timestamp(dateString) always uses exchange timezone, ignoring chart timezone', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2019-01-01').getTime(), new Date('2019-02-01').getTime()); + pineTS.setTimezone('America/New_York'); + + const sourceCode = ` +//@version=6 +indicator("TZ Test") +ts = timestamp("2019-06-10 00:00") +plot(ts, "ts") +`; + const { plots } = await pineTS.run(sourceCode); + // "2019-06-10 00:00" resolves in exchange timezone (UTC), NOT chart timezone + // = 2019-06-10 00:00 UTC = 1560124800000 + expect(plots['ts'].data[0].value).toBe(1560124800000); + }); + + it('timestamp with explicit timezone arg works regardless of chart timezone', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2019-01-01').getTime(), new Date('2019-02-01').getTime()); + pineTS.setTimezone('America/New_York'); + + const sourceCode = ` +//@version=6 +indicator("TZ Test") +ts = timestamp("UTC", 2019, 6, 10, 0, 0, 0) +plot(ts, "ts") +`; + const { plots } = await pineTS.run(sourceCode); + // Explicit "UTC" arg always uses UTC + expect(plots['ts'].data[0].value).toBe(1560124800000); + }); +}); From 6bebf61020a72fc634efeeafe261ddfa62ad4f98 Mon Sep 17 00:00:00 2001 From: alaa-eddine Date: Thu, 12 Mar 2026 13:00:19 +0100 Subject: [PATCH 4/4] changelog + version update --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d73cf65..7f30fb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## [0.9.5] - 2026-03-12 - Time & Timezone Fixes + +### Added + +- **`PineTS.setTimezone(timezone)`**: Display-only chart timezone (like TradingView's timezone picker). Accepts IANA names, `UTC±N` offsets, or `'UTC'`. Only affects `log.*` timestamp formatting — computation functions (`timestamp()`, `hour`, `dayofmonth`, `time_tradingday`, etc.) always use the exchange timezone from `syminfo.timezone`. + +### Fixed + +- **`closeTime` Normalization**: `BinanceProvider` and `MockProvider` now normalize `closeTime` to the TradingView convention (`closeTime = nextBar.openTime`) instead of Binance's raw `nextBarOpen - 1ms`. `IProvider` docs updated to specify this convention. For array-based data missing `closeTime`, `PineTS` now estimates it as `openTime + timeframe duration` (falls back to 1D when unknown). +- **`time_tradingday` Uses Close Date**: Was returning midnight UTC of the bar's open date. Now correctly returns midnight UTC of the **close date** (matching TradingView). E.g. a weekly bar opening `2019-01-07` → closes `2019-01-14` → `time_tradingday = 2019-01-14 00:00 UTC`. +- **`timestamp(dateString)` Exchange Timezone**: Date strings like `"2019-06-10 00:00"` were parsed in the host system's local timezone. Now explicitly resolved in the exchange timezone (`syminfo.timezone`), matching TradingView behaviour. Strings with explicit offsets or `Z` are honoured as-is. +- **`TimeHelper` as `Series`** ([#156](https://github.com/QuantForgeOrg/PineTS/issues/156)): `Series.from()` now unwraps NAMESPACES_LIKE dual-use objects (`time`, `time_close`, etc.) by detecting the `.__value` Series property, instead of wrapping the object itself. Added a null-guard to prevent a crash when the source is `null`. (contribution by [@dcaoyuan](https://github.com/dcaoyuan)) +- **`Log` Timestamps Use Chart Timezone**: `log.info/warning/error` hardcoded UTC for bar timestamp prefixes. They now respect the timezone set via `setTimezone()`, falling back to the exchange timezone. +- **`Etc/UTC` Alias**: Added `'Etc/UTC'` to the fast-path UTC check in `getDatePartsInTimezone()`, fixing date-part calculations for providers that use the canonical `Etc/UTC` identifier (common for crypto). +- **`ta.vwap` Session Timezone**: VWAP day-boundary detection now uses `getDatePartsInTimezone(openTime, syminfo.timezone)` instead of `toISOString().slice(0, 10)`, so session resets are correct for non-UTC exchanges. + +--- + ## [0.9.4] - 2026-03-11 - Color Namespace, Transpiler Overhaul, request.security & Drawing Improvements ### Added diff --git a/package.json b/package.json index 75b3ae2..4447230 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pinets", - "version": "0.9.4", + "version": "0.9.5", "description": "Run Pine Script anywhere. PineTS is an open-source transpiler and runtime that brings Pine Script logic to Node.js and the browser with 1:1 syntax compatibility. Reliably write, port, and run indicators or strategies on your own infrastructure.", "keywords": [ "Pine Script",