explore_ultrashort.py 152 KB

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