explore_ultrashort.py 152 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932
  1. from __future__ import annotations
  2. import argparse
  3. import json
  4. from dataclasses import dataclass
  5. from math import sqrt
  6. from pathlib import Path
  7. import pandas as pd
  8. from okx_codex_trader.bbmr_report import BBMRConfig, run_bbmr_segment
  9. from okx_codex_trader.bbsb_report import BBSBConfig, run_bbsb_segment
  10. from okx_codex_trader.donchian_report import DonchianConfig, run_donchian_segment
  11. from okx_codex_trader.ema_pullback_report import EMAPullbackConfig, run_ema_pullback_segment
  12. from okx_codex_trader.models import Candle
  13. from okx_codex_trader.okx_client import OkxClient
  14. from okx_codex_trader.rsi2_report import RSI2Config, _compute_rsi, run_rsi2_segment
  15. from okx_codex_trader.sampled_report import SegmentResult, mark_to_market, sample_segments, trade_equity
  16. SYMBOLS = ("BTC-USDT-SWAP", "ETH-USDT-SWAP")
  17. BARS = ("1m", "3m", "5m")
  18. ANALYSIS_BARS = ("3m", "5m", "15m")
  19. HISTORY_LIMIT = 4000
  20. SEGMENTS = 8
  21. WINDOW_SIZE = 240
  22. LEVERAGE = 3
  23. INITIAL_EQUITY = 10_000.0
  24. ROBUST_HISTORY_LIMIT = 50_000
  25. GROSS_RETURN_NOTE = "Gross-return backtest only: fees, slippage, and funding rates are excluded."
  26. MINUTES_PER_YEAR = 365 * 24 * 60
  27. CANDLE_CACHE_DIR = Path("data/okx-candles")
  28. CANDLE_BAR_MS = {
  29. "1m": 60_000,
  30. "3m": 180_000,
  31. "5m": 300_000,
  32. "15m": 900_000,
  33. "30m": 1_800_000,
  34. "1H": 3_600_000,
  35. "4H": 14_400_000,
  36. "1D": 86_400_000,
  37. }
  38. @dataclass(frozen=True)
  39. class Candidate:
  40. name: str
  41. warmup_bars: int
  42. run: object
  43. @dataclass(frozen=True)
  44. class PairCandidate:
  45. name: str
  46. warmup_bars: int
  47. run: object
  48. def _format_ts(ts: int) -> str:
  49. return pd.to_datetime(ts, unit="ms", utc=True).strftime("%Y-%m-%d %H:%M")
  50. def candle_cache_file(cache_dir: Path, symbol: str, bar: str) -> Path:
  51. return cache_dir / symbol / f"{bar}.csv"
  52. def candle_cache_meta_file(cache_dir: Path, symbol: str, bar: str) -> Path:
  53. return cache_dir / symbol / f"{bar}.meta.json"
  54. def load_cached_candles(cache_dir: Path, symbol: str, bar: str) -> tuple[list[Candle], bool]:
  55. cache_file = candle_cache_file(cache_dir, symbol, bar)
  56. if not cache_file.exists():
  57. return [], False
  58. frame = pd.read_csv(cache_file)
  59. candles = [
  60. Candle(
  61. symbol=symbol,
  62. ts=int(row.ts),
  63. open=float(row.open),
  64. high=float(row.high),
  65. low=float(row.low),
  66. close=float(row.close),
  67. volume=float(row.volume),
  68. )
  69. for row in frame.itertuples(index=False)
  70. ]
  71. meta_file = candle_cache_meta_file(cache_dir, symbol, bar)
  72. history_exhausted = False
  73. if meta_file.exists():
  74. with meta_file.open("r", encoding="utf-8") as handle:
  75. history_exhausted = bool(json.load(handle).get("history_exhausted"))
  76. return candles, history_exhausted
  77. def save_cached_candles(cache_dir: Path, symbol: str, bar: str, candles: list[Candle], history_exhausted: bool) -> None:
  78. cache_file = candle_cache_file(cache_dir, symbol, bar)
  79. cache_file.parent.mkdir(parents=True, exist_ok=True)
  80. frame = pd.DataFrame(
  81. [
  82. {
  83. "ts": candle.ts,
  84. "open": candle.open,
  85. "high": candle.high,
  86. "low": candle.low,
  87. "close": candle.close,
  88. "volume": candle.volume,
  89. }
  90. for candle in sorted(candles, key=lambda candle: candle.ts)
  91. ]
  92. ).drop_duplicates("ts", keep="last")
  93. frame.to_csv(cache_file, index=False)
  94. meta = {
  95. "symbol": symbol,
  96. "bar": bar,
  97. "history_exhausted": history_exhausted,
  98. "rows": len(frame),
  99. "first_ts": int(frame["ts"].iloc[0]) if len(frame) else None,
  100. "last_ts": int(frame["ts"].iloc[-1]) if len(frame) else None,
  101. }
  102. with candle_cache_meta_file(cache_dir, symbol, bar).open("w", encoding="utf-8") as handle:
  103. json.dump(meta, handle, separators=(",", ":"))
  104. def latest_bridge_count(cached: list[Candle], latest_last_ts: int, interval: int) -> int:
  105. bridge_from_ts = max(candle.ts for candle in cached)
  106. for left, right in zip(cached, cached[1:]):
  107. if right.ts - left.ts != interval:
  108. bridge_from_ts = left.ts
  109. return ((latest_last_ts - bridge_from_ts) // interval) + 1
  110. def get_candles_cached(
  111. client: OkxClient,
  112. symbol: str,
  113. bar: str,
  114. limit: int,
  115. cache_dir: Path = CANDLE_CACHE_DIR,
  116. ) -> list[Candle]:
  117. cached, history_exhausted = load_cached_candles(cache_dir, symbol, bar)
  118. if cached and (len(cached) >= limit or history_exhausted):
  119. latest = client.get_candles(symbol, bar, min(300, limit))
  120. interval = CANDLE_BAR_MS[bar]
  121. if latest:
  122. latest_last_ts = max(candle.ts for candle in latest)
  123. needed_latest_count = latest_bridge_count(cached, latest_last_ts, interval)
  124. if needed_latest_count > len(latest):
  125. latest = client.get_candles(symbol, bar, needed_latest_count)
  126. merged = {candle.ts: candle for candle in cached}
  127. for candle in latest:
  128. merged[candle.ts] = candle
  129. candles = sorted(merged.values(), key=lambda candle: candle.ts)
  130. save_cached_candles(cache_dir, symbol, bar, candles, history_exhausted)
  131. return candles[-limit:] if len(candles) >= limit else candles
  132. fetched = client.get_candles(symbol, bar, limit)
  133. history_exhausted = len(fetched) < limit
  134. save_cached_candles(cache_dir, symbol, bar, fetched, history_exhausted)
  135. return fetched
  136. def align_pair_candles(left: list[Candle], right: list[Candle]) -> tuple[list[Candle], list[Candle]]:
  137. right_by_ts = {candle.ts: candle for candle in right}
  138. left_aligned: list[Candle] = []
  139. right_aligned: list[Candle] = []
  140. for candle in left:
  141. other = right_by_ts.get(candle.ts)
  142. if other is None:
  143. continue
  144. left_aligned.append(candle)
  145. right_aligned.append(other)
  146. return left_aligned, right_aligned
  147. def _trade(
  148. *,
  149. trades: list[dict[str, object]],
  150. exits: list[dict[str, object]],
  151. position: dict[str, object],
  152. candle: Candle,
  153. exit_price: float,
  154. leverage: int,
  155. ) -> tuple[float, bool]:
  156. exit_equity = trade_equity(
  157. side=str(position["side"]),
  158. margin_used=float(position["margin_used"]),
  159. entry_price=float(position["entry_price"]),
  160. exit_price=exit_price,
  161. leverage=leverage,
  162. )
  163. trades.append(
  164. {
  165. "side": "Long" if position["side"] == "long" else "Short",
  166. "entry_time": _format_ts(int(position["entry_time"])),
  167. "exit_time": _format_ts(candle.ts),
  168. "entry_price": round(float(position["entry_price"]), 4),
  169. "exit_price": round(exit_price, 4),
  170. "pnl": round(exit_equity - float(position["margin_used"]), 4),
  171. "return_pct": round((exit_equity - float(position["margin_used"])) / float(position["margin_used"]) * 100, 4),
  172. }
  173. )
  174. exits.append({"ts": candle.ts, "price": exit_price, "side": position["side"]})
  175. return exit_equity, exit_equity > float(position["margin_used"])
  176. def _close_partial_trade(
  177. *,
  178. trades: list[dict[str, object]],
  179. exits: list[dict[str, object]],
  180. position: dict[str, object],
  181. account_equity: float,
  182. candle: Candle,
  183. exit_price: float,
  184. leverage: int,
  185. ) -> tuple[float, bool]:
  186. margin_used = float(position["margin_used"])
  187. exit_equity = trade_equity(
  188. side=str(position["side"]),
  189. margin_used=margin_used,
  190. entry_price=float(position["entry_price"]),
  191. exit_price=exit_price,
  192. leverage=leverage,
  193. )
  194. pnl = exit_equity - margin_used
  195. trades.append(
  196. {
  197. "side": "Long" if position["side"] == "long" else "Short",
  198. "entry_time": _format_ts(int(position["entry_time"])),
  199. "exit_time": _format_ts(candle.ts),
  200. "entry_price": round(float(position["entry_price"]), 4),
  201. "exit_price": round(exit_price, 4),
  202. "pnl": round(pnl, 4),
  203. "return_pct": round(pnl / account_equity * 100, 4),
  204. "cost_weight": round(margin_used / account_equity, 8),
  205. }
  206. )
  207. exits.append({"ts": candle.ts, "price": exit_price, "side": position["side"]})
  208. return account_equity + pnl, pnl > 0.0
  209. def run_range_momentum_segment(
  210. *,
  211. candles: list[Candle],
  212. leverage: int,
  213. warmup_bars: int,
  214. lookback: int,
  215. take_profit_pct: float,
  216. stop_loss_pct: float,
  217. ) -> SegmentResult:
  218. highs = pd.Series([candle.high for candle in candles], dtype=float)
  219. lows = pd.Series([candle.low for candle in candles], dtype=float)
  220. entry_high = highs.shift(1).rolling(lookback).max().tolist()
  221. entry_low = lows.shift(1).rolling(lookback).min().tolist()
  222. equity = INITIAL_EQUITY
  223. ending_equity = equity
  224. peak_equity = equity
  225. max_drawdown = 0.0
  226. wins = 0
  227. trades: list[dict[str, object]] = []
  228. entries: list[dict[str, object]] = []
  229. exits: list[dict[str, object]] = []
  230. equity_curve: list[dict[str, float | int]] = []
  231. position: dict[str, object] | None = None
  232. pending_entry_side: str | None = None
  233. for index in range(warmup_bars, len(candles)):
  234. candle = candles[index]
  235. if pending_entry_side is not None and position is None and equity > 0.0:
  236. position = {
  237. "side": pending_entry_side,
  238. "entry_time": candle.ts,
  239. "entry_price": candle.open,
  240. "entry_index": index,
  241. "margin_used": equity,
  242. "stop_price": candle.open * (1 - stop_loss_pct if pending_entry_side == "long" else 1 + stop_loss_pct),
  243. "take_profit_price": candle.open * (1 + take_profit_pct if pending_entry_side == "long" else 1 - take_profit_pct),
  244. }
  245. entries.append({"ts": candle.ts, "price": candle.open, "side": pending_entry_side})
  246. pending_entry_side = None
  247. current_equity = equity
  248. if position is not None:
  249. stop_hit = (
  250. position["side"] == "long" and candle.low <= float(position["stop_price"])
  251. ) or (
  252. position["side"] == "short" and candle.high >= float(position["stop_price"])
  253. )
  254. take_hit = (
  255. position["side"] == "long" and candle.high >= float(position["take_profit_price"])
  256. ) or (
  257. position["side"] == "short" and candle.low <= float(position["take_profit_price"])
  258. )
  259. if stop_hit or take_hit:
  260. exit_price = float(position["stop_price"] if stop_hit else position["take_profit_price"])
  261. equity, won = _trade(
  262. trades=trades,
  263. exits=exits,
  264. position=position,
  265. candle=candle,
  266. exit_price=exit_price,
  267. leverage=leverage,
  268. )
  269. wins += 1 if won else 0
  270. current_equity = equity
  271. position = None
  272. if position is not None:
  273. current_equity = mark_to_market(
  274. side=str(position["side"]),
  275. margin_used=float(position["margin_used"]),
  276. entry_price=float(position["entry_price"]),
  277. mark_price=candle.close,
  278. leverage=leverage,
  279. )
  280. peak_equity = max(peak_equity, current_equity)
  281. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  282. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  283. ending_equity = current_equity
  284. if index == len(candles) - 1 or position is not None or equity <= 0.0:
  285. continue
  286. if entry_high[index] == entry_high[index] and candle.close > float(entry_high[index]):
  287. pending_entry_side = "long"
  288. elif entry_low[index] == entry_low[index] and candle.close < float(entry_low[index]):
  289. pending_entry_side = "short"
  290. trade_count = len(trades)
  291. return SegmentResult(
  292. trade_count=trade_count,
  293. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  294. win_rate=wins / trade_count if trade_count else 0.0,
  295. max_drawdown=max_drawdown,
  296. trades=trades,
  297. open_position=position,
  298. candles=candles[warmup_bars:],
  299. equity_curve=equity_curve,
  300. entries=entries,
  301. exits=exits,
  302. )
  303. def run_vwap_reversion_segment(
  304. *,
  305. candles: list[Candle],
  306. leverage: int,
  307. warmup_bars: int,
  308. window: int,
  309. entry_z: float,
  310. exit_z: float,
  311. stop_loss_pct: float,
  312. ) -> SegmentResult:
  313. closes = pd.Series([candle.close for candle in candles], dtype=float)
  314. volumes = pd.Series([candle.volume for candle in candles], dtype=float)
  315. vwap = (closes * volumes).rolling(window).sum() / volumes.rolling(window).sum()
  316. deviation = ((closes - vwap) / vwap).tolist()
  317. stdev = pd.Series(deviation, dtype=float).rolling(window).std(ddof=0).tolist()
  318. zscore = [
  319. float("nan") if dev != dev or std != std or std == 0.0 else dev / std
  320. for dev, std in zip(deviation, stdev)
  321. ]
  322. equity = INITIAL_EQUITY
  323. ending_equity = equity
  324. peak_equity = equity
  325. max_drawdown = 0.0
  326. wins = 0
  327. trades: list[dict[str, object]] = []
  328. entries: list[dict[str, object]] = []
  329. exits: list[dict[str, object]] = []
  330. equity_curve: list[dict[str, float | int]] = []
  331. position: dict[str, object] | None = None
  332. pending_entry_side: str | None = None
  333. pending_exit = False
  334. for index in range(warmup_bars, len(candles)):
  335. candle = candles[index]
  336. if pending_exit and position is not None:
  337. equity, won = _trade(
  338. trades=trades,
  339. exits=exits,
  340. position=position,
  341. candle=candle,
  342. exit_price=candle.open,
  343. leverage=leverage,
  344. )
  345. wins += 1 if won else 0
  346. position = None
  347. pending_exit = False
  348. if pending_entry_side is not None and position is None and equity > 0.0:
  349. position = {
  350. "side": pending_entry_side,
  351. "entry_time": candle.ts,
  352. "entry_price": candle.open,
  353. "margin_used": equity,
  354. "stop_price": candle.open * (1 - stop_loss_pct if pending_entry_side == "long" else 1 + stop_loss_pct),
  355. }
  356. entries.append({"ts": candle.ts, "price": candle.open, "side": pending_entry_side})
  357. pending_entry_side = None
  358. current_equity = equity
  359. if position is not None:
  360. stop_hit = (
  361. position["side"] == "long" and candle.low <= float(position["stop_price"])
  362. ) or (
  363. position["side"] == "short" and candle.high >= float(position["stop_price"])
  364. )
  365. if stop_hit:
  366. equity, won = _trade(
  367. trades=trades,
  368. exits=exits,
  369. position=position,
  370. candle=candle,
  371. exit_price=float(position["stop_price"]),
  372. leverage=leverage,
  373. )
  374. wins += 1 if won else 0
  375. current_equity = equity
  376. position = None
  377. if position is not None:
  378. current_equity = mark_to_market(
  379. side=str(position["side"]),
  380. margin_used=float(position["margin_used"]),
  381. entry_price=float(position["entry_price"]),
  382. mark_price=candle.close,
  383. leverage=leverage,
  384. )
  385. peak_equity = max(peak_equity, current_equity)
  386. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  387. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  388. ending_equity = current_equity
  389. if index == len(candles) - 1 or equity <= 0.0:
  390. continue
  391. current_z = zscore[index]
  392. if current_z != current_z:
  393. continue
  394. if position is not None:
  395. if (position["side"] == "long" and current_z >= -exit_z) or (
  396. position["side"] == "short" and current_z <= exit_z
  397. ):
  398. pending_exit = True
  399. continue
  400. if current_z <= -entry_z:
  401. pending_entry_side = "long"
  402. elif current_z >= entry_z:
  403. pending_entry_side = "short"
  404. trade_count = len(trades)
  405. return SegmentResult(
  406. trade_count=trade_count,
  407. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  408. win_rate=wins / trade_count if trade_count else 0.0,
  409. max_drawdown=max_drawdown,
  410. trades=trades,
  411. open_position=position,
  412. candles=candles[warmup_bars:],
  413. equity_curve=equity_curve,
  414. entries=entries,
  415. exits=exits,
  416. )
  417. def run_rsi2_side_segment(
  418. *,
  419. candles: list[Candle],
  420. leverage: int,
  421. warmup_bars: int,
  422. config: RSI2Config,
  423. side_mode: str,
  424. ) -> SegmentResult:
  425. result = run_rsi2_segment(
  426. candles=candles,
  427. leverage=leverage,
  428. warmup_bars=warmup_bars,
  429. config=config,
  430. )
  431. if side_mode == "both":
  432. return result
  433. closes = pd.Series([candle.close for candle in candles], dtype=float)
  434. trend = closes.rolling(config.trend_sma).mean().tolist()
  435. rsi_values = _compute_rsi(closes, config.rsi_length)
  436. equity = config.initial_equity
  437. ending_equity = equity
  438. peak_equity = equity
  439. max_drawdown = 0.0
  440. wins = 0
  441. trades: list[dict[str, object]] = []
  442. entries: list[dict[str, object]] = []
  443. exits: list[dict[str, object]] = []
  444. equity_curve: list[dict[str, float | int]] = []
  445. position: dict[str, object] | None = None
  446. pending_entry_side: str | None = None
  447. pending_exit = False
  448. allowed_side = "long" if side_mode == "long" else "short"
  449. for index in range(warmup_bars, len(candles)):
  450. candle = candles[index]
  451. if pending_exit and position is not None:
  452. equity, won = _trade(
  453. trades=trades,
  454. exits=exits,
  455. position=position,
  456. candle=candle,
  457. exit_price=candle.open,
  458. leverage=leverage,
  459. )
  460. wins += 1 if won else 0
  461. position = None
  462. pending_exit = False
  463. if pending_entry_side is not None and position is None and equity > 0.0:
  464. position = {
  465. "side": pending_entry_side,
  466. "entry_time": candle.ts,
  467. "entry_price": candle.open,
  468. "margin_used": equity,
  469. }
  470. entries.append({"ts": candle.ts, "price": candle.open, "side": pending_entry_side})
  471. pending_entry_side = None
  472. current_equity = equity
  473. if position is not None:
  474. current_equity = mark_to_market(
  475. side=str(position["side"]),
  476. margin_used=float(position["margin_used"]),
  477. entry_price=float(position["entry_price"]),
  478. mark_price=candle.close,
  479. leverage=leverage,
  480. )
  481. peak_equity = max(peak_equity, current_equity)
  482. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  483. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  484. ending_equity = current_equity
  485. if index == len(candles) - 1 or equity <= 0.0:
  486. continue
  487. current_rsi = rsi_values[index]
  488. current_trend = trend[index]
  489. if current_rsi != current_rsi or current_trend != current_trend:
  490. continue
  491. if position is not None:
  492. if (position["side"] == "long" and current_rsi >= config.exit_rsi) or (
  493. position["side"] == "short" and current_rsi <= config.exit_rsi
  494. ):
  495. pending_exit = True
  496. continue
  497. if allowed_side == "long" and candle.close > float(current_trend) and current_rsi <= config.rsi_long_threshold:
  498. pending_entry_side = "long"
  499. elif allowed_side == "short" and candle.close < float(current_trend) and current_rsi >= config.rsi_short_threshold:
  500. pending_entry_side = "short"
  501. trade_count = len(trades)
  502. return SegmentResult(
  503. trade_count=trade_count,
  504. total_return=(ending_equity - config.initial_equity) / config.initial_equity,
  505. win_rate=(wins / trade_count) if trade_count else 0.0,
  506. max_drawdown=max_drawdown,
  507. trades=trades,
  508. open_position=position,
  509. candles=candles[warmup_bars:],
  510. equity_curve=equity_curve,
  511. entries=entries,
  512. exits=exits,
  513. )
  514. def run_rsi2_long_guarded_segment(
  515. *,
  516. candles: list[Candle],
  517. leverage: int,
  518. warmup_bars: int,
  519. trend_sma: int,
  520. rsi_threshold: float,
  521. exit_rsi: float,
  522. stop_loss_pct: float,
  523. max_hold_bars: int,
  524. ) -> SegmentResult:
  525. closes = pd.Series([candle.close for candle in candles], dtype=float)
  526. trend = closes.rolling(trend_sma).mean().tolist()
  527. rsi_values = _compute_rsi(closes, 2)
  528. equity = INITIAL_EQUITY
  529. ending_equity = equity
  530. peak_equity = equity
  531. max_drawdown = 0.0
  532. wins = 0
  533. trades: list[dict[str, object]] = []
  534. entries: list[dict[str, object]] = []
  535. exits: list[dict[str, object]] = []
  536. equity_curve: list[dict[str, float | int]] = []
  537. position: dict[str, object] | None = None
  538. pending_entry = False
  539. pending_exit = False
  540. for index in range(warmup_bars, len(candles)):
  541. candle = candles[index]
  542. if pending_exit and position is not None:
  543. equity, won = _trade(
  544. trades=trades,
  545. exits=exits,
  546. position=position,
  547. candle=candle,
  548. exit_price=candle.open,
  549. leverage=leverage,
  550. )
  551. wins += 1 if won else 0
  552. position = None
  553. pending_exit = False
  554. if pending_entry and position is None and equity > 0.0:
  555. position = {
  556. "side": "long",
  557. "entry_time": candle.ts,
  558. "entry_price": candle.open,
  559. "entry_index": index,
  560. "margin_used": equity,
  561. "stop_price": candle.open * (1 - stop_loss_pct),
  562. }
  563. entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
  564. pending_entry = False
  565. current_equity = equity
  566. if position is not None and candle.low <= float(position["stop_price"]):
  567. equity, won = _trade(
  568. trades=trades,
  569. exits=exits,
  570. position=position,
  571. candle=candle,
  572. exit_price=float(position["stop_price"]),
  573. leverage=leverage,
  574. )
  575. wins += 1 if won else 0
  576. current_equity = equity
  577. position = None
  578. if position is not None:
  579. current_equity = mark_to_market(
  580. side="long",
  581. margin_used=float(position["margin_used"]),
  582. entry_price=float(position["entry_price"]),
  583. mark_price=candle.close,
  584. leverage=leverage,
  585. )
  586. peak_equity = max(peak_equity, current_equity)
  587. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  588. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  589. ending_equity = current_equity
  590. if index == len(candles) - 1 or equity <= 0.0:
  591. continue
  592. current_rsi = rsi_values[index]
  593. current_trend = trend[index]
  594. if current_rsi != current_rsi or current_trend != current_trend:
  595. continue
  596. if position is not None:
  597. held_bars = index - int(position["entry_index"])
  598. if current_rsi >= exit_rsi or held_bars >= max_hold_bars:
  599. pending_exit = True
  600. continue
  601. if candle.close > float(current_trend) and current_rsi <= rsi_threshold:
  602. pending_entry = True
  603. trade_count = len(trades)
  604. return SegmentResult(
  605. trade_count=trade_count,
  606. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  607. win_rate=(wins / trade_count) if trade_count else 0.0,
  608. max_drawdown=max_drawdown,
  609. trades=trades,
  610. open_position=position,
  611. candles=candles[warmup_bars:],
  612. equity_curve=equity_curve,
  613. entries=entries,
  614. exits=exits,
  615. )
  616. def run_rsi2_long_guarded_twap_segment(
  617. *,
  618. candles: list[Candle],
  619. leverage: int,
  620. warmup_bars: int,
  621. trend_sma: int,
  622. rsi_threshold: float,
  623. exit_rsi: float,
  624. stop_loss_pct: float,
  625. max_hold_bars: int,
  626. entry_slices: int,
  627. ) -> SegmentResult:
  628. closes = pd.Series([candle.close for candle in candles], dtype=float)
  629. trend = closes.rolling(trend_sma).mean().tolist()
  630. rsi_values = _compute_rsi(closes, 2)
  631. equity = INITIAL_EQUITY
  632. ending_equity = equity
  633. peak_equity = equity
  634. max_drawdown = 0.0
  635. wins = 0
  636. trades: list[dict[str, object]] = []
  637. entries: list[dict[str, object]] = []
  638. exits: list[dict[str, object]] = []
  639. equity_curve: list[dict[str, float | int]] = []
  640. position: dict[str, object] | None = None
  641. pending_entry_slices = 0
  642. pending_exit = False
  643. for index in range(warmup_bars, len(candles)):
  644. candle = candles[index]
  645. if pending_exit and position is not None:
  646. equity, won = _close_partial_trade(
  647. trades=trades,
  648. exits=exits,
  649. position=position,
  650. account_equity=equity,
  651. candle=candle,
  652. exit_price=candle.open,
  653. leverage=leverage,
  654. )
  655. wins += 1 if won else 0
  656. position = None
  657. pending_exit = False
  658. pending_entry_slices = 0
  659. if pending_entry_slices and equity > 0.0:
  660. slice_margin = equity / entry_slices
  661. if position is None:
  662. position = {
  663. "side": "long",
  664. "entry_time": candle.ts,
  665. "entry_price": candle.open,
  666. "entry_index": index,
  667. "margin_used": slice_margin,
  668. "stop_price": candle.open * (1 - stop_loss_pct),
  669. }
  670. else:
  671. old_margin = float(position["margin_used"])
  672. new_margin = old_margin + slice_margin
  673. entry_price = (float(position["entry_price"]) * old_margin + candle.open * slice_margin) / new_margin
  674. position["entry_price"] = entry_price
  675. position["margin_used"] = new_margin
  676. position["stop_price"] = entry_price * (1 - stop_loss_pct)
  677. entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
  678. pending_entry_slices -= 1
  679. current_equity = equity
  680. if position is not None and candle.low <= float(position["stop_price"]):
  681. equity, won = _close_partial_trade(
  682. trades=trades,
  683. exits=exits,
  684. position=position,
  685. account_equity=equity,
  686. candle=candle,
  687. exit_price=float(position["stop_price"]),
  688. leverage=leverage,
  689. )
  690. wins += 1 if won else 0
  691. current_equity = equity
  692. position = None
  693. pending_entry_slices = 0
  694. if position is not None:
  695. position_equity = mark_to_market(
  696. side="long",
  697. margin_used=float(position["margin_used"]),
  698. entry_price=float(position["entry_price"]),
  699. mark_price=candle.close,
  700. leverage=leverage,
  701. )
  702. current_equity = equity - float(position["margin_used"]) + position_equity
  703. peak_equity = max(peak_equity, current_equity)
  704. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  705. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  706. ending_equity = current_equity
  707. if index == len(candles) - 1 or equity <= 0.0:
  708. continue
  709. current_rsi = rsi_values[index]
  710. current_trend = trend[index]
  711. if current_rsi != current_rsi or current_trend != current_trend:
  712. continue
  713. if position is not None:
  714. held_bars = index - int(position["entry_index"])
  715. if current_rsi >= exit_rsi or held_bars >= max_hold_bars:
  716. pending_exit = True
  717. pending_entry_slices = 0
  718. continue
  719. if pending_entry_slices == 0 and candle.close > float(current_trend) and current_rsi <= rsi_threshold:
  720. pending_entry_slices = entry_slices
  721. trade_count = len(trades)
  722. return SegmentResult(
  723. trade_count=trade_count,
  724. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  725. win_rate=(wins / trade_count) if trade_count else 0.0,
  726. max_drawdown=max_drawdown,
  727. trades=trades,
  728. open_position=position,
  729. candles=candles[warmup_bars:],
  730. equity_curve=equity_curve,
  731. entries=entries,
  732. exits=exits,
  733. )
  734. def run_rsi2_long_guarded_price_twap_segment(
  735. *,
  736. candles: list[Candle],
  737. leverage: int,
  738. warmup_bars: int,
  739. trend_sma: int,
  740. rsi_threshold: float,
  741. exit_rsi: float,
  742. stop_loss_pct: float,
  743. max_hold_bars: int,
  744. entry_offsets: tuple[float, ...],
  745. entry_valid_bars: int,
  746. fill_buffer: float,
  747. ) -> SegmentResult:
  748. closes = pd.Series([candle.close for candle in candles], dtype=float)
  749. trend = closes.rolling(trend_sma).mean().tolist()
  750. rsi_values = _compute_rsi(closes, 2)
  751. equity = INITIAL_EQUITY
  752. ending_equity = equity
  753. peak_equity = equity
  754. max_drawdown = 0.0
  755. wins = 0
  756. trades: list[dict[str, object]] = []
  757. entries: list[dict[str, object]] = []
  758. exits: list[dict[str, object]] = []
  759. equity_curve: list[dict[str, float | int]] = []
  760. position: dict[str, object] | None = None
  761. pending_limits: list[dict[str, float | int]] = []
  762. pending_exit = False
  763. for index in range(warmup_bars, len(candles)):
  764. candle = candles[index]
  765. if pending_exit and position is not None:
  766. equity, won = _close_partial_trade(
  767. trades=trades,
  768. exits=exits,
  769. position=position,
  770. account_equity=equity,
  771. candle=candle,
  772. exit_price=candle.open,
  773. leverage=leverage,
  774. )
  775. wins += 1 if won else 0
  776. position = None
  777. pending_exit = False
  778. pending_limits = []
  779. active_limits: list[dict[str, float | int]] = []
  780. for limit in pending_limits:
  781. if index > int(limit["expires_index"]):
  782. continue
  783. limit_price = float(limit["price"])
  784. if candle.low <= limit_price * (1.0 - fill_buffer) and equity > 0.0:
  785. slice_margin = equity / len(entry_offsets)
  786. if position is None:
  787. position = {
  788. "side": "long",
  789. "entry_time": candle.ts,
  790. "entry_price": limit_price,
  791. "entry_index": index,
  792. "margin_used": slice_margin,
  793. "stop_price": limit_price * (1 - stop_loss_pct),
  794. }
  795. else:
  796. old_margin = float(position["margin_used"])
  797. new_margin = old_margin + slice_margin
  798. entry_price = (float(position["entry_price"]) * old_margin + limit_price * slice_margin) / new_margin
  799. position["entry_price"] = entry_price
  800. position["margin_used"] = new_margin
  801. position["stop_price"] = entry_price * (1 - stop_loss_pct)
  802. entries.append({"ts": candle.ts, "price": limit_price, "side": "long"})
  803. else:
  804. active_limits.append(limit)
  805. pending_limits = active_limits
  806. current_equity = equity
  807. if position is not None and candle.low <= float(position["stop_price"]):
  808. equity, won = _close_partial_trade(
  809. trades=trades,
  810. exits=exits,
  811. position=position,
  812. account_equity=equity,
  813. candle=candle,
  814. exit_price=float(position["stop_price"]),
  815. leverage=leverage,
  816. )
  817. wins += 1 if won else 0
  818. current_equity = equity
  819. position = None
  820. pending_limits = []
  821. if position is not None:
  822. position_equity = mark_to_market(
  823. side="long",
  824. margin_used=float(position["margin_used"]),
  825. entry_price=float(position["entry_price"]),
  826. mark_price=candle.close,
  827. leverage=leverage,
  828. )
  829. current_equity = equity - float(position["margin_used"]) + position_equity
  830. peak_equity = max(peak_equity, current_equity)
  831. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  832. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  833. ending_equity = current_equity
  834. if index == len(candles) - 1 or equity <= 0.0:
  835. continue
  836. current_rsi = rsi_values[index]
  837. current_trend = trend[index]
  838. if current_rsi != current_rsi or current_trend != current_trend:
  839. continue
  840. if position is not None:
  841. held_bars = index - int(position["entry_index"])
  842. if current_rsi >= exit_rsi or held_bars >= max_hold_bars:
  843. pending_exit = True
  844. pending_limits = []
  845. continue
  846. if not pending_limits and candle.close > float(current_trend) and current_rsi <= rsi_threshold:
  847. pending_limits = [
  848. {
  849. "price": candle.close * (1.0 - offset),
  850. "expires_index": index + entry_valid_bars,
  851. }
  852. for offset in entry_offsets
  853. ]
  854. trade_count = len(trades)
  855. return SegmentResult(
  856. trade_count=trade_count,
  857. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  858. win_rate=(wins / trade_count) if trade_count else 0.0,
  859. max_drawdown=max_drawdown,
  860. trades=trades,
  861. open_position=position,
  862. candles=candles[warmup_bars:],
  863. equity_curve=equity_curve,
  864. entries=entries,
  865. exits=exits,
  866. )
  867. def run_ma_cross_segment(
  868. *,
  869. candles: list[Candle],
  870. leverage: int,
  871. warmup_bars: int,
  872. fast: int,
  873. slow: int,
  874. side_mode: str,
  875. ) -> SegmentResult:
  876. closes = pd.Series([candle.close for candle in candles], dtype=float)
  877. fast_ma = closes.rolling(fast).mean().tolist()
  878. slow_ma = closes.rolling(slow).mean().tolist()
  879. equity = INITIAL_EQUITY
  880. ending_equity = equity
  881. peak_equity = equity
  882. max_drawdown = 0.0
  883. wins = 0
  884. trades: list[dict[str, object]] = []
  885. entries: list[dict[str, object]] = []
  886. exits: list[dict[str, object]] = []
  887. equity_curve: list[dict[str, float | int]] = []
  888. position: dict[str, object] | None = None
  889. pending_entry_side: str | None = None
  890. pending_exit = False
  891. for index in range(warmup_bars, len(candles)):
  892. candle = candles[index]
  893. if pending_exit and position is not None:
  894. equity, won = _trade(
  895. trades=trades,
  896. exits=exits,
  897. position=position,
  898. candle=candle,
  899. exit_price=candle.open,
  900. leverage=leverage,
  901. )
  902. wins += 1 if won else 0
  903. position = None
  904. pending_exit = False
  905. if pending_entry_side is not None and position is None and equity > 0.0:
  906. position = {
  907. "side": pending_entry_side,
  908. "entry_time": candle.ts,
  909. "entry_price": candle.open,
  910. "margin_used": equity,
  911. }
  912. entries.append({"ts": candle.ts, "price": candle.open, "side": pending_entry_side})
  913. pending_entry_side = None
  914. current_equity = equity
  915. if position is not None:
  916. current_equity = mark_to_market(
  917. side=str(position["side"]),
  918. margin_used=float(position["margin_used"]),
  919. entry_price=float(position["entry_price"]),
  920. mark_price=candle.close,
  921. leverage=leverage,
  922. )
  923. peak_equity = max(peak_equity, current_equity)
  924. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  925. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  926. ending_equity = current_equity
  927. if index == len(candles) - 1 or equity <= 0.0:
  928. continue
  929. current_fast = fast_ma[index]
  930. current_slow = slow_ma[index]
  931. previous_fast = fast_ma[index - 1]
  932. previous_slow = slow_ma[index - 1]
  933. if current_fast != current_fast or current_slow != current_slow or previous_fast != previous_fast or previous_slow != previous_slow:
  934. continue
  935. crossed_up = previous_fast <= previous_slow and current_fast > current_slow
  936. crossed_down = previous_fast >= previous_slow and current_fast < current_slow
  937. if position is not None:
  938. if (position["side"] == "long" and crossed_down) or (position["side"] == "short" and crossed_up):
  939. pending_exit = True
  940. continue
  941. if crossed_up and side_mode in {"both", "long"}:
  942. pending_entry_side = "long"
  943. elif crossed_down and side_mode in {"both", "short"}:
  944. pending_entry_side = "short"
  945. trade_count = len(trades)
  946. return SegmentResult(
  947. trade_count=trade_count,
  948. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  949. win_rate=wins / trade_count if trade_count else 0.0,
  950. max_drawdown=max_drawdown,
  951. trades=trades,
  952. open_position=position,
  953. candles=candles[warmup_bars:],
  954. equity_curve=equity_curve,
  955. entries=entries,
  956. exits=exits,
  957. )
  958. def run_trend_rsi_bb_long_segment(
  959. *,
  960. candles: list[Candle],
  961. leverage: int,
  962. warmup_bars: int,
  963. trend_sma: int,
  964. band_length: int,
  965. std_multiplier: float,
  966. rsi_threshold: float,
  967. exit_rsi: float,
  968. stop_loss_pct: float,
  969. ) -> SegmentResult:
  970. closes = pd.Series([candle.close for candle in candles], dtype=float)
  971. trend = closes.rolling(trend_sma).mean().tolist()
  972. middle = closes.rolling(band_length).mean().tolist()
  973. stdev = closes.rolling(band_length).std(ddof=0).tolist()
  974. lower = [
  975. float("nan") if middle_value != middle_value or std_value != std_value else middle_value - std_multiplier * std_value
  976. for middle_value, std_value in zip(middle, stdev)
  977. ]
  978. rsi_values = _compute_rsi(closes, 2)
  979. equity = INITIAL_EQUITY
  980. ending_equity = equity
  981. peak_equity = equity
  982. max_drawdown = 0.0
  983. wins = 0
  984. trades: list[dict[str, object]] = []
  985. entries: list[dict[str, object]] = []
  986. exits: list[dict[str, object]] = []
  987. equity_curve: list[dict[str, float | int]] = []
  988. position: dict[str, object] | None = None
  989. pending_entry = False
  990. pending_exit = False
  991. for index in range(warmup_bars, len(candles)):
  992. candle = candles[index]
  993. if pending_exit and position is not None:
  994. equity, won = _trade(
  995. trades=trades,
  996. exits=exits,
  997. position=position,
  998. candle=candle,
  999. exit_price=candle.open,
  1000. leverage=leverage,
  1001. )
  1002. wins += 1 if won else 0
  1003. position = None
  1004. pending_exit = False
  1005. if pending_entry and position is None and equity > 0.0:
  1006. position = {
  1007. "side": "long",
  1008. "entry_time": candle.ts,
  1009. "entry_price": candle.open,
  1010. "entry_index": index,
  1011. "margin_used": equity,
  1012. "stop_price": candle.open * (1 - stop_loss_pct),
  1013. }
  1014. entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
  1015. pending_entry = False
  1016. current_equity = equity
  1017. if position is not None and candle.low <= float(position["stop_price"]):
  1018. equity, won = _trade(
  1019. trades=trades,
  1020. exits=exits,
  1021. position=position,
  1022. candle=candle,
  1023. exit_price=float(position["stop_price"]),
  1024. leverage=leverage,
  1025. )
  1026. wins += 1 if won else 0
  1027. current_equity = equity
  1028. position = None
  1029. if position is not None:
  1030. current_equity = mark_to_market(
  1031. side="long",
  1032. margin_used=float(position["margin_used"]),
  1033. entry_price=float(position["entry_price"]),
  1034. mark_price=candle.close,
  1035. leverage=leverage,
  1036. )
  1037. peak_equity = max(peak_equity, current_equity)
  1038. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  1039. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  1040. ending_equity = current_equity
  1041. if index == len(candles) - 1 or equity <= 0.0:
  1042. continue
  1043. current_rsi = rsi_values[index]
  1044. current_trend = trend[index]
  1045. current_middle = middle[index]
  1046. current_lower = lower[index]
  1047. if current_rsi != current_rsi or current_trend != current_trend or current_middle != current_middle or current_lower != current_lower:
  1048. continue
  1049. if position is not None:
  1050. if current_rsi >= exit_rsi or candle.close >= float(current_middle):
  1051. pending_exit = True
  1052. continue
  1053. if candle.close > float(current_trend) and candle.close <= float(current_lower) and current_rsi <= rsi_threshold:
  1054. pending_entry = True
  1055. trade_count = len(trades)
  1056. return SegmentResult(
  1057. trade_count=trade_count,
  1058. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  1059. win_rate=wins / trade_count if trade_count else 0.0,
  1060. max_drawdown=max_drawdown,
  1061. trades=trades,
  1062. open_position=position,
  1063. candles=candles[warmup_bars:],
  1064. equity_curve=equity_curve,
  1065. entries=entries,
  1066. exits=exits,
  1067. )
  1068. def run_regime_hybrid_segment(
  1069. *,
  1070. candles: list[Candle],
  1071. leverage: int,
  1072. warmup_bars: int,
  1073. trend_sma: int,
  1074. regime_lookback: int,
  1075. neutral_ma_distance: float,
  1076. rsi_long_threshold: float,
  1077. rsi_exit: float,
  1078. bb_length: int,
  1079. bb_std: float,
  1080. bb_bandwidth_lookback: int,
  1081. stop_loss_pct: float,
  1082. ) -> SegmentResult:
  1083. closes = pd.Series([candle.close for candle in candles], dtype=float)
  1084. trend = closes.rolling(trend_sma).mean().tolist()
  1085. rsi_values = _compute_rsi(closes, 2)
  1086. middle = closes.rolling(bb_length).mean().tolist()
  1087. stdev = closes.rolling(bb_length).std(ddof=0).tolist()
  1088. upper = [
  1089. float("nan") if middle_value != middle_value or std_value != std_value else middle_value + bb_std * std_value
  1090. for middle_value, std_value in zip(middle, stdev)
  1091. ]
  1092. lower = [
  1093. float("nan") if middle_value != middle_value or std_value != std_value else middle_value - bb_std * std_value
  1094. for middle_value, std_value in zip(middle, stdev)
  1095. ]
  1096. bandwidth = [
  1097. float("nan") if upper_value != upper_value or lower_value != lower_value or middle_value != middle_value or middle_value == 0.0 else (upper_value - lower_value) / middle_value
  1098. for upper_value, lower_value, middle_value in zip(upper, lower, middle)
  1099. ]
  1100. equity = INITIAL_EQUITY
  1101. ending_equity = equity
  1102. peak_equity = equity
  1103. max_drawdown = 0.0
  1104. wins = 0
  1105. trades: list[dict[str, object]] = []
  1106. entries: list[dict[str, object]] = []
  1107. exits: list[dict[str, object]] = []
  1108. equity_curve: list[dict[str, float | int]] = []
  1109. position: dict[str, object] | None = None
  1110. pending_entry: dict[str, object] | None = None
  1111. pending_exit = False
  1112. for index in range(warmup_bars, len(candles)):
  1113. candle = candles[index]
  1114. if pending_exit and position is not None:
  1115. equity, won = _trade(
  1116. trades=trades,
  1117. exits=exits,
  1118. position=position,
  1119. candle=candle,
  1120. exit_price=candle.open,
  1121. leverage=leverage,
  1122. )
  1123. wins += 1 if won else 0
  1124. position = None
  1125. pending_exit = False
  1126. if pending_entry is not None and position is None and equity > 0.0:
  1127. side = str(pending_entry["side"])
  1128. position = {
  1129. "side": side,
  1130. "entry_time": candle.ts,
  1131. "entry_price": candle.open,
  1132. "entry_index": index,
  1133. "margin_used": equity,
  1134. "stop_price": candle.open * (1 - stop_loss_pct if side == "long" else 1 + stop_loss_pct),
  1135. "mode": str(pending_entry["mode"]),
  1136. }
  1137. entries.append({"ts": candle.ts, "price": candle.open, "side": side})
  1138. pending_entry = None
  1139. current_equity = equity
  1140. if position is not None:
  1141. stop_hit = (
  1142. position["side"] == "long" and candle.low <= float(position["stop_price"])
  1143. ) or (
  1144. position["side"] == "short" and candle.high >= float(position["stop_price"])
  1145. )
  1146. if stop_hit:
  1147. equity, won = _trade(
  1148. trades=trades,
  1149. exits=exits,
  1150. position=position,
  1151. candle=candle,
  1152. exit_price=float(position["stop_price"]),
  1153. leverage=leverage,
  1154. )
  1155. wins += 1 if won else 0
  1156. current_equity = equity
  1157. position = None
  1158. if position is not None:
  1159. current_equity = mark_to_market(
  1160. side=str(position["side"]),
  1161. margin_used=float(position["margin_used"]),
  1162. entry_price=float(position["entry_price"]),
  1163. mark_price=candle.close,
  1164. leverage=leverage,
  1165. )
  1166. peak_equity = max(peak_equity, current_equity)
  1167. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  1168. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  1169. ending_equity = current_equity
  1170. if index == len(candles) - 1 or equity <= 0.0:
  1171. continue
  1172. current_trend = trend[index]
  1173. current_rsi = rsi_values[index]
  1174. current_middle = middle[index]
  1175. current_upper = upper[index]
  1176. current_lower = lower[index]
  1177. if (
  1178. current_trend != current_trend
  1179. or current_rsi != current_rsi
  1180. or current_middle != current_middle
  1181. or current_upper != current_upper
  1182. or current_lower != current_lower
  1183. ):
  1184. continue
  1185. if position is not None:
  1186. if position["mode"] == "rsi" and (current_rsi >= rsi_exit or candle.close < float(current_trend)):
  1187. pending_exit = True
  1188. elif position["mode"] == "bbmr" and (
  1189. (position["side"] == "long" and candle.close >= float(current_middle))
  1190. or (position["side"] == "short" and candle.close <= float(current_middle))
  1191. ):
  1192. pending_exit = True
  1193. continue
  1194. regime_return = candle.close / candles[index - regime_lookback].close - 1.0
  1195. ma_distance = candle.close / float(current_trend) - 1.0
  1196. if candle.close > float(current_trend) and regime_return > 0.0 and current_rsi <= rsi_long_threshold:
  1197. pending_entry = {"side": "long", "mode": "rsi"}
  1198. continue
  1199. previous_bandwidths = [value for value in bandwidth[max(0, index - bb_bandwidth_lookback) : index] if value == value]
  1200. if abs(ma_distance) > neutral_ma_distance or len(previous_bandwidths) < bb_bandwidth_lookback:
  1201. continue
  1202. if bandwidth[index] == bandwidth[index] and bandwidth[index] <= pd.Series(previous_bandwidths, dtype=float).median():
  1203. if candle.close < float(current_lower):
  1204. pending_entry = {"side": "long", "mode": "bbmr"}
  1205. elif candle.close > float(current_upper):
  1206. pending_entry = {"side": "short", "mode": "bbmr"}
  1207. trade_count = len(trades)
  1208. return SegmentResult(
  1209. trade_count=trade_count,
  1210. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  1211. win_rate=wins / trade_count if trade_count else 0.0,
  1212. max_drawdown=max_drawdown,
  1213. trades=trades,
  1214. open_position=position,
  1215. candles=candles[warmup_bars:],
  1216. equity_curve=equity_curve,
  1217. entries=entries,
  1218. exits=exits,
  1219. )
  1220. def run_eth_btc_rsi_filter_segment(
  1221. *,
  1222. eth_candles: list[Candle],
  1223. btc_candles: list[Candle],
  1224. leverage: int,
  1225. warmup_bars: int,
  1226. eth_trend_sma: int,
  1227. eth_rsi_threshold: float,
  1228. eth_exit_rsi: float,
  1229. btc_trend_sma: int,
  1230. btc_momentum_lookback: int,
  1231. btc_min_momentum: float,
  1232. ) -> SegmentResult:
  1233. eth_closes = pd.Series([candle.close for candle in eth_candles], dtype=float)
  1234. btc_closes = pd.Series([candle.close for candle in btc_candles], dtype=float)
  1235. eth_trend = eth_closes.rolling(eth_trend_sma).mean().tolist()
  1236. eth_rsi = _compute_rsi(eth_closes, 2)
  1237. btc_trend = btc_closes.rolling(btc_trend_sma).mean().tolist()
  1238. equity = INITIAL_EQUITY
  1239. ending_equity = equity
  1240. peak_equity = equity
  1241. max_drawdown = 0.0
  1242. wins = 0
  1243. trades: list[dict[str, object]] = []
  1244. entries: list[dict[str, object]] = []
  1245. exits: list[dict[str, object]] = []
  1246. equity_curve: list[dict[str, float | int]] = []
  1247. position: dict[str, object] | None = None
  1248. pending_entry = False
  1249. pending_exit = False
  1250. for index in range(warmup_bars, len(eth_candles)):
  1251. candle = eth_candles[index]
  1252. if pending_exit and position is not None:
  1253. equity, won = _trade(
  1254. trades=trades,
  1255. exits=exits,
  1256. position=position,
  1257. candle=candle,
  1258. exit_price=candle.open,
  1259. leverage=leverage,
  1260. )
  1261. wins += 1 if won else 0
  1262. position = None
  1263. pending_exit = False
  1264. if pending_entry and position is None and equity > 0.0:
  1265. position = {
  1266. "side": "long",
  1267. "entry_time": candle.ts,
  1268. "entry_price": candle.open,
  1269. "margin_used": equity,
  1270. }
  1271. entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
  1272. pending_entry = False
  1273. current_equity = equity
  1274. if position is not None:
  1275. current_equity = mark_to_market(
  1276. side="long",
  1277. margin_used=float(position["margin_used"]),
  1278. entry_price=float(position["entry_price"]),
  1279. mark_price=candle.close,
  1280. leverage=leverage,
  1281. )
  1282. peak_equity = max(peak_equity, current_equity)
  1283. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  1284. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  1285. ending_equity = current_equity
  1286. if index == len(eth_candles) - 1 or equity <= 0.0:
  1287. continue
  1288. current_eth_trend = eth_trend[index]
  1289. current_eth_rsi = eth_rsi[index]
  1290. current_btc_trend = btc_trend[index]
  1291. if current_eth_trend != current_eth_trend or current_eth_rsi != current_eth_rsi or current_btc_trend != current_btc_trend:
  1292. continue
  1293. if position is not None:
  1294. if current_eth_rsi >= eth_exit_rsi or btc_candles[index].close < float(current_btc_trend):
  1295. pending_exit = True
  1296. continue
  1297. btc_momentum = btc_candles[index].close / btc_candles[index - btc_momentum_lookback].close - 1.0
  1298. btc_risk_on = btc_candles[index].close > float(current_btc_trend) and btc_momentum >= btc_min_momentum
  1299. eth_pullback = candle.close > float(current_eth_trend) and current_eth_rsi <= eth_rsi_threshold
  1300. if btc_risk_on and eth_pullback:
  1301. pending_entry = True
  1302. trade_count = len(trades)
  1303. return SegmentResult(
  1304. trade_count=trade_count,
  1305. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  1306. win_rate=wins / trade_count if trade_count else 0.0,
  1307. max_drawdown=max_drawdown,
  1308. trades=trades,
  1309. open_position=position,
  1310. candles=eth_candles[warmup_bars:],
  1311. equity_curve=equity_curve,
  1312. entries=entries,
  1313. exits=exits,
  1314. )
  1315. def run_eth_btc_shock_filter_segment(
  1316. *,
  1317. eth_candles: list[Candle],
  1318. btc_candles: list[Candle],
  1319. leverage: int,
  1320. warmup_bars: int,
  1321. eth_trend_sma: int,
  1322. eth_rsi_threshold: float,
  1323. eth_exit_rsi: float,
  1324. btc_trend_sma: int,
  1325. btc_momentum_lookback: int,
  1326. btc_min_momentum: float,
  1327. btc_shock_lookback: int,
  1328. btc_max_realized_vol: float,
  1329. btc_max_drawdown: float,
  1330. ) -> SegmentResult:
  1331. eth_closes = pd.Series([candle.close for candle in eth_candles], dtype=float)
  1332. btc_closes = pd.Series([candle.close for candle in btc_candles], dtype=float)
  1333. eth_trend = eth_closes.rolling(eth_trend_sma).mean().tolist()
  1334. eth_rsi = _compute_rsi(eth_closes, 2)
  1335. btc_trend = btc_closes.rolling(btc_trend_sma).mean().tolist()
  1336. btc_realized_vol = btc_closes.pct_change().rolling(btc_shock_lookback).std(ddof=1).tolist()
  1337. btc_recent_high = btc_closes.rolling(btc_shock_lookback).max().tolist()
  1338. equity = INITIAL_EQUITY
  1339. ending_equity = equity
  1340. peak_equity = equity
  1341. max_drawdown = 0.0
  1342. wins = 0
  1343. trades: list[dict[str, object]] = []
  1344. entries: list[dict[str, object]] = []
  1345. exits: list[dict[str, object]] = []
  1346. equity_curve: list[dict[str, float | int]] = []
  1347. position: dict[str, object] | None = None
  1348. pending_entry = False
  1349. pending_exit = False
  1350. for index in range(warmup_bars, len(eth_candles)):
  1351. candle = eth_candles[index]
  1352. if pending_exit and position is not None:
  1353. equity, won = _trade(
  1354. trades=trades,
  1355. exits=exits,
  1356. position=position,
  1357. candle=candle,
  1358. exit_price=candle.open,
  1359. leverage=leverage,
  1360. )
  1361. wins += 1 if won else 0
  1362. position = None
  1363. pending_exit = False
  1364. if pending_entry and position is None and equity > 0.0:
  1365. position = {
  1366. "side": "long",
  1367. "entry_time": candle.ts,
  1368. "entry_price": candle.open,
  1369. "margin_used": equity,
  1370. }
  1371. entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
  1372. pending_entry = False
  1373. current_equity = equity
  1374. if position is not None:
  1375. current_equity = mark_to_market(
  1376. side="long",
  1377. margin_used=float(position["margin_used"]),
  1378. entry_price=float(position["entry_price"]),
  1379. mark_price=candle.close,
  1380. leverage=leverage,
  1381. )
  1382. peak_equity = max(peak_equity, current_equity)
  1383. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  1384. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  1385. ending_equity = current_equity
  1386. if index == len(eth_candles) - 1 or equity <= 0.0:
  1387. continue
  1388. current_eth_trend = eth_trend[index]
  1389. current_eth_rsi = eth_rsi[index]
  1390. current_btc_trend = btc_trend[index]
  1391. current_btc_vol = btc_realized_vol[index]
  1392. current_btc_high = btc_recent_high[index]
  1393. if (
  1394. current_eth_trend != current_eth_trend
  1395. or current_eth_rsi != current_eth_rsi
  1396. or current_btc_trend != current_btc_trend
  1397. or current_btc_vol != current_btc_vol
  1398. or current_btc_high != current_btc_high
  1399. ):
  1400. continue
  1401. btc_drawdown = btc_candles[index].close / float(current_btc_high) - 1.0
  1402. btc_shock_ok = current_btc_vol <= btc_max_realized_vol and btc_drawdown >= -btc_max_drawdown
  1403. if position is not None:
  1404. if current_eth_rsi >= eth_exit_rsi or btc_candles[index].close < float(current_btc_trend) or not btc_shock_ok:
  1405. pending_exit = True
  1406. continue
  1407. btc_momentum = btc_candles[index].close / btc_candles[index - btc_momentum_lookback].close - 1.0
  1408. btc_risk_on = (
  1409. btc_candles[index].close > float(current_btc_trend)
  1410. and btc_momentum >= btc_min_momentum
  1411. and btc_shock_ok
  1412. )
  1413. eth_pullback = candle.close > float(current_eth_trend) and current_eth_rsi <= eth_rsi_threshold
  1414. if btc_risk_on and eth_pullback:
  1415. pending_entry = True
  1416. trade_count = len(trades)
  1417. return SegmentResult(
  1418. trade_count=trade_count,
  1419. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  1420. win_rate=wins / trade_count if trade_count else 0.0,
  1421. max_drawdown=max_drawdown,
  1422. trades=trades,
  1423. open_position=position,
  1424. candles=eth_candles[warmup_bars:],
  1425. equity_curve=equity_curve,
  1426. entries=entries,
  1427. exits=exits,
  1428. )
  1429. def run_eth_btc_ratio_pullback_segment(
  1430. *,
  1431. eth_candles: list[Candle],
  1432. btc_candles: list[Candle],
  1433. leverage: int,
  1434. warmup_bars: int,
  1435. btc_trend_sma: int,
  1436. btc_momentum_lookback: int,
  1437. btc_min_momentum: float,
  1438. ratio_length: int,
  1439. ratio_std: float,
  1440. ratio_rsi_threshold: float,
  1441. stop_loss_pct: float,
  1442. ) -> SegmentResult:
  1443. eth_closes = pd.Series([candle.close for candle in eth_candles], dtype=float)
  1444. btc_closes = pd.Series([candle.close for candle in btc_candles], dtype=float)
  1445. ratio = eth_closes / btc_closes
  1446. btc_trend = btc_closes.rolling(btc_trend_sma).mean().tolist()
  1447. ratio_middle = ratio.rolling(ratio_length).mean().tolist()
  1448. ratio_stdev = ratio.rolling(ratio_length).std(ddof=0).tolist()
  1449. ratio_lower = [
  1450. float("nan") if middle != middle or stdev != stdev else middle - ratio_std * stdev
  1451. for middle, stdev in zip(ratio_middle, ratio_stdev)
  1452. ]
  1453. ratio_rsi = _compute_rsi(ratio, 2)
  1454. equity = INITIAL_EQUITY
  1455. ending_equity = equity
  1456. peak_equity = equity
  1457. max_drawdown = 0.0
  1458. wins = 0
  1459. trades: list[dict[str, object]] = []
  1460. entries: list[dict[str, object]] = []
  1461. exits: list[dict[str, object]] = []
  1462. equity_curve: list[dict[str, float | int]] = []
  1463. position: dict[str, object] | None = None
  1464. pending_entry = False
  1465. pending_exit = False
  1466. for index in range(warmup_bars, len(eth_candles)):
  1467. candle = eth_candles[index]
  1468. if pending_exit and position is not None:
  1469. equity, won = _trade(
  1470. trades=trades,
  1471. exits=exits,
  1472. position=position,
  1473. candle=candle,
  1474. exit_price=candle.open,
  1475. leverage=leverage,
  1476. )
  1477. wins += 1 if won else 0
  1478. position = None
  1479. pending_exit = False
  1480. if pending_entry and position is None and equity > 0.0:
  1481. position = {
  1482. "side": "long",
  1483. "entry_time": candle.ts,
  1484. "entry_price": candle.open,
  1485. "entry_index": index,
  1486. "margin_used": equity,
  1487. "stop_price": candle.open * (1 - stop_loss_pct),
  1488. }
  1489. entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
  1490. pending_entry = False
  1491. current_equity = equity
  1492. if position is not None and candle.low <= float(position["stop_price"]):
  1493. equity, won = _trade(
  1494. trades=trades,
  1495. exits=exits,
  1496. position=position,
  1497. candle=candle,
  1498. exit_price=float(position["stop_price"]),
  1499. leverage=leverage,
  1500. )
  1501. wins += 1 if won else 0
  1502. current_equity = equity
  1503. position = None
  1504. if position is not None:
  1505. current_equity = mark_to_market(
  1506. side="long",
  1507. margin_used=float(position["margin_used"]),
  1508. entry_price=float(position["entry_price"]),
  1509. mark_price=candle.close,
  1510. leverage=leverage,
  1511. )
  1512. peak_equity = max(peak_equity, current_equity)
  1513. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  1514. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  1515. ending_equity = current_equity
  1516. if index == len(eth_candles) - 1 or equity <= 0.0:
  1517. continue
  1518. current_btc_trend = btc_trend[index]
  1519. current_ratio_middle = ratio_middle[index]
  1520. current_ratio_lower = ratio_lower[index]
  1521. current_ratio_rsi = ratio_rsi[index]
  1522. if (
  1523. current_btc_trend != current_btc_trend
  1524. or current_ratio_middle != current_ratio_middle
  1525. or current_ratio_lower != current_ratio_lower
  1526. or current_ratio_rsi != current_ratio_rsi
  1527. ):
  1528. continue
  1529. btc_momentum = btc_candles[index].close / btc_candles[index - btc_momentum_lookback].close - 1.0
  1530. btc_risk_on = btc_candles[index].close > float(current_btc_trend) and btc_momentum >= btc_min_momentum
  1531. if position is not None:
  1532. if not btc_risk_on or ratio.iloc[index] >= float(current_ratio_middle):
  1533. pending_exit = True
  1534. continue
  1535. ratio_pullback = ratio.iloc[index] <= float(current_ratio_lower) or current_ratio_rsi <= ratio_rsi_threshold
  1536. if btc_risk_on and ratio_pullback:
  1537. pending_entry = True
  1538. trade_count = len(trades)
  1539. return SegmentResult(
  1540. trade_count=trade_count,
  1541. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  1542. win_rate=wins / trade_count if trade_count else 0.0,
  1543. max_drawdown=max_drawdown,
  1544. trades=trades,
  1545. open_position=position,
  1546. candles=eth_candles[warmup_bars:],
  1547. equity_curve=equity_curve,
  1548. entries=entries,
  1549. exits=exits,
  1550. )
  1551. def run_btc_lead_eth_lag_segment(
  1552. *,
  1553. eth_candles: list[Candle],
  1554. btc_candles: list[Candle],
  1555. leverage: int,
  1556. warmup_bars: int,
  1557. lead_lookback: int,
  1558. btc_return_threshold: float,
  1559. lag_gap: float,
  1560. max_hold_bars: int,
  1561. stop_loss_pct: float,
  1562. take_profit_pct: float,
  1563. ) -> SegmentResult:
  1564. equity = INITIAL_EQUITY
  1565. ending_equity = equity
  1566. peak_equity = equity
  1567. max_drawdown = 0.0
  1568. wins = 0
  1569. trades: list[dict[str, object]] = []
  1570. entries: list[dict[str, object]] = []
  1571. exits: list[dict[str, object]] = []
  1572. equity_curve: list[dict[str, float | int]] = []
  1573. position: dict[str, object] | None = None
  1574. pending_entry = False
  1575. pending_exit = False
  1576. for index in range(warmup_bars, len(eth_candles)):
  1577. candle = eth_candles[index]
  1578. if pending_exit and position is not None:
  1579. equity, won = _trade(
  1580. trades=trades,
  1581. exits=exits,
  1582. position=position,
  1583. candle=candle,
  1584. exit_price=candle.open,
  1585. leverage=leverage,
  1586. )
  1587. wins += 1 if won else 0
  1588. position = None
  1589. pending_exit = False
  1590. if pending_entry and position is None and equity > 0.0:
  1591. position = {
  1592. "side": "long",
  1593. "entry_time": candle.ts,
  1594. "entry_price": candle.open,
  1595. "entry_index": index,
  1596. "margin_used": equity,
  1597. "stop_price": candle.open * (1.0 - stop_loss_pct),
  1598. "take_profit_price": candle.open * (1.0 + take_profit_pct),
  1599. }
  1600. entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
  1601. pending_entry = False
  1602. current_equity = equity
  1603. if position is not None:
  1604. if candle.low <= float(position["stop_price"]):
  1605. equity, won = _trade(
  1606. trades=trades,
  1607. exits=exits,
  1608. position=position,
  1609. candle=candle,
  1610. exit_price=float(position["stop_price"]),
  1611. leverage=leverage,
  1612. )
  1613. wins += 1 if won else 0
  1614. current_equity = equity
  1615. position = None
  1616. elif candle.high >= float(position["take_profit_price"]):
  1617. equity, won = _trade(
  1618. trades=trades,
  1619. exits=exits,
  1620. position=position,
  1621. candle=candle,
  1622. exit_price=float(position["take_profit_price"]),
  1623. leverage=leverage,
  1624. )
  1625. wins += 1 if won else 0
  1626. current_equity = equity
  1627. position = None
  1628. if position is not None:
  1629. current_equity = mark_to_market(
  1630. side="long",
  1631. margin_used=float(position["margin_used"]),
  1632. entry_price=float(position["entry_price"]),
  1633. mark_price=candle.close,
  1634. leverage=leverage,
  1635. )
  1636. peak_equity = max(peak_equity, current_equity)
  1637. max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
  1638. equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
  1639. ending_equity = current_equity
  1640. if index == len(eth_candles) - 1 or equity <= 0.0:
  1641. continue
  1642. if position is not None:
  1643. if index - int(position["entry_index"]) >= max_hold_bars:
  1644. pending_exit = True
  1645. continue
  1646. btc_return = btc_candles[index].close / btc_candles[index - lead_lookback].close - 1.0
  1647. eth_return = candle.close / eth_candles[index - lead_lookback].close - 1.0
  1648. if btc_return >= btc_return_threshold and btc_return - eth_return >= lag_gap:
  1649. pending_entry = True
  1650. trade_count = len(trades)
  1651. return SegmentResult(
  1652. trade_count=trade_count,
  1653. total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
  1654. win_rate=wins / trade_count if trade_count else 0.0,
  1655. max_drawdown=max_drawdown,
  1656. trades=trades,
  1657. open_position=position,
  1658. candles=eth_candles[warmup_bars:],
  1659. equity_curve=equity_curve,
  1660. entries=entries,
  1661. exits=exits,
  1662. )
  1663. def build_candidates() -> list[Candidate]:
  1664. candidates: list[Candidate] = []
  1665. candidates.append(Candidate("bbmr-default", 69, lambda candles, leverage, warmup_bars: run_bbmr_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=BBMRConfig())))
  1666. candidates.append(Candidate("bbsb-default", 69, lambda candles, leverage, warmup_bars: run_bbsb_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=BBSBConfig())))
  1667. for entry_window in (8, 12, 20):
  1668. for exit_window in (4, 6, 10):
  1669. for stop in (0.004, 0.008, 0.012):
  1670. config = DonchianConfig(entry_window=entry_window, exit_window=exit_window, stop_loss_pct=stop)
  1671. candidates.append(Candidate(f"donchian-e{entry_window}-x{exit_window}-s{stop}", max(entry_window, exit_window), lambda candles, leverage, warmup_bars, config=config: run_donchian_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=config)))
  1672. for trend in (30, 50, 80):
  1673. for long_threshold, short_threshold in ((8.0, 92.0), (12.0, 88.0), (18.0, 82.0)):
  1674. config = RSI2Config(trend_sma=trend, rsi_length=2, rsi_long_threshold=long_threshold, rsi_short_threshold=short_threshold, exit_rsi=50.0)
  1675. candidates.append(Candidate(f"rsi2-t{trend}-l{long_threshold}-s{short_threshold}", max(trend, 3), lambda candles, leverage, warmup_bars, config=config: run_rsi2_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=config)))
  1676. for fast, slow in ((8, 21), (13, 34), (20, 50)):
  1677. for stop in (0.003, 0.006, 0.01):
  1678. config = EMAPullbackConfig(fast_ema=fast, slow_ema=slow, stop_buffer_pct=stop)
  1679. candidates.append(Candidate(f"ema-pullback-f{fast}-s{slow}-b{stop}", max(fast, slow), lambda candles, leverage, warmup_bars, config=config: run_ema_pullback_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=config)))
  1680. for lookback in (6, 10, 16):
  1681. for take, stop in ((0.004, 0.003), (0.006, 0.004), (0.01, 0.005)):
  1682. candidates.append(Candidate(f"range-momo-l{lookback}-tp{take}-sl{stop}", lookback, lambda candles, leverage, warmup_bars, lookback=lookback, take=take, stop=stop: run_range_momentum_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, lookback=lookback, take_profit_pct=take, stop_loss_pct=stop)))
  1683. for window in (24, 48, 72):
  1684. for entry_z in (1.5, 2.0, 2.5):
  1685. candidates.append(Candidate(f"vwap-revert-w{window}-z{entry_z}", window * 2, lambda candles, leverage, warmup_bars, window=window, entry_z=entry_z: run_vwap_reversion_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, window=window, entry_z=entry_z, exit_z=0.2, stop_loss_pct=0.006)))
  1686. return candidates
  1687. def evaluate_candidate(candidate: Candidate, candles: list[Candle]) -> dict[str, object]:
  1688. sampled = sample_segments(
  1689. candles=candles,
  1690. segments=SEGMENTS,
  1691. window_size=WINDOW_SIZE,
  1692. warmup_bars=candidate.warmup_bars,
  1693. )
  1694. results = [
  1695. candidate.run(
  1696. candles=candles[segment.context_start : segment.report_end],
  1697. leverage=LEVERAGE,
  1698. warmup_bars=candidate.warmup_bars,
  1699. )
  1700. for segment in sampled
  1701. ]
  1702. returns = [result.total_return for result in results]
  1703. return {
  1704. "name": candidate.name,
  1705. "avg_return": sum(returns) / len(returns),
  1706. "median_return": float(pd.Series(returns).median()),
  1707. "worst_return": min(returns),
  1708. "best_return": max(returns),
  1709. "trades": sum(result.trade_count for result in results),
  1710. "win_rate": sum(result.win_rate for result in results) / len(results),
  1711. "max_drawdown": max(result.max_drawdown for result in results),
  1712. }
  1713. def evaluate_candidate_all_windows(
  1714. *,
  1715. candidate: Candidate,
  1716. candles: list[Candle],
  1717. window_size: int,
  1718. leverage: int,
  1719. ) -> dict[str, object]:
  1720. rows = evaluate_candidate_window_rows(
  1721. candidate=candidate,
  1722. candles=candles,
  1723. window_size=window_size,
  1724. leverage=leverage,
  1725. )
  1726. return summarize_window_rows(rows, candidate.name)
  1727. def evaluate_candidate_window_rows(
  1728. *,
  1729. candidate: Candidate,
  1730. candles: list[Candle],
  1731. window_size: int,
  1732. leverage: int,
  1733. ) -> list[dict[str, object]]:
  1734. block_size = candidate.warmup_bars + window_size
  1735. context_starts = list(range(0, len(candles) - block_size + 1, window_size))
  1736. rows: list[dict[str, object]] = []
  1737. for start in context_starts:
  1738. result = candidate.run(
  1739. candles=candles[start : start + block_size],
  1740. leverage=leverage,
  1741. warmup_bars=candidate.warmup_bars,
  1742. )
  1743. report_start = start + candidate.warmup_bars
  1744. report_end = start + block_size - 1
  1745. rows.append(
  1746. {
  1747. "window_start_ts": candles[report_start].ts,
  1748. "window_end_ts": candles[report_end].ts,
  1749. "total_return": result.total_return,
  1750. "trade_count": result.trade_count,
  1751. "win_rate": result.win_rate,
  1752. "max_drawdown": result.max_drawdown,
  1753. "trades": result.trades,
  1754. }
  1755. )
  1756. return rows
  1757. def evaluate_pair_candidate_window_rows(
  1758. *,
  1759. candidate: PairCandidate,
  1760. eth_candles: list[Candle],
  1761. btc_candles: list[Candle],
  1762. window_size: int,
  1763. leverage: int,
  1764. ) -> list[dict[str, object]]:
  1765. block_size = candidate.warmup_bars + window_size
  1766. context_starts = list(range(0, len(eth_candles) - block_size + 1, window_size))
  1767. rows: list[dict[str, object]] = []
  1768. for start in context_starts:
  1769. result = candidate.run(
  1770. eth_candles=eth_candles[start : start + block_size],
  1771. btc_candles=btc_candles[start : start + block_size],
  1772. leverage=leverage,
  1773. warmup_bars=candidate.warmup_bars,
  1774. )
  1775. report_start = start + candidate.warmup_bars
  1776. report_end = start + block_size - 1
  1777. rows.append(
  1778. {
  1779. "window_start_ts": eth_candles[report_start].ts,
  1780. "window_end_ts": eth_candles[report_end].ts,
  1781. "total_return": result.total_return,
  1782. "trade_count": result.trade_count,
  1783. "win_rate": result.win_rate,
  1784. "max_drawdown": result.max_drawdown,
  1785. "trades": result.trades,
  1786. }
  1787. )
  1788. return rows
  1789. def summarize_window_rows(rows: list[dict[str, object]], name: str = "") -> dict[str, object]:
  1790. returns = [float(row["total_return"]) for row in rows]
  1791. trade_returns = [
  1792. float(trade["return_pct"]) / 100.0
  1793. for row in rows
  1794. for trade in row["trades"]
  1795. ]
  1796. winning_trade_returns = [value for value in trade_returns if value > 0.0]
  1797. losing_trade_returns = [value for value in trade_returns if value < 0.0]
  1798. avg_win_return = sum(winning_trade_returns) / len(winning_trade_returns) if winning_trade_returns else 0.0
  1799. avg_loss_return_abs = abs(sum(losing_trade_returns) / len(losing_trade_returns)) if losing_trade_returns else 0.0
  1800. gross_profit = sum(winning_trade_returns)
  1801. gross_loss_abs = abs(sum(losing_trade_returns))
  1802. series = pd.Series(returns, dtype=float)
  1803. sample_count = len(returns)
  1804. std = float(series.std(ddof=1)) if sample_count > 1 else 0.0
  1805. ci_half_width = 1.96 * std / sqrt(sample_count) if sample_count > 1 else 0.0
  1806. return {
  1807. "name": name,
  1808. "sample_count": sample_count,
  1809. "avg_return": float(series.mean()),
  1810. "ci95_low": float(series.mean() - ci_half_width),
  1811. "ci95_high": float(series.mean() + ci_half_width),
  1812. "median_return": float(series.median()),
  1813. "positive_window_rate": float((series > 0).mean()),
  1814. "worst_return": float(series.min()),
  1815. "p10_return": float(series.quantile(0.10)),
  1816. "p90_return": float(series.quantile(0.90)),
  1817. "best_return": float(series.max()),
  1818. "trades": sum(int(row["trade_count"]) for row in rows),
  1819. "avg_trades_per_window": sum(int(row["trade_count"]) for row in rows) / sample_count,
  1820. "win_rate": sum(float(row["win_rate"]) for row in rows) / sample_count,
  1821. "trade_win_rate": len(winning_trade_returns) / len(trade_returns) if trade_returns else 0.0,
  1822. "avg_trade_return": sum(trade_returns) / len(trade_returns) if trade_returns else 0.0,
  1823. "avg_win_return": avg_win_return,
  1824. "avg_loss_return_abs": avg_loss_return_abs,
  1825. "payoff_ratio": avg_win_return / avg_loss_return_abs if avg_loss_return_abs else 0.0,
  1826. "profit_factor": gross_profit / gross_loss_abs if gross_loss_abs else 0.0,
  1827. "expectancy_per_trade": sum(trade_returns) / len(trade_returns) if trade_returns else 0.0,
  1828. "max_drawdown": max(float(row["max_drawdown"]) for row in rows),
  1829. "return_drawdown_ratio": float(series.mean()) / max(float(row["max_drawdown"]) for row in rows) if max(float(row["max_drawdown"]) for row in rows) else 0.0,
  1830. }
  1831. def sort_robust_results(frame: pd.DataFrame) -> pd.DataFrame:
  1832. return frame.sort_values(["ci95_low", "avg_return"], ascending=False)
  1833. def add_cost_metrics(frame: pd.DataFrame, roundtrip_cost_on_margin: float) -> pd.DataFrame:
  1834. frame = frame.copy()
  1835. cost = frame["avg_trades_per_window"] * roundtrip_cost_on_margin
  1836. frame["roundtrip_cost_on_margin"] = roundtrip_cost_on_margin
  1837. frame["net_avg_return"] = frame["avg_return"] - cost
  1838. frame["net_ci95_low"] = frame["ci95_low"] - cost
  1839. frame["net_ci95_high"] = frame["ci95_high"] - cost
  1840. frame["breakeven_roundtrip_cost_on_margin"] = frame["avg_return"] / frame["avg_trades_per_window"]
  1841. return frame
  1842. def sort_cost_results(frame: pd.DataFrame) -> pd.DataFrame:
  1843. return frame.sort_values(["net_ci95_low", "net_avg_return"], ascending=False)
  1844. def max_drawdown_from_equity(equity_values: list[float]) -> float:
  1845. peak = equity_values[0]
  1846. max_drawdown = 0.0
  1847. for equity in equity_values:
  1848. peak = max(peak, equity)
  1849. if peak > 0.0:
  1850. max_drawdown = max(max_drawdown, (peak - equity) / peak)
  1851. return max_drawdown
  1852. def cost_adjusted_trade_equity_frame(result: SegmentResult, roundtrip_cost_on_margin: float) -> pd.DataFrame:
  1853. rows = [{"ts": pd.to_datetime(result.equity_curve[0]["ts"], unit="ms", utc=True), "equity": INITIAL_EQUITY}]
  1854. equity = INITIAL_EQUITY
  1855. for trade in result.trades:
  1856. equity *= 1.0 + float(trade["return_pct"]) / 100.0 - roundtrip_cost_on_margin * float(trade.get("cost_weight", 1.0))
  1857. rows.append({"ts": pd.to_datetime(str(trade["exit_time"]), utc=True), "equity": equity})
  1858. return pd.DataFrame(rows)
  1859. def annualized_metrics_from_equity(frame: pd.DataFrame, first_ts: int, last_ts: int) -> dict[str, float]:
  1860. years = (last_ts - first_ts) / 86_400_000 / 365
  1861. total_return = float(frame["equity"].iloc[-1] / frame["equity"].iloc[0] - 1.0)
  1862. annualized_return = (1.0 + total_return) ** (1.0 / years) - 1.0 if total_return > -1.0 and years > 0.0 else 0.0
  1863. max_drawdown = max_drawdown_from_equity([float(value) for value in frame["equity"]])
  1864. daily = frame.set_index("ts")["equity"].resample("1D").last().ffill()
  1865. daily_returns = daily.pct_change().dropna()
  1866. daily_std = float(daily_returns.std(ddof=1)) if len(daily_returns) > 1 else 0.0
  1867. sharpe = float(daily_returns.mean()) / daily_std * sqrt(365) if daily_std else 0.0
  1868. return {
  1869. "net_total_return": total_return,
  1870. "net_annualized_return": annualized_return,
  1871. "net_max_drawdown": max_drawdown,
  1872. "net_calmar": annualized_return / max_drawdown if max_drawdown else 0.0,
  1873. "net_sharpe_daily": sharpe,
  1874. }
  1875. def recent_horizon_metrics_from_equity(
  1876. frame: pd.DataFrame,
  1877. last_ts: int,
  1878. horizons: tuple[tuple[str, pd.DateOffset], ...],
  1879. ) -> pd.DataFrame:
  1880. rows: list[dict[str, object]] = []
  1881. end_time = pd.to_datetime(last_ts, unit="ms", utc=True)
  1882. for label, offset in horizons:
  1883. cutoff = end_time - offset
  1884. before_cutoff = frame[frame["ts"] <= cutoff]
  1885. if len(before_cutoff):
  1886. start_equity = float(before_cutoff["equity"].iloc[-1])
  1887. start_time = cutoff
  1888. after_cutoff = frame[frame["ts"] > cutoff]
  1889. horizon_frame = pd.concat(
  1890. [
  1891. pd.DataFrame([{"ts": start_time, "equity": start_equity}]),
  1892. after_cutoff[["ts", "equity"]],
  1893. ],
  1894. ignore_index=True,
  1895. )
  1896. else:
  1897. horizon_frame = frame[["ts", "equity"]].copy()
  1898. start_time = pd.Timestamp(horizon_frame["ts"].iloc[0])
  1899. metrics = annualized_metrics_from_equity(
  1900. horizon_frame,
  1901. int(start_time.timestamp() * 1000),
  1902. last_ts,
  1903. )
  1904. rows.append(
  1905. {
  1906. "horizon": label,
  1907. "horizon_start": start_time.strftime("%Y-%m-%d %H:%M"),
  1908. "horizon_end": end_time.strftime("%Y-%m-%d %H:%M"),
  1909. "horizon_days": (end_time - start_time).total_seconds() / 86_400,
  1910. **metrics,
  1911. }
  1912. )
  1913. return pd.DataFrame(rows)
  1914. def build_rsi2_candidate(trend: int, long_threshold: float, short_threshold: float) -> Candidate:
  1915. config = RSI2Config(
  1916. trend_sma=trend,
  1917. rsi_length=2,
  1918. rsi_long_threshold=long_threshold,
  1919. rsi_short_threshold=short_threshold,
  1920. exit_rsi=50.0,
  1921. )
  1922. return Candidate(
  1923. f"rsi2-t{trend}-l{long_threshold}-s{short_threshold}",
  1924. max(trend, 3),
  1925. lambda candles, leverage, warmup_bars, config=config: run_rsi2_segment(
  1926. candles=candles,
  1927. leverage=leverage,
  1928. warmup_bars=warmup_bars,
  1929. config=config,
  1930. ),
  1931. )
  1932. def build_rsi2_side_candidate(
  1933. trend: int,
  1934. long_threshold: float,
  1935. short_threshold: float,
  1936. exit_rsi: float,
  1937. side_mode: str,
  1938. ) -> Candidate:
  1939. config = RSI2Config(
  1940. trend_sma=trend,
  1941. rsi_length=2,
  1942. rsi_long_threshold=long_threshold,
  1943. rsi_short_threshold=short_threshold,
  1944. exit_rsi=exit_rsi,
  1945. )
  1946. return Candidate(
  1947. f"rsi2-{side_mode}-t{trend}-l{long_threshold}-s{short_threshold}-x{exit_rsi}",
  1948. max(trend, 3),
  1949. lambda candles, leverage, warmup_bars, config=config, side_mode=side_mode: run_rsi2_side_segment(
  1950. candles=candles,
  1951. leverage=leverage,
  1952. warmup_bars=warmup_bars,
  1953. config=config,
  1954. side_mode=side_mode,
  1955. ),
  1956. )
  1957. def build_rsi2_long_guarded_candidate(
  1958. trend: int,
  1959. rsi_threshold: float,
  1960. exit_rsi: float,
  1961. stop_loss_pct: float,
  1962. max_hold_bars: int,
  1963. ) -> Candidate:
  1964. return Candidate(
  1965. f"rsi2-long-guarded-t{trend}-l{rsi_threshold}-x{exit_rsi}-sl{stop_loss_pct}-mh{max_hold_bars}",
  1966. max(trend, 3),
  1967. lambda candles, leverage, warmup_bars, trend=trend, rsi_threshold=rsi_threshold, exit_rsi=exit_rsi, stop_loss_pct=stop_loss_pct, max_hold_bars=max_hold_bars: run_rsi2_long_guarded_segment(
  1968. candles=candles,
  1969. leverage=leverage,
  1970. warmup_bars=warmup_bars,
  1971. trend_sma=trend,
  1972. rsi_threshold=rsi_threshold,
  1973. exit_rsi=exit_rsi,
  1974. stop_loss_pct=stop_loss_pct,
  1975. max_hold_bars=max_hold_bars,
  1976. ),
  1977. )
  1978. def build_rsi2_long_guarded_twap_candidate(
  1979. trend: int,
  1980. rsi_threshold: float,
  1981. exit_rsi: float,
  1982. stop_loss_pct: float,
  1983. max_hold_bars: int,
  1984. entry_slices: int,
  1985. ) -> Candidate:
  1986. return Candidate(
  1987. f"rsi2-long-guarded-twap{entry_slices}-t{trend}-l{rsi_threshold}-x{exit_rsi}-sl{stop_loss_pct}-mh{max_hold_bars}",
  1988. max(trend, 3),
  1989. lambda candles, leverage, warmup_bars, trend=trend, rsi_threshold=rsi_threshold, exit_rsi=exit_rsi, stop_loss_pct=stop_loss_pct, max_hold_bars=max_hold_bars, entry_slices=entry_slices: run_rsi2_long_guarded_twap_segment(
  1990. candles=candles,
  1991. leverage=leverage,
  1992. warmup_bars=warmup_bars,
  1993. trend_sma=trend,
  1994. rsi_threshold=rsi_threshold,
  1995. exit_rsi=exit_rsi,
  1996. stop_loss_pct=stop_loss_pct,
  1997. max_hold_bars=max_hold_bars,
  1998. entry_slices=entry_slices,
  1999. ),
  2000. )
  2001. def build_rsi2_long_guarded_price_twap_candidate(
  2002. trend: int,
  2003. rsi_threshold: float,
  2004. exit_rsi: float,
  2005. stop_loss_pct: float,
  2006. max_hold_bars: int,
  2007. entry_offsets: tuple[float, ...],
  2008. entry_valid_bars: int,
  2009. fill_buffer: float = 0.0,
  2010. ) -> Candidate:
  2011. offset_label = "-".join(f"{offset:.4f}" for offset in entry_offsets)
  2012. buffer_label = f"-fb{fill_buffer:.4f}" if fill_buffer else ""
  2013. return Candidate(
  2014. f"rsi2-long-guarded-price-twap-o{offset_label}-v{entry_valid_bars}{buffer_label}-t{trend}-l{rsi_threshold}-x{exit_rsi}-sl{stop_loss_pct}-mh{max_hold_bars}",
  2015. max(trend, 3),
  2016. lambda candles, leverage, warmup_bars, trend=trend, rsi_threshold=rsi_threshold, exit_rsi=exit_rsi, stop_loss_pct=stop_loss_pct, max_hold_bars=max_hold_bars, entry_offsets=entry_offsets, entry_valid_bars=entry_valid_bars, fill_buffer=fill_buffer: run_rsi2_long_guarded_price_twap_segment(
  2017. candles=candles,
  2018. leverage=leverage,
  2019. warmup_bars=warmup_bars,
  2020. trend_sma=trend,
  2021. rsi_threshold=rsi_threshold,
  2022. exit_rsi=exit_rsi,
  2023. stop_loss_pct=stop_loss_pct,
  2024. max_hold_bars=max_hold_bars,
  2025. entry_offsets=entry_offsets,
  2026. entry_valid_bars=entry_valid_bars,
  2027. fill_buffer=fill_buffer,
  2028. ),
  2029. )
  2030. def build_ma_cross_candidate(fast: int, slow: int, side_mode: str) -> Candidate:
  2031. return Candidate(
  2032. f"ma-cross-{side_mode}-f{fast}-s{slow}",
  2033. slow,
  2034. lambda candles, leverage, warmup_bars, fast=fast, slow=slow, side_mode=side_mode: run_ma_cross_segment(
  2035. candles=candles,
  2036. leverage=leverage,
  2037. warmup_bars=warmup_bars,
  2038. fast=fast,
  2039. slow=slow,
  2040. side_mode=side_mode,
  2041. ),
  2042. )
  2043. def build_trend_rsi_bb_long_candidate(
  2044. trend_sma: int,
  2045. band_length: int,
  2046. std_multiplier: float,
  2047. rsi_threshold: float,
  2048. exit_rsi: float,
  2049. stop_loss_pct: float,
  2050. ) -> Candidate:
  2051. return Candidate(
  2052. f"trend-rsi-bb-long-t{trend_sma}-b{band_length}-m{std_multiplier}-r{rsi_threshold}-x{exit_rsi}-sl{stop_loss_pct}",
  2053. max(trend_sma, band_length, 3),
  2054. lambda candles, leverage, warmup_bars, trend_sma=trend_sma, band_length=band_length, std_multiplier=std_multiplier, rsi_threshold=rsi_threshold, exit_rsi=exit_rsi, stop_loss_pct=stop_loss_pct: run_trend_rsi_bb_long_segment(
  2055. candles=candles,
  2056. leverage=leverage,
  2057. warmup_bars=warmup_bars,
  2058. trend_sma=trend_sma,
  2059. band_length=band_length,
  2060. std_multiplier=std_multiplier,
  2061. rsi_threshold=rsi_threshold,
  2062. exit_rsi=exit_rsi,
  2063. stop_loss_pct=stop_loss_pct,
  2064. ),
  2065. )
  2066. def build_regime_hybrid_candidate(
  2067. trend_sma: int,
  2068. regime_lookback: int,
  2069. neutral_ma_distance: float,
  2070. rsi_long_threshold: float,
  2071. rsi_exit: float,
  2072. bb_std: float,
  2073. stop_loss_pct: float,
  2074. ) -> Candidate:
  2075. return Candidate(
  2076. f"regime-hybrid-t{trend_sma}-r{regime_lookback}-n{neutral_ma_distance}-l{rsi_long_threshold}-x{rsi_exit}-m{bb_std}-sl{stop_loss_pct}",
  2077. max(trend_sma, regime_lookback, 20, 50, 3),
  2078. lambda candles, leverage, warmup_bars, trend_sma=trend_sma, regime_lookback=regime_lookback, neutral_ma_distance=neutral_ma_distance, rsi_long_threshold=rsi_long_threshold, rsi_exit=rsi_exit, bb_std=bb_std, stop_loss_pct=stop_loss_pct: run_regime_hybrid_segment(
  2079. candles=candles,
  2080. leverage=leverage,
  2081. warmup_bars=warmup_bars,
  2082. trend_sma=trend_sma,
  2083. regime_lookback=regime_lookback,
  2084. neutral_ma_distance=neutral_ma_distance,
  2085. rsi_long_threshold=rsi_long_threshold,
  2086. rsi_exit=rsi_exit,
  2087. bb_length=20,
  2088. bb_std=bb_std,
  2089. bb_bandwidth_lookback=50,
  2090. stop_loss_pct=stop_loss_pct,
  2091. ),
  2092. )
  2093. def build_eth_btc_rsi_filter_candidate(
  2094. eth_trend_sma: int,
  2095. eth_rsi_threshold: float,
  2096. eth_exit_rsi: float,
  2097. btc_trend_sma: int,
  2098. btc_momentum_lookback: int,
  2099. btc_min_momentum: float,
  2100. ) -> PairCandidate:
  2101. return PairCandidate(
  2102. f"eth-btc-rsi-filter-et{eth_trend_sma}-l{eth_rsi_threshold}-x{eth_exit_rsi}-bt{btc_trend_sma}-bm{btc_momentum_lookback}-br{btc_min_momentum}",
  2103. max(eth_trend_sma, btc_trend_sma, btc_momentum_lookback, 3),
  2104. lambda eth_candles, btc_candles, leverage, warmup_bars, eth_trend_sma=eth_trend_sma, eth_rsi_threshold=eth_rsi_threshold, eth_exit_rsi=eth_exit_rsi, btc_trend_sma=btc_trend_sma, btc_momentum_lookback=btc_momentum_lookback, btc_min_momentum=btc_min_momentum: run_eth_btc_rsi_filter_segment(
  2105. eth_candles=eth_candles,
  2106. btc_candles=btc_candles,
  2107. leverage=leverage,
  2108. warmup_bars=warmup_bars,
  2109. eth_trend_sma=eth_trend_sma,
  2110. eth_rsi_threshold=eth_rsi_threshold,
  2111. eth_exit_rsi=eth_exit_rsi,
  2112. btc_trend_sma=btc_trend_sma,
  2113. btc_momentum_lookback=btc_momentum_lookback,
  2114. btc_min_momentum=btc_min_momentum,
  2115. ),
  2116. )
  2117. def build_eth_btc_shock_filter_candidate(
  2118. eth_trend_sma: int,
  2119. eth_rsi_threshold: float,
  2120. eth_exit_rsi: float,
  2121. btc_trend_sma: int,
  2122. btc_momentum_lookback: int,
  2123. btc_min_momentum: float,
  2124. btc_shock_lookback: int,
  2125. btc_max_realized_vol: float,
  2126. btc_max_drawdown: float,
  2127. ) -> PairCandidate:
  2128. return PairCandidate(
  2129. f"eth-btc-shock-filter-et{eth_trend_sma}-l{eth_rsi_threshold}-x{eth_exit_rsi}-bt{btc_trend_sma}-bm{btc_momentum_lookback}-br{btc_min_momentum}-sw{btc_shock_lookback}-sv{btc_max_realized_vol}-sd{btc_max_drawdown}",
  2130. max(eth_trend_sma, btc_trend_sma, btc_momentum_lookback, btc_shock_lookback + 1, 3),
  2131. lambda eth_candles, btc_candles, leverage, warmup_bars, eth_trend_sma=eth_trend_sma, eth_rsi_threshold=eth_rsi_threshold, eth_exit_rsi=eth_exit_rsi, btc_trend_sma=btc_trend_sma, btc_momentum_lookback=btc_momentum_lookback, btc_min_momentum=btc_min_momentum, btc_shock_lookback=btc_shock_lookback, btc_max_realized_vol=btc_max_realized_vol, btc_max_drawdown=btc_max_drawdown: run_eth_btc_shock_filter_segment(
  2132. eth_candles=eth_candles,
  2133. btc_candles=btc_candles,
  2134. leverage=leverage,
  2135. warmup_bars=warmup_bars,
  2136. eth_trend_sma=eth_trend_sma,
  2137. eth_rsi_threshold=eth_rsi_threshold,
  2138. eth_exit_rsi=eth_exit_rsi,
  2139. btc_trend_sma=btc_trend_sma,
  2140. btc_momentum_lookback=btc_momentum_lookback,
  2141. btc_min_momentum=btc_min_momentum,
  2142. btc_shock_lookback=btc_shock_lookback,
  2143. btc_max_realized_vol=btc_max_realized_vol,
  2144. btc_max_drawdown=btc_max_drawdown,
  2145. ),
  2146. )
  2147. def build_eth_btc_ratio_pullback_candidate(
  2148. btc_trend_sma: int,
  2149. btc_momentum_lookback: int,
  2150. btc_min_momentum: float,
  2151. ratio_length: int,
  2152. ratio_std: float,
  2153. ratio_rsi_threshold: float,
  2154. stop_loss_pct: float,
  2155. ) -> PairCandidate:
  2156. return PairCandidate(
  2157. f"eth-btc-ratio-pullback-bt{btc_trend_sma}-bm{btc_momentum_lookback}-br{btc_min_momentum}-rl{ratio_length}-rs{ratio_std}-rr{ratio_rsi_threshold}-sl{stop_loss_pct}",
  2158. max(btc_trend_sma, btc_momentum_lookback, ratio_length, 3),
  2159. lambda eth_candles, btc_candles, leverage, warmup_bars, btc_trend_sma=btc_trend_sma, btc_momentum_lookback=btc_momentum_lookback, btc_min_momentum=btc_min_momentum, ratio_length=ratio_length, ratio_std=ratio_std, ratio_rsi_threshold=ratio_rsi_threshold, stop_loss_pct=stop_loss_pct: run_eth_btc_ratio_pullback_segment(
  2160. eth_candles=eth_candles,
  2161. btc_candles=btc_candles,
  2162. leverage=leverage,
  2163. warmup_bars=warmup_bars,
  2164. btc_trend_sma=btc_trend_sma,
  2165. btc_momentum_lookback=btc_momentum_lookback,
  2166. btc_min_momentum=btc_min_momentum,
  2167. ratio_length=ratio_length,
  2168. ratio_std=ratio_std,
  2169. ratio_rsi_threshold=ratio_rsi_threshold,
  2170. stop_loss_pct=stop_loss_pct,
  2171. ),
  2172. )
  2173. def build_btc_lead_eth_lag_candidate(
  2174. lead_lookback: int,
  2175. btc_return_threshold: float,
  2176. lag_gap: float,
  2177. max_hold_bars: int,
  2178. stop_loss_pct: float,
  2179. take_profit_pct: float,
  2180. ) -> PairCandidate:
  2181. return PairCandidate(
  2182. f"btc-lead-eth-lag-lb{lead_lookback}-br{btc_return_threshold}-gap{lag_gap}-mh{max_hold_bars}-sl{stop_loss_pct}-tp{take_profit_pct}",
  2183. lead_lookback,
  2184. lambda eth_candles, btc_candles, leverage, warmup_bars, lead_lookback=lead_lookback, btc_return_threshold=btc_return_threshold, lag_gap=lag_gap, max_hold_bars=max_hold_bars, stop_loss_pct=stop_loss_pct, take_profit_pct=take_profit_pct: run_btc_lead_eth_lag_segment(
  2185. eth_candles=eth_candles,
  2186. btc_candles=btc_candles,
  2187. leverage=leverage,
  2188. warmup_bars=warmup_bars,
  2189. lead_lookback=lead_lookback,
  2190. btc_return_threshold=btc_return_threshold,
  2191. lag_gap=lag_gap,
  2192. max_hold_bars=max_hold_bars,
  2193. stop_loss_pct=stop_loss_pct,
  2194. take_profit_pct=take_profit_pct,
  2195. ),
  2196. )
  2197. def history_bars_for_years(bar: str, years: float) -> int:
  2198. if not bar.endswith("m"):
  2199. raise ValueError("minute bar is required")
  2200. minutes = int(bar[:-1])
  2201. if minutes <= 0:
  2202. raise ValueError("minute bar is required")
  2203. return int(MINUTES_PER_YEAR * years / minutes)
  2204. def build_strategy_timeframe_candidates() -> list[Candidate]:
  2205. return [
  2206. Candidate("bbmr-default", 69, lambda candles, leverage, warmup_bars: run_bbmr_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=BBMRConfig())),
  2207. Candidate("bbsb-default", 69, lambda candles, leverage, warmup_bars: run_bbsb_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=BBSBConfig())),
  2208. Candidate("donchian-e12-x6-s0.008", 12, lambda candles, leverage, warmup_bars: run_donchian_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=DonchianConfig(entry_window=12, exit_window=6, stop_loss_pct=0.008))),
  2209. build_rsi2_candidate(50, 3.0, 97.0),
  2210. Candidate("ema-pullback-f13-s34-b0.006", 34, lambda candles, leverage, warmup_bars: run_ema_pullback_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=EMAPullbackConfig(fast_ema=13, slow_ema=34, stop_buffer_pct=0.006))),
  2211. Candidate("range-momo-l10-tp0.006-sl0.004", 10, lambda candles, leverage, warmup_bars: run_range_momentum_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, lookback=10, take_profit_pct=0.006, stop_loss_pct=0.004)),
  2212. Candidate("vwap-revert-w72-z2.0-sl0.006", 144, lambda candles, leverage, warmup_bars: run_vwap_reversion_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, window=72, entry_z=2.0, exit_z=0.2, stop_loss_pct=0.006)),
  2213. ]
  2214. def summarize_periods(frame: pd.DataFrame, period: str, roundtrip_cost_on_margin: float) -> pd.DataFrame:
  2215. period_frame = frame.copy()
  2216. period_frame["period"] = pd.to_datetime(period_frame["window_end_ts"], unit="ms", utc=True).dt.tz_localize(None).dt.to_period(period).astype(str)
  2217. grouped = (
  2218. period_frame.groupby("period", as_index=False)
  2219. .agg(
  2220. window_count=("total_return", "size"),
  2221. avg_return=("total_return", "mean"),
  2222. median_return=("total_return", "median"),
  2223. positive_window_rate=("total_return", lambda values: float((values > 0.0).mean())),
  2224. worst_return=("total_return", "min"),
  2225. best_return=("total_return", "max"),
  2226. trades=("trade_count", "sum"),
  2227. avg_trades_per_window=("trade_count", "mean"),
  2228. avg_window_win_rate=("win_rate", "mean"),
  2229. max_drawdown=("max_drawdown", "max"),
  2230. )
  2231. .sort_values("period")
  2232. )
  2233. grouped["net_avg_return"] = grouped["avg_return"] - grouped["avg_trades_per_window"] * roundtrip_cost_on_margin
  2234. return grouped
  2235. def summarize_equity_periods(result: SegmentResult, period: str) -> pd.DataFrame:
  2236. frame = pd.DataFrame(result.equity_curve)
  2237. frame["period"] = pd.to_datetime(frame["ts"], unit="ms", utc=True).dt.tz_localize(None).dt.to_period(period).astype(str)
  2238. grouped = (
  2239. frame.groupby("period", as_index=False)
  2240. .agg(
  2241. start_equity=("equity", "first"),
  2242. end_equity=("equity", "last"),
  2243. min_equity=("equity", "min"),
  2244. max_equity=("equity", "max"),
  2245. bars=("equity", "size"),
  2246. )
  2247. .sort_values("period")
  2248. )
  2249. grouped["return"] = grouped["end_equity"] / grouped["start_equity"] - 1.0
  2250. grouped["drawdown_from_period_high"] = (grouped["max_equity"] - grouped["min_equity"]) / grouped["max_equity"]
  2251. return grouped
  2252. def summarize_cost_adjusted_trade_equity_periods(
  2253. result: SegmentResult,
  2254. period: str,
  2255. roundtrip_cost_on_margin: float,
  2256. ) -> pd.DataFrame:
  2257. frame = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
  2258. frame["period"] = frame["ts"].dt.tz_localize(None).dt.to_period(period).astype(str)
  2259. grouped = (
  2260. frame.groupby("period", as_index=False)
  2261. .agg(
  2262. start_equity=("equity", "first"),
  2263. end_equity=("equity", "last"),
  2264. min_equity=("equity", "min"),
  2265. max_equity=("equity", "max"),
  2266. trades=("equity", lambda values: max(len(values) - 1, 0)),
  2267. )
  2268. .sort_values("period")
  2269. )
  2270. grouped["return"] = grouped["end_equity"] / grouped["start_equity"] - 1.0
  2271. grouped["drawdown_from_period_high"] = (grouped["max_equity"] - grouped["min_equity"]) / grouped["max_equity"]
  2272. return grouped
  2273. def add_market_regime_columns(candles: list[Candle], rows: list[dict[str, object]], roundtrip_cost_on_margin: float) -> pd.DataFrame:
  2274. index_by_ts = {candle.ts: index for index, candle in enumerate(candles)}
  2275. output_rows: list[dict[str, object]] = []
  2276. closes = pd.Series([candle.close for candle in candles], dtype=float)
  2277. long_ma = closes.rolling(240).mean().tolist()
  2278. for row in rows:
  2279. start = index_by_ts[int(row["window_start_ts"])]
  2280. end = index_by_ts[int(row["window_end_ts"])]
  2281. window_closes = closes.iloc[start : end + 1]
  2282. returns = window_closes.pct_change().dropna()
  2283. ma_value = long_ma[end]
  2284. close_value = float(window_closes.iloc[-1])
  2285. output_rows.append(
  2286. {
  2287. **{key: value for key, value in row.items() if key != "trades"},
  2288. "net_return": float(row["total_return"]) - int(row["trade_count"]) * roundtrip_cost_on_margin,
  2289. "market_return": close_value / float(window_closes.iloc[0]) - 1.0,
  2290. "realized_vol": float(returns.std(ddof=1)) if len(returns) > 1 else 0.0,
  2291. "ma240_distance": close_value / float(ma_value) - 1.0 if ma_value == ma_value and ma_value else 0.0,
  2292. }
  2293. )
  2294. frame = pd.DataFrame(output_rows)
  2295. frame["market_return_bucket"] = pd.qcut(frame["market_return"], 3, labels=["down", "flat", "up"], duplicates="drop")
  2296. frame["realized_vol_bucket"] = pd.qcut(frame["realized_vol"], 3, labels=["low", "mid", "high"], duplicates="drop")
  2297. frame["ma240_distance_bucket"] = pd.qcut(frame["ma240_distance"], 3, labels=["below", "near", "above"], duplicates="drop")
  2298. return frame
  2299. def summarize_regime_columns(frame: pd.DataFrame) -> pd.DataFrame:
  2300. summaries: list[pd.DataFrame] = []
  2301. for column in ("market_return_bucket", "realized_vol_bucket", "ma240_distance_bucket"):
  2302. grouped = (
  2303. frame.groupby(["symbol", "bar", "name", column], observed=True, as_index=False)
  2304. .agg(
  2305. sample_count=("net_return", "size"),
  2306. avg_net_return=("net_return", "mean"),
  2307. median_net_return=("net_return", "median"),
  2308. positive_window_rate=("net_return", lambda values: float((values > 0.0).mean())),
  2309. worst_net_return=("net_return", "min"),
  2310. best_net_return=("net_return", "max"),
  2311. avg_trades=("trade_count", "mean"),
  2312. avg_market_return=("market_return", "mean"),
  2313. avg_realized_vol=("realized_vol", "mean"),
  2314. avg_ma240_distance=("ma240_distance", "mean"),
  2315. )
  2316. .rename(columns={column: "regime"})
  2317. )
  2318. grouped.insert(3, "regime_type", column.removesuffix("_bucket"))
  2319. summaries.append(grouped)
  2320. return pd.concat(summaries, ignore_index=True).sort_values(
  2321. ["name", "regime_type", "avg_net_return"],
  2322. ascending=[True, True, False],
  2323. )
  2324. def main() -> int:
  2325. client = OkxClient()
  2326. candidates = build_candidates()
  2327. rows: list[dict[str, object]] = []
  2328. for symbol in SYMBOLS:
  2329. for bar in BARS:
  2330. candles = get_candles_cached(client, symbol, bar, HISTORY_LIMIT)
  2331. for candidate in candidates:
  2332. metrics = evaluate_candidate(candidate, candles)
  2333. rows.append({"symbol": symbol, "bar": bar, **metrics})
  2334. print(f"done {symbol} {bar}")
  2335. frame = pd.DataFrame(rows)
  2336. frame["score"] = frame["avg_return"] - frame["max_drawdown"] * 0.25
  2337. columns = ["symbol", "bar", "name", "avg_return", "median_return", "worst_return", "best_return", "trades", "win_rate", "max_drawdown", "score"]
  2338. print(frame.sort_values(["avg_return", "median_return"], ascending=False)[columns].head(30).to_string(index=False))
  2339. frame.to_csv("ultrashort-exploration.csv", index=False)
  2340. return 0
  2341. def focus_vwap() -> int:
  2342. client = OkxClient()
  2343. candles = get_candles_cached(client, "ETH-USDT-SWAP", "3m", HISTORY_LIMIT)
  2344. rows: list[dict[str, object]] = []
  2345. for seed in (3, 7, 11, 17, 23):
  2346. for window in (56, 64, 72, 80, 96):
  2347. for entry_z in (1.6, 1.8, 2.0, 2.2, 2.4):
  2348. for stop in (0.004, 0.006, 0.008):
  2349. warmup_bars = window * 2
  2350. sampled = sample_segments(
  2351. candles=candles,
  2352. segments=SEGMENTS,
  2353. window_size=WINDOW_SIZE,
  2354. warmup_bars=warmup_bars,
  2355. seed=seed,
  2356. )
  2357. results = [
  2358. run_vwap_reversion_segment(
  2359. candles=candles[segment.context_start : segment.report_end],
  2360. leverage=LEVERAGE,
  2361. warmup_bars=warmup_bars,
  2362. window=window,
  2363. entry_z=entry_z,
  2364. exit_z=0.2,
  2365. stop_loss_pct=stop,
  2366. )
  2367. for segment in sampled
  2368. ]
  2369. returns = [result.total_return for result in results]
  2370. rows.append(
  2371. {
  2372. "seed": seed,
  2373. "window": window,
  2374. "entry_z": entry_z,
  2375. "stop": stop,
  2376. "avg_return": sum(returns) / len(returns),
  2377. "median_return": float(pd.Series(returns).median()),
  2378. "worst_return": min(returns),
  2379. "best_return": max(returns),
  2380. "trades": sum(result.trade_count for result in results),
  2381. "win_rate": sum(result.win_rate for result in results) / len(results),
  2382. "max_drawdown": max(result.max_drawdown for result in results),
  2383. }
  2384. )
  2385. frame = pd.DataFrame(rows)
  2386. grouped = (
  2387. frame.groupby(["window", "entry_z", "stop"], as_index=False)
  2388. .agg(
  2389. avg_return=("avg_return", "mean"),
  2390. median_return=("median_return", "mean"),
  2391. worst_seed_avg=("avg_return", "min"),
  2392. worst_window_return=("worst_return", "min"),
  2393. trades=("trades", "mean"),
  2394. win_rate=("win_rate", "mean"),
  2395. max_drawdown=("max_drawdown", "max"),
  2396. )
  2397. .sort_values(["avg_return", "worst_seed_avg"], ascending=False)
  2398. )
  2399. print(grouped.head(30).to_string(index=False))
  2400. grouped.to_csv("ultrashort-vwap-focus.csv", index=False)
  2401. return 0
  2402. def robust_vwap(history_limit: int, window_size: int) -> int:
  2403. client = OkxClient()
  2404. rows: list[dict[str, object]] = []
  2405. candidates = [
  2406. Candidate(
  2407. f"vwap-revert-w{window}-z{entry_z}-sl{stop}",
  2408. window * 2,
  2409. lambda candles, leverage, warmup_bars, window=window, entry_z=entry_z, stop=stop: run_vwap_reversion_segment(
  2410. candles=candles,
  2411. leverage=leverage,
  2412. warmup_bars=warmup_bars,
  2413. window=window,
  2414. entry_z=entry_z,
  2415. exit_z=0.2,
  2416. stop_loss_pct=stop,
  2417. ),
  2418. )
  2419. for window in (56, 64, 72, 80, 96)
  2420. for entry_z in (1.6, 1.8, 2.0, 2.2, 2.4)
  2421. for stop in (0.004, 0.006, 0.008)
  2422. ]
  2423. for symbol in SYMBOLS:
  2424. candles = get_candles_cached(client, symbol, "3m", history_limit)
  2425. for candidate in candidates:
  2426. metrics = evaluate_candidate_all_windows(
  2427. candidate=candidate,
  2428. candles=candles,
  2429. window_size=window_size,
  2430. leverage=LEVERAGE,
  2431. )
  2432. rows.append({"symbol": symbol, "bar": "3m", "history_bars": len(candles), **metrics})
  2433. print(f"done robust {symbol} 3m {len(candles)} bars")
  2434. frame = pd.DataFrame(rows)
  2435. columns = [
  2436. "symbol",
  2437. "bar",
  2438. "history_bars",
  2439. "name",
  2440. "sample_count",
  2441. "avg_return",
  2442. "ci95_low",
  2443. "ci95_high",
  2444. "median_return",
  2445. "positive_window_rate",
  2446. "worst_return",
  2447. "p10_return",
  2448. "p90_return",
  2449. "best_return",
  2450. "trades",
  2451. "avg_trades_per_window",
  2452. "win_rate",
  2453. "trade_win_rate",
  2454. "avg_trade_return",
  2455. "avg_win_return",
  2456. "avg_loss_return_abs",
  2457. "payoff_ratio",
  2458. "profit_factor",
  2459. "expectancy_per_trade",
  2460. "max_drawdown",
  2461. "return_drawdown_ratio",
  2462. ]
  2463. frame = sort_robust_results(frame)
  2464. print(GROSS_RETURN_NOTE)
  2465. print(frame[columns].head(30).to_string(index=False))
  2466. frame.to_csv("ultrashort-vwap-robust.csv", index=False)
  2467. return 0
  2468. def robust_all(history_limit: int, window_size: int) -> int:
  2469. client = OkxClient()
  2470. candidates = build_candidates()
  2471. rows: list[dict[str, object]] = []
  2472. for symbol in SYMBOLS:
  2473. for bar in BARS:
  2474. candles = get_candles_cached(client, symbol, bar, history_limit)
  2475. for candidate in candidates:
  2476. metrics = evaluate_candidate_all_windows(
  2477. candidate=candidate,
  2478. candles=candles,
  2479. window_size=window_size,
  2480. leverage=LEVERAGE,
  2481. )
  2482. rows.append({"symbol": symbol, "bar": bar, "history_bars": len(candles), **metrics})
  2483. print(f"done robust all {symbol} {bar} {len(candles)} bars")
  2484. frame = pd.DataFrame(rows)
  2485. columns = [
  2486. "symbol",
  2487. "bar",
  2488. "history_bars",
  2489. "name",
  2490. "sample_count",
  2491. "avg_return",
  2492. "ci95_low",
  2493. "ci95_high",
  2494. "median_return",
  2495. "positive_window_rate",
  2496. "worst_return",
  2497. "p10_return",
  2498. "p90_return",
  2499. "best_return",
  2500. "trades",
  2501. "avg_trades_per_window",
  2502. "win_rate",
  2503. "trade_win_rate",
  2504. "avg_trade_return",
  2505. "avg_win_return",
  2506. "avg_loss_return_abs",
  2507. "payoff_ratio",
  2508. "profit_factor",
  2509. "expectancy_per_trade",
  2510. "max_drawdown",
  2511. "return_drawdown_ratio",
  2512. ]
  2513. frame = sort_robust_results(frame)
  2514. print(GROSS_RETURN_NOTE)
  2515. print(frame[columns].head(40).to_string(index=False))
  2516. frame.to_csv("ultrashort-robust-all.csv", index=False)
  2517. return 0
  2518. def robust_rsi2_cost_search(history_limit: int, window_size: int, roundtrip_cost_on_margin: float) -> int:
  2519. client = OkxClient()
  2520. candidates = [
  2521. build_rsi2_candidate(trend, long_threshold, short_threshold)
  2522. for trend in (30, 50, 80, 120, 160, 240)
  2523. for long_threshold, short_threshold in (
  2524. (3.0, 97.0),
  2525. (5.0, 95.0),
  2526. (8.0, 92.0),
  2527. (10.0, 90.0),
  2528. (12.0, 88.0),
  2529. (15.0, 85.0),
  2530. (18.0, 82.0),
  2531. )
  2532. ]
  2533. rows: list[dict[str, object]] = []
  2534. for symbol in SYMBOLS:
  2535. for bar in ("3m", "5m", "15m"):
  2536. candles = get_candles_cached(client, symbol, bar, history_limit)
  2537. for candidate in candidates:
  2538. metrics = evaluate_candidate_all_windows(
  2539. candidate=candidate,
  2540. candles=candles,
  2541. window_size=window_size,
  2542. leverage=LEVERAGE,
  2543. )
  2544. rows.append({"symbol": symbol, "bar": bar, "history_bars": len(candles), **metrics})
  2545. print(f"done robust rsi2 cost {symbol} {bar} {len(candles)} bars")
  2546. frame = sort_cost_results(add_cost_metrics(pd.DataFrame(rows), roundtrip_cost_on_margin))
  2547. columns = [
  2548. "symbol",
  2549. "bar",
  2550. "history_bars",
  2551. "name",
  2552. "sample_count",
  2553. "avg_return",
  2554. "ci95_low",
  2555. "avg_trades_per_window",
  2556. "breakeven_roundtrip_cost_on_margin",
  2557. "roundtrip_cost_on_margin",
  2558. "net_avg_return",
  2559. "net_ci95_low",
  2560. "net_ci95_high",
  2561. "median_return",
  2562. "positive_window_rate",
  2563. "worst_return",
  2564. "trade_win_rate",
  2565. "avg_trade_return",
  2566. "avg_win_return",
  2567. "avg_loss_return_abs",
  2568. "payoff_ratio",
  2569. "profit_factor",
  2570. "expectancy_per_trade",
  2571. "max_drawdown",
  2572. "return_drawdown_ratio",
  2573. ]
  2574. print(GROSS_RETURN_NOTE)
  2575. print(frame[columns].head(40).to_string(index=False))
  2576. frame.to_csv("ultrashort-rsi2-cost-search.csv", index=False)
  2577. return 0
  2578. def rsi2_period_analysis(
  2579. *,
  2580. symbol: str,
  2581. bar: str,
  2582. history_limit: int,
  2583. window_size: int,
  2584. roundtrip_cost_on_margin: float,
  2585. trend: int,
  2586. long_threshold: float,
  2587. short_threshold: float,
  2588. ) -> int:
  2589. client = OkxClient()
  2590. candles = get_candles_cached(client, symbol, bar, history_limit)
  2591. candidate = build_rsi2_candidate(trend, long_threshold, short_threshold)
  2592. rows = evaluate_candidate_window_rows(
  2593. candidate=candidate,
  2594. candles=candles,
  2595. window_size=window_size,
  2596. leverage=LEVERAGE,
  2597. )
  2598. window_frame = pd.DataFrame(rows).drop(columns=["trades"])
  2599. window_frame["window_start"] = pd.to_datetime(window_frame["window_start_ts"], unit="ms", utc=True).dt.strftime("%Y-%m-%d %H:%M")
  2600. window_frame["window_end"] = pd.to_datetime(window_frame["window_end_ts"], unit="ms", utc=True).dt.strftime("%Y-%m-%d %H:%M")
  2601. window_frame["net_return"] = window_frame["total_return"] - window_frame["trade_count"] * roundtrip_cost_on_margin
  2602. window_frame.insert(0, "name", candidate.name)
  2603. window_frame.insert(0, "history_bars", len(candles))
  2604. window_frame.insert(0, "bar", bar)
  2605. window_frame.insert(0, "symbol", symbol)
  2606. monthly = summarize_periods(window_frame, "M", roundtrip_cost_on_margin)
  2607. quarterly = summarize_periods(window_frame, "Q", roundtrip_cost_on_margin)
  2608. full_result = candidate.run(candles=candles, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
  2609. equity_monthly = summarize_equity_periods(full_result, "M")
  2610. equity_quarterly = summarize_equity_periods(full_result, "Q")
  2611. net_trade_equity_monthly = summarize_cost_adjusted_trade_equity_periods(full_result, "M", roundtrip_cost_on_margin)
  2612. net_trade_equity_quarterly = summarize_cost_adjusted_trade_equity_periods(full_result, "Q", roundtrip_cost_on_margin)
  2613. window_frame.to_csv("ultrashort-rsi2-window-distribution.csv", index=False)
  2614. monthly.to_csv("ultrashort-rsi2-monthly.csv", index=False)
  2615. quarterly.to_csv("ultrashort-rsi2-quarterly.csv", index=False)
  2616. equity_monthly.to_csv("ultrashort-rsi2-equity-monthly.csv", index=False)
  2617. equity_quarterly.to_csv("ultrashort-rsi2-equity-quarterly.csv", index=False)
  2618. net_trade_equity_monthly.to_csv("ultrashort-rsi2-net-trade-equity-monthly.csv", index=False)
  2619. net_trade_equity_quarterly.to_csv("ultrashort-rsi2-net-trade-equity-quarterly.csv", index=False)
  2620. summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin)
  2621. first_candle = _format_ts(candles[0].ts)
  2622. last_candle = _format_ts(candles[-1].ts)
  2623. print(f"actual data range UTC: {first_candle} -> {last_candle}; bars={len(candles)}")
  2624. print(
  2625. summary[
  2626. [
  2627. "sample_count",
  2628. "avg_return",
  2629. "ci95_low",
  2630. "avg_trades_per_window",
  2631. "roundtrip_cost_on_margin",
  2632. "net_avg_return",
  2633. "net_ci95_low",
  2634. "positive_window_rate",
  2635. "trades",
  2636. "trade_win_rate",
  2637. "avg_win_return",
  2638. "avg_loss_return_abs",
  2639. "payoff_ratio",
  2640. "profit_factor",
  2641. "max_drawdown",
  2642. ]
  2643. ].to_string(index=False)
  2644. )
  2645. print("monthly")
  2646. print(monthly.to_string(index=False))
  2647. print("quarterly")
  2648. print(quarterly.to_string(index=False))
  2649. print("equity monthly")
  2650. print(equity_monthly.to_string(index=False))
  2651. print("equity quarterly")
  2652. print(equity_quarterly.to_string(index=False))
  2653. print("cost-adjusted closed-trade equity monthly")
  2654. print(net_trade_equity_monthly.to_string(index=False))
  2655. print("cost-adjusted closed-trade equity quarterly")
  2656. print(net_trade_equity_quarterly.to_string(index=False))
  2657. return 0
  2658. def strategy_timeframe_analysis(
  2659. *,
  2660. symbols: tuple[str, ...],
  2661. bars: tuple[str, ...],
  2662. years: float,
  2663. window_size: int,
  2664. roundtrip_cost_on_margin: float,
  2665. period: str,
  2666. ) -> int:
  2667. client = OkxClient()
  2668. candidates = build_strategy_timeframe_candidates()
  2669. summary_rows: list[dict[str, object]] = []
  2670. period_frames: list[pd.DataFrame] = []
  2671. availability_rows: list[dict[str, object]] = []
  2672. for symbol in symbols:
  2673. for bar in bars:
  2674. requested_bars = history_bars_for_years(bar, years)
  2675. candles = get_candles_cached(client, symbol, bar, requested_bars)
  2676. first_ts = candles[0].ts
  2677. last_ts = candles[-1].ts
  2678. availability_rows.append(
  2679. {
  2680. "symbol": symbol,
  2681. "bar": bar,
  2682. "requested_years": years,
  2683. "requested_bars": requested_bars,
  2684. "actual_bars": len(candles),
  2685. "first_candle": _format_ts(first_ts),
  2686. "last_candle": _format_ts(last_ts),
  2687. "actual_days": (last_ts - first_ts) / 86_400_000,
  2688. "complete_requested_range": len(candles) >= requested_bars,
  2689. }
  2690. )
  2691. for candidate in candidates:
  2692. rows = evaluate_candidate_window_rows(
  2693. candidate=candidate,
  2694. candles=candles,
  2695. window_size=window_size,
  2696. leverage=LEVERAGE,
  2697. )
  2698. summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
  2699. summary_rows.append(
  2700. {
  2701. "symbol": symbol,
  2702. "bar": bar,
  2703. "requested_bars": requested_bars,
  2704. "actual_bars": len(candles),
  2705. "first_candle": _format_ts(first_ts),
  2706. "last_candle": _format_ts(last_ts),
  2707. **summary,
  2708. }
  2709. )
  2710. window_frame = pd.DataFrame(rows).drop(columns=["trades"])
  2711. period_frame = summarize_periods(window_frame, period, roundtrip_cost_on_margin)
  2712. period_frame.insert(0, "name", candidate.name)
  2713. period_frame.insert(0, "bar", bar)
  2714. period_frame.insert(0, "symbol", symbol)
  2715. period_frames.append(period_frame)
  2716. print(f"done strategy timeframe {symbol} {bar} {candidate.name} windows={len(rows)}")
  2717. availability = pd.DataFrame(availability_rows)
  2718. summary_frame = sort_cost_results(pd.DataFrame(summary_rows))
  2719. period_summary = pd.concat(period_frames, ignore_index=True)
  2720. availability.to_csv("ultrashort-strategy-timeframe-availability.csv", index=False)
  2721. summary_frame.to_csv("ultrashort-strategy-timeframe-summary.csv", index=False)
  2722. period_summary.to_csv("ultrashort-strategy-timeframe-periods.csv", index=False)
  2723. print("availability")
  2724. print(availability.to_string(index=False))
  2725. print("summary")
  2726. print(
  2727. summary_frame[
  2728. [
  2729. "symbol",
  2730. "bar",
  2731. "name",
  2732. "actual_bars",
  2733. "sample_count",
  2734. "trades",
  2735. "net_avg_return",
  2736. "net_ci95_low",
  2737. "positive_window_rate",
  2738. "trade_win_rate",
  2739. "payoff_ratio",
  2740. "profit_factor",
  2741. "max_drawdown",
  2742. ]
  2743. ].head(50).to_string(index=False)
  2744. )
  2745. return 0
  2746. def rsi2_variant_search(
  2747. *,
  2748. symbol: str,
  2749. bar: str,
  2750. years: float,
  2751. window_size: int,
  2752. roundtrip_cost_on_margin: float,
  2753. ) -> int:
  2754. client = OkxClient()
  2755. requested_bars = history_bars_for_years(bar, years)
  2756. candles = get_candles_cached(client, symbol, bar, requested_bars)
  2757. candidates = [
  2758. build_rsi2_side_candidate(trend, long_threshold, short_threshold, exit_rsi, side_mode)
  2759. for trend in (50, 120, 240, 480)
  2760. for long_threshold, short_threshold in (
  2761. (2.0, 98.0),
  2762. (3.0, 97.0),
  2763. (5.0, 95.0),
  2764. )
  2765. for exit_rsi in (45.0, 55.0)
  2766. for side_mode in ("both", "long", "short")
  2767. ]
  2768. rows: list[dict[str, object]] = []
  2769. for candidate in candidates:
  2770. metrics = evaluate_candidate_all_windows(
  2771. candidate=candidate,
  2772. candles=candles,
  2773. window_size=window_size,
  2774. leverage=LEVERAGE,
  2775. )
  2776. rows.append(
  2777. {
  2778. "symbol": symbol,
  2779. "bar": bar,
  2780. "requested_bars": requested_bars,
  2781. "actual_bars": len(candles),
  2782. "first_candle": _format_ts(candles[0].ts),
  2783. "last_candle": _format_ts(candles[-1].ts),
  2784. **metrics,
  2785. }
  2786. )
  2787. print(f"done rsi2 variant {candidate.name}")
  2788. frame = sort_cost_results(add_cost_metrics(pd.DataFrame(rows), roundtrip_cost_on_margin))
  2789. frame.to_csv("ultrashort-rsi2-variant-search.csv", index=False)
  2790. print(
  2791. frame[
  2792. [
  2793. "symbol",
  2794. "bar",
  2795. "name",
  2796. "sample_count",
  2797. "trades",
  2798. "net_avg_return",
  2799. "net_ci95_low",
  2800. "positive_window_rate",
  2801. "trade_win_rate",
  2802. "avg_win_return",
  2803. "avg_loss_return_abs",
  2804. "payoff_ratio",
  2805. "profit_factor",
  2806. "max_drawdown",
  2807. ]
  2808. ].head(50).to_string(index=False)
  2809. )
  2810. return 0
  2811. def build_best_known_candidates() -> list[Candidate]:
  2812. bbmr_config = BBMRConfig(band_length=20, std_multiplier=2.5, bandwidth_lookback=50, stop_loss_pct=0.005)
  2813. return [
  2814. Candidate(
  2815. "bbmr-l20-m2.5-sl0.005",
  2816. 50,
  2817. lambda candles, leverage, warmup_bars, config=bbmr_config: run_bbmr_segment(
  2818. candles=candles,
  2819. leverage=leverage,
  2820. warmup_bars=warmup_bars,
  2821. config=config,
  2822. ),
  2823. ),
  2824. build_rsi2_side_candidate(50, 3.0, 97.0, 45.0, "long"),
  2825. build_rsi2_side_candidate(50, 3.0, 97.0, 55.0, "long"),
  2826. build_rsi2_side_candidate(240, 2.0, 98.0, 55.0, "long"),
  2827. build_trend_rsi_bb_long_candidate(480, 20, 2.0, 5.0, 45.0, 0.005),
  2828. build_trend_rsi_bb_long_candidate(240, 20, 2.5, 5.0, 55.0, 0.008),
  2829. build_regime_hybrid_candidate(240, 240, 0.015, 2.0, 55.0, 2.5, 0.008),
  2830. build_regime_hybrid_candidate(50, 240, 0.02, 3.0, 55.0, 2.5, 0.005),
  2831. ]
  2832. def best_total_annualized(
  2833. *,
  2834. symbols: tuple[str, ...],
  2835. bar: str,
  2836. years: float,
  2837. roundtrip_cost_on_margin: float,
  2838. ) -> int:
  2839. client = OkxClient()
  2840. rows: list[dict[str, object]] = []
  2841. horizon_rows: list[dict[str, object]] = []
  2842. horizons = (
  2843. ("3y", pd.DateOffset(years=3)),
  2844. ("1y", pd.DateOffset(years=1)),
  2845. ("6m", pd.DateOffset(months=6)),
  2846. ("3m", pd.DateOffset(months=3)),
  2847. )
  2848. for symbol in symbols:
  2849. candles = get_candles_cached(client, symbol, bar, history_bars_for_years(bar, years))
  2850. for candidate in build_best_known_candidates():
  2851. result = candidate.run(candles=candles, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
  2852. net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
  2853. metrics = annualized_metrics_from_equity(net_equity, candles[0].ts, candles[-1].ts)
  2854. gross_years = (candles[-1].ts - candles[0].ts) / 86_400_000 / 365
  2855. gross_annualized = (1.0 + result.total_return) ** (1.0 / gross_years) - 1.0 if result.total_return > -1.0 else 0.0
  2856. rows.append(
  2857. {
  2858. "symbol": symbol,
  2859. "bar": bar,
  2860. "name": candidate.name,
  2861. "first_candle": _format_ts(candles[0].ts),
  2862. "last_candle": _format_ts(candles[-1].ts),
  2863. "years": gross_years,
  2864. "trades": result.trade_count,
  2865. "gross_total_return": result.total_return,
  2866. "gross_annualized_return": gross_annualized,
  2867. "gross_max_drawdown_mark_to_market": result.max_drawdown,
  2868. **metrics,
  2869. }
  2870. )
  2871. horizon_frame = recent_horizon_metrics_from_equity(net_equity, candles[-1].ts, horizons)
  2872. for horizon_row in horizon_frame.to_dict("records"):
  2873. horizon_rows.append(
  2874. {
  2875. "symbol": symbol,
  2876. "bar": bar,
  2877. "name": candidate.name,
  2878. "first_candle": _format_ts(candles[0].ts),
  2879. "last_candle": _format_ts(candles[-1].ts),
  2880. "trades": result.trade_count,
  2881. **horizon_row,
  2882. }
  2883. )
  2884. frame = pd.DataFrame(rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
  2885. horizon_output = pd.DataFrame(horizon_rows)
  2886. horizon_output["horizon"] = pd.Categorical(horizon_output["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
  2887. horizon_output = horizon_output.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
  2888. frame.to_csv("ultrashort-best-total-annualized.csv", index=False)
  2889. horizon_output.to_csv("ultrashort-best-horizon-returns.csv", index=False)
  2890. print(frame.to_string(index=False))
  2891. print("recent horizon returns")
  2892. print(horizon_output.to_string(index=False))
  2893. return 0
  2894. def ma_cross_search(
  2895. *,
  2896. symbols: tuple[str, ...],
  2897. bar: str,
  2898. years: float,
  2899. window_size: int,
  2900. roundtrip_cost_on_margin: float,
  2901. ) -> int:
  2902. client = OkxClient()
  2903. rows: list[dict[str, object]] = []
  2904. for symbol in symbols:
  2905. candles = get_candles_cached(client, symbol, bar, history_bars_for_years(bar, years))
  2906. candidates = [
  2907. build_ma_cross_candidate(fast, slow, side_mode)
  2908. for fast, slow in ((12, 48), (20, 80), (30, 120), (50, 200), (80, 320))
  2909. for side_mode in ("both", "long", "short")
  2910. ]
  2911. for candidate in candidates:
  2912. metrics = evaluate_candidate_all_windows(
  2913. candidate=candidate,
  2914. candles=candles,
  2915. window_size=window_size,
  2916. leverage=LEVERAGE,
  2917. )
  2918. rows.append(
  2919. {
  2920. "symbol": symbol,
  2921. "bar": bar,
  2922. "actual_bars": len(candles),
  2923. "first_candle": _format_ts(candles[0].ts),
  2924. "last_candle": _format_ts(candles[-1].ts),
  2925. **metrics,
  2926. }
  2927. )
  2928. print(f"done ma cross {symbol} {candidate.name}")
  2929. frame = sort_cost_results(add_cost_metrics(pd.DataFrame(rows), roundtrip_cost_on_margin))
  2930. frame.to_csv("ultrashort-ma-cross-search.csv", index=False)
  2931. print(
  2932. frame[
  2933. [
  2934. "symbol",
  2935. "bar",
  2936. "name",
  2937. "sample_count",
  2938. "trades",
  2939. "net_avg_return",
  2940. "net_ci95_low",
  2941. "positive_window_rate",
  2942. "trade_win_rate",
  2943. "payoff_ratio",
  2944. "profit_factor",
  2945. "max_drawdown",
  2946. ]
  2947. ].head(40).to_string(index=False)
  2948. )
  2949. return 0
  2950. def trend_rsi_bb_search(
  2951. *,
  2952. symbols: tuple[str, ...],
  2953. bar: str,
  2954. years: float,
  2955. window_size: int,
  2956. roundtrip_cost_on_margin: float,
  2957. ) -> int:
  2958. client = OkxClient()
  2959. rows: list[dict[str, object]] = []
  2960. candidates = [
  2961. build_trend_rsi_bb_long_candidate(trend_sma, band_length, std_multiplier, rsi_threshold, exit_rsi, stop_loss_pct)
  2962. for trend_sma in (120, 240, 480)
  2963. for band_length in (20, 30)
  2964. for std_multiplier in (2.0, 2.5)
  2965. for rsi_threshold in (2.0, 3.0, 5.0)
  2966. for exit_rsi in (45.0, 55.0)
  2967. for stop_loss_pct in (0.005, 0.008)
  2968. ]
  2969. for symbol in symbols:
  2970. candles = get_candles_cached(client, symbol, bar, history_bars_for_years(bar, years))
  2971. for candidate in candidates:
  2972. metrics = evaluate_candidate_all_windows(
  2973. candidate=candidate,
  2974. candles=candles,
  2975. window_size=window_size,
  2976. leverage=LEVERAGE,
  2977. )
  2978. rows.append(
  2979. {
  2980. "symbol": symbol,
  2981. "bar": bar,
  2982. "actual_bars": len(candles),
  2983. "first_candle": _format_ts(candles[0].ts),
  2984. "last_candle": _format_ts(candles[-1].ts),
  2985. **metrics,
  2986. }
  2987. )
  2988. print(f"done trend rsi bb {symbol} {candidate.name}")
  2989. frame = sort_cost_results(add_cost_metrics(pd.DataFrame(rows), roundtrip_cost_on_margin))
  2990. frame.to_csv("ultrashort-trend-rsi-bb-search.csv", index=False)
  2991. print(
  2992. frame[
  2993. [
  2994. "symbol",
  2995. "bar",
  2996. "name",
  2997. "sample_count",
  2998. "trades",
  2999. "net_avg_return",
  3000. "net_ci95_low",
  3001. "positive_window_rate",
  3002. "trade_win_rate",
  3003. "avg_win_return",
  3004. "avg_loss_return_abs",
  3005. "payoff_ratio",
  3006. "profit_factor",
  3007. "max_drawdown",
  3008. ]
  3009. ].head(50).to_string(index=False)
  3010. )
  3011. return 0
  3012. def regime_analysis(
  3013. *,
  3014. symbols: tuple[str, ...],
  3015. bar: str,
  3016. years: float,
  3017. window_size: int,
  3018. roundtrip_cost_on_margin: float,
  3019. ) -> int:
  3020. client = OkxClient()
  3021. window_frames: list[pd.DataFrame] = []
  3022. for symbol in symbols:
  3023. candles = get_candles_cached(client, symbol, bar, history_bars_for_years(bar, years))
  3024. for candidate in build_best_known_candidates():
  3025. rows = evaluate_candidate_window_rows(
  3026. candidate=candidate,
  3027. candles=candles,
  3028. window_size=window_size,
  3029. leverage=LEVERAGE,
  3030. )
  3031. frame = add_market_regime_columns(candles, rows, roundtrip_cost_on_margin)
  3032. frame.insert(0, "name", candidate.name)
  3033. frame.insert(0, "last_candle", _format_ts(candles[-1].ts))
  3034. frame.insert(0, "first_candle", _format_ts(candles[0].ts))
  3035. frame.insert(0, "bar", bar)
  3036. frame.insert(0, "symbol", symbol)
  3037. window_frames.append(frame)
  3038. print(f"done regime {symbol} {candidate.name} windows={len(frame)}")
  3039. windows = pd.concat(window_frames, ignore_index=True)
  3040. summary = summarize_regime_columns(windows)
  3041. windows.to_csv("ultrashort-regime-windows.csv", index=False)
  3042. summary.to_csv("ultrashort-regime-summary.csv", index=False)
  3043. print(
  3044. summary[
  3045. [
  3046. "symbol",
  3047. "bar",
  3048. "name",
  3049. "regime_type",
  3050. "regime",
  3051. "sample_count",
  3052. "avg_net_return",
  3053. "median_net_return",
  3054. "positive_window_rate",
  3055. "avg_trades",
  3056. "avg_market_return",
  3057. "avg_realized_vol",
  3058. "avg_ma240_distance",
  3059. ]
  3060. ].to_string(index=False)
  3061. )
  3062. return 0
  3063. def eth_btc_signal_search(
  3064. *,
  3065. bar: str,
  3066. years: float,
  3067. window_size: int,
  3068. roundtrip_cost_on_margin: float,
  3069. ) -> int:
  3070. client = OkxClient()
  3071. requested_bars = history_bars_for_years(bar, years)
  3072. eth = get_candles_cached(client, "ETH-USDT-SWAP", bar, requested_bars)
  3073. btc = get_candles_cached(client, "BTC-USDT-SWAP", bar, requested_bars)
  3074. eth, btc = align_pair_candles(eth, btc)
  3075. candidates = [
  3076. build_eth_btc_rsi_filter_candidate(eth_trend, eth_rsi, eth_exit, btc_trend, btc_momentum, btc_min_momentum)
  3077. for eth_trend in (50, 120)
  3078. for eth_rsi in (2.0, 3.0, 5.0)
  3079. for eth_exit in (45.0, 55.0)
  3080. for btc_trend in (120, 240, 480)
  3081. for btc_momentum in (96, 240)
  3082. for btc_min_momentum in (0.0, 0.01)
  3083. ]
  3084. summary_rows: list[dict[str, object]] = []
  3085. total_rows: list[dict[str, object]] = []
  3086. horizon_rows: list[dict[str, object]] = []
  3087. horizons = (
  3088. ("3y", pd.DateOffset(years=3)),
  3089. ("1y", pd.DateOffset(years=1)),
  3090. ("6m", pd.DateOffset(months=6)),
  3091. ("3m", pd.DateOffset(months=3)),
  3092. )
  3093. for candidate in candidates:
  3094. rows = evaluate_pair_candidate_window_rows(
  3095. candidate=candidate,
  3096. eth_candles=eth,
  3097. btc_candles=btc,
  3098. window_size=window_size,
  3099. leverage=LEVERAGE,
  3100. )
  3101. summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
  3102. summary_rows.append(
  3103. {
  3104. "symbol": "ETH-USDT-SWAP",
  3105. "signal_symbol": "BTC-USDT-SWAP",
  3106. "bar": bar,
  3107. "actual_bars": len(eth),
  3108. "first_candle": _format_ts(eth[0].ts),
  3109. "last_candle": _format_ts(eth[-1].ts),
  3110. **summary,
  3111. }
  3112. )
  3113. result = candidate.run(eth_candles=eth, btc_candles=btc, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
  3114. net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
  3115. metrics = annualized_metrics_from_equity(net_equity, eth[0].ts, eth[-1].ts)
  3116. years_actual = (eth[-1].ts - eth[0].ts) / 86_400_000 / 365
  3117. gross_annualized = (1.0 + result.total_return) ** (1.0 / years_actual) - 1.0 if result.total_return > -1.0 else 0.0
  3118. total_rows.append(
  3119. {
  3120. "symbol": "ETH-USDT-SWAP",
  3121. "signal_symbol": "BTC-USDT-SWAP",
  3122. "bar": bar,
  3123. "name": candidate.name,
  3124. "first_candle": _format_ts(eth[0].ts),
  3125. "last_candle": _format_ts(eth[-1].ts),
  3126. "years": years_actual,
  3127. "trades": result.trade_count,
  3128. "gross_total_return": result.total_return,
  3129. "gross_annualized_return": gross_annualized,
  3130. "gross_max_drawdown_mark_to_market": result.max_drawdown,
  3131. **metrics,
  3132. }
  3133. )
  3134. horizon_frame = recent_horizon_metrics_from_equity(net_equity, eth[-1].ts, horizons)
  3135. for horizon_row in horizon_frame.to_dict("records"):
  3136. horizon_rows.append(
  3137. {
  3138. "symbol": "ETH-USDT-SWAP",
  3139. "signal_symbol": "BTC-USDT-SWAP",
  3140. "bar": bar,
  3141. "name": candidate.name,
  3142. "trades": result.trade_count,
  3143. **horizon_row,
  3144. }
  3145. )
  3146. print(f"done eth btc signal {candidate.name}")
  3147. summary = sort_cost_results(pd.DataFrame(summary_rows))
  3148. totals = pd.DataFrame(total_rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
  3149. horizon = pd.DataFrame(horizon_rows)
  3150. horizon["horizon"] = pd.Categorical(horizon["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
  3151. horizon = horizon.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
  3152. summary.to_csv("ultrashort-eth-btc-signal-summary.csv", index=False)
  3153. totals.to_csv("ultrashort-eth-btc-signal-total.csv", index=False)
  3154. horizon.to_csv("ultrashort-eth-btc-signal-horizon.csv", index=False)
  3155. print("window summary")
  3156. print(
  3157. summary[
  3158. [
  3159. "name",
  3160. "sample_count",
  3161. "trades",
  3162. "net_avg_return",
  3163. "net_ci95_low",
  3164. "positive_window_rate",
  3165. "trade_win_rate",
  3166. "payoff_ratio",
  3167. "profit_factor",
  3168. "max_drawdown",
  3169. ]
  3170. ].head(30).to_string(index=False)
  3171. )
  3172. print("total")
  3173. print(totals.head(30).to_string(index=False))
  3174. print("horizon")
  3175. print(horizon.head(60).to_string(index=False))
  3176. return 0
  3177. def eth_btc_shock_filter_search(
  3178. *,
  3179. bar: str,
  3180. years: float,
  3181. window_size: int,
  3182. roundtrip_cost_on_margin: float,
  3183. ) -> int:
  3184. client = OkxClient()
  3185. requested_bars = history_bars_for_years(bar, years)
  3186. eth = get_candles_cached(client, "ETH-USDT-SWAP", bar, requested_bars)
  3187. btc = get_candles_cached(client, "BTC-USDT-SWAP", bar, requested_bars)
  3188. eth, btc = align_pair_candles(eth, btc)
  3189. candidates = [
  3190. build_eth_btc_shock_filter_candidate(
  3191. eth_trend,
  3192. eth_rsi,
  3193. eth_exit,
  3194. btc_trend,
  3195. btc_momentum,
  3196. btc_min_momentum,
  3197. btc_shock_lookback,
  3198. btc_max_realized_vol,
  3199. btc_max_drawdown,
  3200. )
  3201. for eth_trend in (50,)
  3202. for eth_rsi in (3.0,)
  3203. for eth_exit in (45.0, 55.0)
  3204. for btc_trend in (480,)
  3205. for btc_momentum in (240,)
  3206. for btc_min_momentum in (0.0, 0.01)
  3207. for btc_shock_lookback in (96, 240)
  3208. for btc_max_realized_vol in (0.006, 0.01)
  3209. for btc_max_drawdown in (0.03, 0.05, 0.08)
  3210. ]
  3211. summary_rows: list[dict[str, object]] = []
  3212. total_rows: list[dict[str, object]] = []
  3213. horizon_rows: list[dict[str, object]] = []
  3214. horizons = (
  3215. ("3y", pd.DateOffset(years=3)),
  3216. ("1y", pd.DateOffset(years=1)),
  3217. ("6m", pd.DateOffset(months=6)),
  3218. ("3m", pd.DateOffset(months=3)),
  3219. )
  3220. for candidate in candidates:
  3221. rows = evaluate_pair_candidate_window_rows(
  3222. candidate=candidate,
  3223. eth_candles=eth,
  3224. btc_candles=btc,
  3225. window_size=window_size,
  3226. leverage=LEVERAGE,
  3227. )
  3228. summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
  3229. summary_rows.append(
  3230. {
  3231. "symbol": "ETH-USDT-SWAP",
  3232. "signal_symbol": "BTC-USDT-SWAP",
  3233. "bar": bar,
  3234. "actual_bars": len(eth),
  3235. "first_candle": _format_ts(eth[0].ts),
  3236. "last_candle": _format_ts(eth[-1].ts),
  3237. **summary,
  3238. }
  3239. )
  3240. result = candidate.run(eth_candles=eth, btc_candles=btc, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
  3241. net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
  3242. metrics = annualized_metrics_from_equity(net_equity, eth[0].ts, eth[-1].ts)
  3243. years_actual = (eth[-1].ts - eth[0].ts) / 86_400_000 / 365
  3244. gross_annualized = (1.0 + result.total_return) ** (1.0 / years_actual) - 1.0 if result.total_return > -1.0 else 0.0
  3245. total_rows.append(
  3246. {
  3247. "symbol": "ETH-USDT-SWAP",
  3248. "signal_symbol": "BTC-USDT-SWAP",
  3249. "bar": bar,
  3250. "name": candidate.name,
  3251. "first_candle": _format_ts(eth[0].ts),
  3252. "last_candle": _format_ts(eth[-1].ts),
  3253. "years": years_actual,
  3254. "trades": result.trade_count,
  3255. "gross_total_return": result.total_return,
  3256. "gross_annualized_return": gross_annualized,
  3257. "gross_max_drawdown_mark_to_market": result.max_drawdown,
  3258. **metrics,
  3259. }
  3260. )
  3261. horizon_frame = recent_horizon_metrics_from_equity(net_equity, eth[-1].ts, horizons)
  3262. for horizon_row in horizon_frame.to_dict("records"):
  3263. horizon_rows.append(
  3264. {
  3265. "symbol": "ETH-USDT-SWAP",
  3266. "signal_symbol": "BTC-USDT-SWAP",
  3267. "bar": bar,
  3268. "name": candidate.name,
  3269. "trades": result.trade_count,
  3270. **horizon_row,
  3271. }
  3272. )
  3273. print(f"done eth btc shock filter {candidate.name}")
  3274. summary = sort_cost_results(pd.DataFrame(summary_rows))
  3275. totals = pd.DataFrame(total_rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
  3276. horizon = pd.DataFrame(horizon_rows)
  3277. horizon["horizon"] = pd.Categorical(horizon["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
  3278. horizon = horizon.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
  3279. summary.to_csv("ultrashort-eth-btc-shock-filter-summary.csv", index=False)
  3280. totals.to_csv("ultrashort-eth-btc-shock-filter-total.csv", index=False)
  3281. horizon.to_csv("ultrashort-eth-btc-shock-filter-horizon.csv", index=False)
  3282. print("window summary")
  3283. print(
  3284. summary[
  3285. [
  3286. "name",
  3287. "sample_count",
  3288. "trades",
  3289. "net_avg_return",
  3290. "net_ci95_low",
  3291. "positive_window_rate",
  3292. "trade_win_rate",
  3293. "payoff_ratio",
  3294. "profit_factor",
  3295. "max_drawdown",
  3296. ]
  3297. ].head(30).to_string(index=False)
  3298. )
  3299. print("total")
  3300. print(totals.head(30).to_string(index=False))
  3301. print("horizon")
  3302. print(horizon.head(60).to_string(index=False))
  3303. return 0
  3304. def eth_btc_ratio_search(
  3305. *,
  3306. bar: str,
  3307. years: float,
  3308. window_size: int,
  3309. roundtrip_cost_on_margin: float,
  3310. ) -> int:
  3311. client = OkxClient()
  3312. requested_bars = history_bars_for_years(bar, years)
  3313. eth = get_candles_cached(client, "ETH-USDT-SWAP", bar, requested_bars)
  3314. btc = get_candles_cached(client, "BTC-USDT-SWAP", bar, requested_bars)
  3315. eth, btc = align_pair_candles(eth, btc)
  3316. candidates = [
  3317. build_eth_btc_ratio_pullback_candidate(btc_trend, btc_momentum, btc_min_momentum, ratio_length, ratio_std, ratio_rsi, stop)
  3318. for btc_trend in (480,)
  3319. for btc_momentum in (96, 240)
  3320. for btc_min_momentum in (0.0, 0.01)
  3321. for ratio_length in (48, 96)
  3322. for ratio_std in (1.5, 2.0)
  3323. for ratio_rsi in (5.0,)
  3324. for stop in (0.005, 0.008)
  3325. ]
  3326. summary_rows: list[dict[str, object]] = []
  3327. total_rows: list[dict[str, object]] = []
  3328. horizon_rows: list[dict[str, object]] = []
  3329. horizons = (
  3330. ("3y", pd.DateOffset(years=3)),
  3331. ("1y", pd.DateOffset(years=1)),
  3332. ("6m", pd.DateOffset(months=6)),
  3333. ("3m", pd.DateOffset(months=3)),
  3334. )
  3335. for candidate in candidates:
  3336. rows = evaluate_pair_candidate_window_rows(
  3337. candidate=candidate,
  3338. eth_candles=eth,
  3339. btc_candles=btc,
  3340. window_size=window_size,
  3341. leverage=LEVERAGE,
  3342. )
  3343. summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
  3344. summary_rows.append(
  3345. {
  3346. "symbol": "ETH-USDT-SWAP",
  3347. "signal_symbol": "BTC-USDT-SWAP",
  3348. "bar": bar,
  3349. "actual_bars": len(eth),
  3350. "first_candle": _format_ts(eth[0].ts),
  3351. "last_candle": _format_ts(eth[-1].ts),
  3352. **summary,
  3353. }
  3354. )
  3355. result = candidate.run(eth_candles=eth, btc_candles=btc, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
  3356. net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
  3357. metrics = annualized_metrics_from_equity(net_equity, eth[0].ts, eth[-1].ts)
  3358. years_actual = (eth[-1].ts - eth[0].ts) / 86_400_000 / 365
  3359. gross_annualized = (1.0 + result.total_return) ** (1.0 / years_actual) - 1.0 if result.total_return > -1.0 else 0.0
  3360. total_rows.append(
  3361. {
  3362. "symbol": "ETH-USDT-SWAP",
  3363. "signal_symbol": "BTC-USDT-SWAP",
  3364. "bar": bar,
  3365. "name": candidate.name,
  3366. "first_candle": _format_ts(eth[0].ts),
  3367. "last_candle": _format_ts(eth[-1].ts),
  3368. "years": years_actual,
  3369. "trades": result.trade_count,
  3370. "gross_total_return": result.total_return,
  3371. "gross_annualized_return": gross_annualized,
  3372. "gross_max_drawdown_mark_to_market": result.max_drawdown,
  3373. **metrics,
  3374. }
  3375. )
  3376. horizon_frame = recent_horizon_metrics_from_equity(net_equity, eth[-1].ts, horizons)
  3377. for horizon_row in horizon_frame.to_dict("records"):
  3378. horizon_rows.append(
  3379. {
  3380. "symbol": "ETH-USDT-SWAP",
  3381. "signal_symbol": "BTC-USDT-SWAP",
  3382. "bar": bar,
  3383. "name": candidate.name,
  3384. "trades": result.trade_count,
  3385. **horizon_row,
  3386. }
  3387. )
  3388. print(f"done eth btc ratio {candidate.name}")
  3389. summary = sort_cost_results(pd.DataFrame(summary_rows))
  3390. totals = pd.DataFrame(total_rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
  3391. horizon = pd.DataFrame(horizon_rows)
  3392. horizon["horizon"] = pd.Categorical(horizon["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
  3393. horizon = horizon.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
  3394. summary.to_csv("ultrashort-eth-btc-ratio-summary.csv", index=False)
  3395. totals.to_csv("ultrashort-eth-btc-ratio-total.csv", index=False)
  3396. horizon.to_csv("ultrashort-eth-btc-ratio-horizon.csv", index=False)
  3397. print("window summary")
  3398. print(
  3399. summary[
  3400. [
  3401. "name",
  3402. "sample_count",
  3403. "trades",
  3404. "net_avg_return",
  3405. "net_ci95_low",
  3406. "positive_window_rate",
  3407. "trade_win_rate",
  3408. "payoff_ratio",
  3409. "profit_factor",
  3410. "max_drawdown",
  3411. ]
  3412. ].head(30).to_string(index=False)
  3413. )
  3414. print("total")
  3415. print(totals.head(30).to_string(index=False))
  3416. print("horizon")
  3417. print(horizon.head(60).to_string(index=False))
  3418. return 0
  3419. def btc_lead_eth_lag_search(
  3420. *,
  3421. bar: str,
  3422. years: float,
  3423. window_size: int,
  3424. roundtrip_cost_on_margin: float,
  3425. ) -> int:
  3426. client = OkxClient()
  3427. requested_bars = history_bars_for_years(bar, years)
  3428. eth = get_candles_cached(client, "ETH-USDT-SWAP", bar, requested_bars)
  3429. btc = get_candles_cached(client, "BTC-USDT-SWAP", bar, requested_bars)
  3430. eth, btc = align_pair_candles(eth, btc)
  3431. candidates = [
  3432. build_btc_lead_eth_lag_candidate(lead_lookback, btc_return_threshold, lag_gap, max_hold_bars, stop_loss_pct, take_profit_pct)
  3433. for lead_lookback in (8, 16)
  3434. for btc_return_threshold in (0.012, 0.018, 0.024)
  3435. for lag_gap in (0.006, 0.012)
  3436. for max_hold_bars in (8, 32)
  3437. for stop_loss_pct in (0.006, 0.008)
  3438. for take_profit_pct in (0.012, 0.018)
  3439. ]
  3440. summary_rows: list[dict[str, object]] = []
  3441. total_rows: list[dict[str, object]] = []
  3442. horizon_rows: list[dict[str, object]] = []
  3443. horizons = (
  3444. ("3y", pd.DateOffset(years=3)),
  3445. ("1y", pd.DateOffset(years=1)),
  3446. ("6m", pd.DateOffset(months=6)),
  3447. ("3m", pd.DateOffset(months=3)),
  3448. )
  3449. for candidate in candidates:
  3450. rows = evaluate_pair_candidate_window_rows(
  3451. candidate=candidate,
  3452. eth_candles=eth,
  3453. btc_candles=btc,
  3454. window_size=window_size,
  3455. leverage=LEVERAGE,
  3456. )
  3457. summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
  3458. summary_rows.append(
  3459. {
  3460. "symbol": "ETH-USDT-SWAP",
  3461. "signal_symbol": "BTC-USDT-SWAP",
  3462. "bar": bar,
  3463. "actual_bars": len(eth),
  3464. "first_candle": _format_ts(eth[0].ts),
  3465. "last_candle": _format_ts(eth[-1].ts),
  3466. **summary,
  3467. }
  3468. )
  3469. result = candidate.run(eth_candles=eth, btc_candles=btc, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
  3470. net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
  3471. metrics = annualized_metrics_from_equity(net_equity, eth[0].ts, eth[-1].ts)
  3472. years_actual = (eth[-1].ts - eth[0].ts) / 86_400_000 / 365
  3473. gross_annualized = (1.0 + result.total_return) ** (1.0 / years_actual) - 1.0 if result.total_return > -1.0 else 0.0
  3474. total_rows.append(
  3475. {
  3476. "symbol": "ETH-USDT-SWAP",
  3477. "signal_symbol": "BTC-USDT-SWAP",
  3478. "bar": bar,
  3479. "name": candidate.name,
  3480. "first_candle": _format_ts(eth[0].ts),
  3481. "last_candle": _format_ts(eth[-1].ts),
  3482. "years": years_actual,
  3483. "trades": result.trade_count,
  3484. "gross_total_return": result.total_return,
  3485. "gross_annualized_return": gross_annualized,
  3486. "gross_max_drawdown_mark_to_market": result.max_drawdown,
  3487. **metrics,
  3488. }
  3489. )
  3490. horizon_frame = recent_horizon_metrics_from_equity(net_equity, eth[-1].ts, horizons)
  3491. for horizon_row in horizon_frame.to_dict("records"):
  3492. horizon_rows.append(
  3493. {
  3494. "symbol": "ETH-USDT-SWAP",
  3495. "signal_symbol": "BTC-USDT-SWAP",
  3496. "bar": bar,
  3497. "name": candidate.name,
  3498. "trades": result.trade_count,
  3499. **horizon_row,
  3500. }
  3501. )
  3502. print(f"done btc lead eth lag {candidate.name}")
  3503. summary = sort_cost_results(pd.DataFrame(summary_rows))
  3504. totals = pd.DataFrame(total_rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
  3505. horizon = pd.DataFrame(horizon_rows)
  3506. horizon["horizon"] = pd.Categorical(horizon["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
  3507. horizon = horizon.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
  3508. output_prefix = f"ultrashort-btc-lead-eth-lag-{bar}"
  3509. summary.to_csv(f"{output_prefix}-summary.csv", index=False)
  3510. totals.to_csv(f"{output_prefix}-total.csv", index=False)
  3511. horizon.to_csv(f"{output_prefix}-horizon.csv", index=False)
  3512. print("window summary")
  3513. print(
  3514. summary[
  3515. [
  3516. "name",
  3517. "sample_count",
  3518. "trades",
  3519. "net_avg_return",
  3520. "net_ci95_low",
  3521. "positive_window_rate",
  3522. "trade_win_rate",
  3523. "payoff_ratio",
  3524. "profit_factor",
  3525. "max_drawdown",
  3526. ]
  3527. ].head(30).to_string(index=False)
  3528. )
  3529. print("total")
  3530. print(totals.head(30).to_string(index=False))
  3531. print("horizon")
  3532. print(horizon.head(60).to_string(index=False))
  3533. return 0
  3534. if __name__ == "__main__":
  3535. parser = argparse.ArgumentParser()
  3536. parser.add_argument("--focus-vwap", action="store_true")
  3537. parser.add_argument("--robust-vwap", action="store_true")
  3538. parser.add_argument("--robust-all", action="store_true")
  3539. parser.add_argument("--rsi2-cost-search", action="store_true")
  3540. parser.add_argument("--rsi2-period-analysis", action="store_true")
  3541. parser.add_argument("--strategy-timeframe-analysis", action="store_true")
  3542. parser.add_argument("--rsi2-variant-search", action="store_true")
  3543. parser.add_argument("--best-total-annualized", action="store_true")
  3544. parser.add_argument("--ma-cross-search", action="store_true")
  3545. parser.add_argument("--trend-rsi-bb-search", action="store_true")
  3546. parser.add_argument("--regime-analysis", action="store_true")
  3547. parser.add_argument("--eth-btc-signal-search", action="store_true")
  3548. parser.add_argument("--eth-btc-shock-filter-search", action="store_true")
  3549. parser.add_argument("--eth-btc-ratio-search", action="store_true")
  3550. parser.add_argument("--btc-lead-eth-lag-search", action="store_true")
  3551. parser.add_argument("--history-limit", type=int, default=ROBUST_HISTORY_LIMIT)
  3552. parser.add_argument("--window-size", type=int, default=WINDOW_SIZE)
  3553. parser.add_argument("--roundtrip-cost-on-margin", type=float, default=0.0012)
  3554. parser.add_argument("--symbol", default="BTC-USDT-SWAP")
  3555. parser.add_argument("--symbols", default="BTC-USDT-SWAP")
  3556. parser.add_argument("--bar", default="15m")
  3557. parser.add_argument("--bars", default=",".join(ANALYSIS_BARS))
  3558. parser.add_argument("--years", type=float, default=10.0)
  3559. parser.add_argument("--period", default="Q")
  3560. parser.add_argument("--rsi2-trend", type=int, default=50)
  3561. parser.add_argument("--rsi2-long-threshold", type=float, default=3.0)
  3562. parser.add_argument("--rsi2-short-threshold", type=float, default=97.0)
  3563. args = parser.parse_args()
  3564. if args.rsi2_period_analysis:
  3565. raise SystemExit(
  3566. rsi2_period_analysis(
  3567. symbol=args.symbol,
  3568. bar=args.bar,
  3569. history_limit=args.history_limit,
  3570. window_size=args.window_size,
  3571. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3572. trend=args.rsi2_trend,
  3573. long_threshold=args.rsi2_long_threshold,
  3574. short_threshold=args.rsi2_short_threshold,
  3575. )
  3576. )
  3577. if args.rsi2_cost_search:
  3578. raise SystemExit(robust_rsi2_cost_search(args.history_limit, args.window_size, args.roundtrip_cost_on_margin))
  3579. if args.best_total_annualized:
  3580. raise SystemExit(
  3581. best_total_annualized(
  3582. symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
  3583. bar=args.bar,
  3584. years=args.years,
  3585. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3586. )
  3587. )
  3588. if args.ma_cross_search:
  3589. raise SystemExit(
  3590. ma_cross_search(
  3591. symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
  3592. bar=args.bar,
  3593. years=args.years,
  3594. window_size=args.window_size,
  3595. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3596. )
  3597. )
  3598. if args.trend_rsi_bb_search:
  3599. raise SystemExit(
  3600. trend_rsi_bb_search(
  3601. symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
  3602. bar=args.bar,
  3603. years=args.years,
  3604. window_size=args.window_size,
  3605. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3606. )
  3607. )
  3608. if args.regime_analysis:
  3609. raise SystemExit(
  3610. regime_analysis(
  3611. symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
  3612. bar=args.bar,
  3613. years=args.years,
  3614. window_size=args.window_size,
  3615. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3616. )
  3617. )
  3618. if args.eth_btc_signal_search:
  3619. raise SystemExit(
  3620. eth_btc_signal_search(
  3621. bar=args.bar,
  3622. years=args.years,
  3623. window_size=args.window_size,
  3624. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3625. )
  3626. )
  3627. if args.eth_btc_shock_filter_search:
  3628. raise SystemExit(
  3629. eth_btc_shock_filter_search(
  3630. bar=args.bar,
  3631. years=args.years,
  3632. window_size=args.window_size,
  3633. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3634. )
  3635. )
  3636. if args.eth_btc_ratio_search:
  3637. raise SystemExit(
  3638. eth_btc_ratio_search(
  3639. bar=args.bar,
  3640. years=args.years,
  3641. window_size=args.window_size,
  3642. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3643. )
  3644. )
  3645. if args.btc_lead_eth_lag_search:
  3646. raise SystemExit(
  3647. btc_lead_eth_lag_search(
  3648. bar=args.bar,
  3649. years=args.years,
  3650. window_size=args.window_size,
  3651. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3652. )
  3653. )
  3654. if args.rsi2_variant_search:
  3655. raise SystemExit(
  3656. rsi2_variant_search(
  3657. symbol=args.symbol,
  3658. bar=args.bar,
  3659. years=args.years,
  3660. window_size=args.window_size,
  3661. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3662. )
  3663. )
  3664. if args.strategy_timeframe_analysis:
  3665. raise SystemExit(
  3666. strategy_timeframe_analysis(
  3667. symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
  3668. bars=tuple(value.strip() for value in args.bars.split(",") if value.strip()),
  3669. years=args.years,
  3670. window_size=args.window_size,
  3671. roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
  3672. period=args.period,
  3673. )
  3674. )
  3675. if args.robust_all:
  3676. raise SystemExit(robust_all(args.history_limit, args.window_size))
  3677. if args.robust_vwap:
  3678. raise SystemExit(robust_vwap(args.history_limit, args.window_size))
  3679. raise SystemExit(focus_vwap() if args.focus_vwap else main())