explore_ultrashort.py 139 KB

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