explore_ultrashort.py 138 KB

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