| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931 |
- from __future__ import annotations
- import argparse
- import json
- from dataclasses import dataclass
- from math import sqrt
- from pathlib import Path
- import pandas as pd
- from okx_codex_trader.bbmr_report import BBMRConfig, run_bbmr_segment
- from okx_codex_trader.bbsb_report import BBSBConfig, run_bbsb_segment
- from okx_codex_trader.donchian_report import DonchianConfig, run_donchian_segment
- from okx_codex_trader.ema_pullback_report import EMAPullbackConfig, run_ema_pullback_segment
- from okx_codex_trader.models import Candle
- from okx_codex_trader.okx_client import OkxClient
- from okx_codex_trader.rsi2_report import RSI2Config, _compute_rsi, run_rsi2_segment
- from okx_codex_trader.sampled_report import SegmentResult, mark_to_market, sample_segments, trade_equity
- SYMBOLS = ("BTC-USDT-SWAP", "ETH-USDT-SWAP")
- BARS = ("1m", "3m", "5m")
- ANALYSIS_BARS = ("3m", "5m", "15m")
- HISTORY_LIMIT = 4000
- SEGMENTS = 8
- WINDOW_SIZE = 240
- LEVERAGE = 3
- INITIAL_EQUITY = 10_000.0
- ROBUST_HISTORY_LIMIT = 50_000
- GROSS_RETURN_NOTE = "Gross-return backtest only: fees, slippage, and funding rates are excluded."
- MINUTES_PER_YEAR = 365 * 24 * 60
- CANDLE_CACHE_DIR = Path("data/okx-candles")
- CANDLE_BAR_MS = {
- "1m": 60_000,
- "3m": 180_000,
- "5m": 300_000,
- "15m": 900_000,
- "30m": 1_800_000,
- "1H": 3_600_000,
- "4H": 14_400_000,
- "1D": 86_400_000,
- }
- @dataclass(frozen=True)
- class Candidate:
- name: str
- warmup_bars: int
- run: object
- @dataclass(frozen=True)
- class PairCandidate:
- name: str
- warmup_bars: int
- run: object
- def _format_ts(ts: int) -> str:
- return pd.to_datetime(ts, unit="ms", utc=True).strftime("%Y-%m-%d %H:%M")
- def candle_cache_file(cache_dir: Path, symbol: str, bar: str) -> Path:
- return cache_dir / symbol / f"{bar}.csv"
- def candle_cache_meta_file(cache_dir: Path, symbol: str, bar: str) -> Path:
- return cache_dir / symbol / f"{bar}.meta.json"
- def load_cached_candles(cache_dir: Path, symbol: str, bar: str) -> tuple[list[Candle], bool]:
- cache_file = candle_cache_file(cache_dir, symbol, bar)
- if not cache_file.exists():
- return [], False
- frame = pd.read_csv(cache_file)
- candles = [
- Candle(
- symbol=symbol,
- ts=int(row.ts),
- open=float(row.open),
- high=float(row.high),
- low=float(row.low),
- close=float(row.close),
- volume=float(row.volume),
- )
- for row in frame.itertuples(index=False)
- ]
- meta_file = candle_cache_meta_file(cache_dir, symbol, bar)
- history_exhausted = False
- if meta_file.exists():
- with meta_file.open("r", encoding="utf-8") as handle:
- history_exhausted = bool(json.load(handle).get("history_exhausted"))
- return candles, history_exhausted
- def save_cached_candles(cache_dir: Path, symbol: str, bar: str, candles: list[Candle], history_exhausted: bool) -> None:
- cache_file = candle_cache_file(cache_dir, symbol, bar)
- cache_file.parent.mkdir(parents=True, exist_ok=True)
- frame = pd.DataFrame(
- [
- {
- "ts": candle.ts,
- "open": candle.open,
- "high": candle.high,
- "low": candle.low,
- "close": candle.close,
- "volume": candle.volume,
- }
- for candle in sorted(candles, key=lambda candle: candle.ts)
- ]
- ).drop_duplicates("ts", keep="last")
- frame.to_csv(cache_file, index=False)
- meta = {
- "symbol": symbol,
- "bar": bar,
- "history_exhausted": history_exhausted,
- "rows": len(frame),
- "first_ts": int(frame["ts"].iloc[0]) if len(frame) else None,
- "last_ts": int(frame["ts"].iloc[-1]) if len(frame) else None,
- }
- with candle_cache_meta_file(cache_dir, symbol, bar).open("w", encoding="utf-8") as handle:
- json.dump(meta, handle, separators=(",", ":"))
- def latest_bridge_count(cached: list[Candle], latest_last_ts: int, interval: int) -> int:
- bridge_from_ts = max(candle.ts for candle in cached)
- for left, right in zip(cached, cached[1:]):
- if right.ts - left.ts != interval:
- bridge_from_ts = left.ts
- return ((latest_last_ts - bridge_from_ts) // interval) + 1
- def get_candles_cached(
- client: OkxClient,
- symbol: str,
- bar: str,
- limit: int,
- cache_dir: Path = CANDLE_CACHE_DIR,
- ) -> list[Candle]:
- cached, history_exhausted = load_cached_candles(cache_dir, symbol, bar)
- if cached and (len(cached) >= limit or history_exhausted):
- latest = client.get_candles(symbol, bar, min(300, limit))
- interval = CANDLE_BAR_MS[bar]
- if latest:
- latest_last_ts = max(candle.ts for candle in latest)
- needed_latest_count = latest_bridge_count(cached, latest_last_ts, interval)
- if needed_latest_count > len(latest):
- latest = client.get_candles(symbol, bar, needed_latest_count)
- merged = {candle.ts: candle for candle in cached}
- for candle in latest:
- merged[candle.ts] = candle
- candles = sorted(merged.values(), key=lambda candle: candle.ts)
- save_cached_candles(cache_dir, symbol, bar, candles, history_exhausted)
- return candles[-limit:] if len(candles) >= limit else candles
- fetched = client.get_candles(symbol, bar, limit)
- history_exhausted = len(fetched) < limit
- save_cached_candles(cache_dir, symbol, bar, fetched, history_exhausted)
- return fetched
- def align_pair_candles(left: list[Candle], right: list[Candle]) -> tuple[list[Candle], list[Candle]]:
- right_by_ts = {candle.ts: candle for candle in right}
- left_aligned: list[Candle] = []
- right_aligned: list[Candle] = []
- for candle in left:
- other = right_by_ts.get(candle.ts)
- if other is None:
- continue
- left_aligned.append(candle)
- right_aligned.append(other)
- return left_aligned, right_aligned
- def _trade(
- *,
- trades: list[dict[str, object]],
- exits: list[dict[str, object]],
- position: dict[str, object],
- candle: Candle,
- exit_price: float,
- leverage: int,
- ) -> tuple[float, bool]:
- exit_equity = trade_equity(
- side=str(position["side"]),
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- exit_price=exit_price,
- leverage=leverage,
- )
- trades.append(
- {
- "side": "Long" if position["side"] == "long" else "Short",
- "entry_time": _format_ts(int(position["entry_time"])),
- "exit_time": _format_ts(candle.ts),
- "entry_price": round(float(position["entry_price"]), 4),
- "exit_price": round(exit_price, 4),
- "pnl": round(exit_equity - float(position["margin_used"]), 4),
- "return_pct": round((exit_equity - float(position["margin_used"])) / float(position["margin_used"]) * 100, 4),
- }
- )
- exits.append({"ts": candle.ts, "price": exit_price, "side": position["side"]})
- return exit_equity, exit_equity > float(position["margin_used"])
- def _close_partial_trade(
- *,
- trades: list[dict[str, object]],
- exits: list[dict[str, object]],
- position: dict[str, object],
- account_equity: float,
- candle: Candle,
- exit_price: float,
- leverage: int,
- ) -> tuple[float, bool]:
- margin_used = float(position["margin_used"])
- exit_equity = trade_equity(
- side=str(position["side"]),
- margin_used=margin_used,
- entry_price=float(position["entry_price"]),
- exit_price=exit_price,
- leverage=leverage,
- )
- pnl = exit_equity - margin_used
- trades.append(
- {
- "side": "Long" if position["side"] == "long" else "Short",
- "entry_time": _format_ts(int(position["entry_time"])),
- "exit_time": _format_ts(candle.ts),
- "entry_price": round(float(position["entry_price"]), 4),
- "exit_price": round(exit_price, 4),
- "pnl": round(pnl, 4),
- "return_pct": round(pnl / account_equity * 100, 4),
- "cost_weight": round(margin_used / account_equity, 8),
- }
- )
- exits.append({"ts": candle.ts, "price": exit_price, "side": position["side"]})
- return account_equity + pnl, pnl > 0.0
- def run_range_momentum_segment(
- *,
- candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- lookback: int,
- take_profit_pct: float,
- stop_loss_pct: float,
- ) -> SegmentResult:
- highs = pd.Series([candle.high for candle in candles], dtype=float)
- lows = pd.Series([candle.low for candle in candles], dtype=float)
- entry_high = highs.shift(1).rolling(lookback).max().tolist()
- entry_low = lows.shift(1).rolling(lookback).min().tolist()
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry_side: str | None = None
- for index in range(warmup_bars, len(candles)):
- candle = candles[index]
- if pending_entry_side is not None and position is None and equity > 0.0:
- position = {
- "side": pending_entry_side,
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "entry_index": index,
- "margin_used": equity,
- "stop_price": candle.open * (1 - stop_loss_pct if pending_entry_side == "long" else 1 + stop_loss_pct),
- "take_profit_price": candle.open * (1 + take_profit_pct if pending_entry_side == "long" else 1 - take_profit_pct),
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": pending_entry_side})
- pending_entry_side = None
- current_equity = equity
- if position is not None:
- stop_hit = (
- position["side"] == "long" and candle.low <= float(position["stop_price"])
- ) or (
- position["side"] == "short" and candle.high >= float(position["stop_price"])
- )
- take_hit = (
- position["side"] == "long" and candle.high >= float(position["take_profit_price"])
- ) or (
- position["side"] == "short" and candle.low <= float(position["take_profit_price"])
- )
- if stop_hit or take_hit:
- exit_price = float(position["stop_price"] if stop_hit else position["take_profit_price"])
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=exit_price,
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- if position is not None:
- current_equity = mark_to_market(
- side=str(position["side"]),
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(candles) - 1 or position is not None or equity <= 0.0:
- continue
- if entry_high[index] == entry_high[index] and candle.close > float(entry_high[index]):
- pending_entry_side = "long"
- elif entry_low[index] == entry_low[index] and candle.close < float(entry_low[index]):
- pending_entry_side = "short"
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=wins / trade_count if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_vwap_reversion_segment(
- *,
- candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- window: int,
- entry_z: float,
- exit_z: float,
- stop_loss_pct: float,
- ) -> SegmentResult:
- closes = pd.Series([candle.close for candle in candles], dtype=float)
- volumes = pd.Series([candle.volume for candle in candles], dtype=float)
- vwap = (closes * volumes).rolling(window).sum() / volumes.rolling(window).sum()
- deviation = ((closes - vwap) / vwap).tolist()
- stdev = pd.Series(deviation, dtype=float).rolling(window).std(ddof=0).tolist()
- zscore = [
- float("nan") if dev != dev or std != std or std == 0.0 else dev / std
- for dev, std in zip(deviation, stdev)
- ]
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry_side: str | None = None
- pending_exit = False
- for index in range(warmup_bars, len(candles)):
- candle = candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry_side is not None and position is None and equity > 0.0:
- position = {
- "side": pending_entry_side,
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "margin_used": equity,
- "stop_price": candle.open * (1 - stop_loss_pct if pending_entry_side == "long" else 1 + stop_loss_pct),
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": pending_entry_side})
- pending_entry_side = None
- current_equity = equity
- if position is not None:
- stop_hit = (
- position["side"] == "long" and candle.low <= float(position["stop_price"])
- ) or (
- position["side"] == "short" and candle.high >= float(position["stop_price"])
- )
- if stop_hit:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=float(position["stop_price"]),
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- if position is not None:
- current_equity = mark_to_market(
- side=str(position["side"]),
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(candles) - 1 or equity <= 0.0:
- continue
- current_z = zscore[index]
- if current_z != current_z:
- continue
- if position is not None:
- if (position["side"] == "long" and current_z >= -exit_z) or (
- position["side"] == "short" and current_z <= exit_z
- ):
- pending_exit = True
- continue
- if current_z <= -entry_z:
- pending_entry_side = "long"
- elif current_z >= entry_z:
- pending_entry_side = "short"
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=wins / trade_count if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_rsi2_side_segment(
- *,
- candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- config: RSI2Config,
- side_mode: str,
- ) -> SegmentResult:
- result = run_rsi2_segment(
- candles=candles,
- leverage=leverage,
- warmup_bars=warmup_bars,
- config=config,
- )
- if side_mode == "both":
- return result
- closes = pd.Series([candle.close for candle in candles], dtype=float)
- trend = closes.rolling(config.trend_sma).mean().tolist()
- rsi_values = _compute_rsi(closes, config.rsi_length)
- equity = config.initial_equity
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry_side: str | None = None
- pending_exit = False
- allowed_side = "long" if side_mode == "long" else "short"
- for index in range(warmup_bars, len(candles)):
- candle = candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry_side is not None and position is None and equity > 0.0:
- position = {
- "side": pending_entry_side,
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "margin_used": equity,
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": pending_entry_side})
- pending_entry_side = None
- current_equity = equity
- if position is not None:
- current_equity = mark_to_market(
- side=str(position["side"]),
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(candles) - 1 or equity <= 0.0:
- continue
- current_rsi = rsi_values[index]
- current_trend = trend[index]
- if current_rsi != current_rsi or current_trend != current_trend:
- continue
- if position is not None:
- if (position["side"] == "long" and current_rsi >= config.exit_rsi) or (
- position["side"] == "short" and current_rsi <= config.exit_rsi
- ):
- pending_exit = True
- continue
- if allowed_side == "long" and candle.close > float(current_trend) and current_rsi <= config.rsi_long_threshold:
- pending_entry_side = "long"
- elif allowed_side == "short" and candle.close < float(current_trend) and current_rsi >= config.rsi_short_threshold:
- pending_entry_side = "short"
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - config.initial_equity) / config.initial_equity,
- win_rate=(wins / trade_count) if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_rsi2_long_guarded_segment(
- *,
- candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- trend_sma: int,
- rsi_threshold: float,
- exit_rsi: float,
- stop_loss_pct: float,
- max_hold_bars: int,
- ) -> SegmentResult:
- closes = pd.Series([candle.close for candle in candles], dtype=float)
- trend = closes.rolling(trend_sma).mean().tolist()
- rsi_values = _compute_rsi(closes, 2)
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry = False
- pending_exit = False
- for index in range(warmup_bars, len(candles)):
- candle = candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry and position is None and equity > 0.0:
- position = {
- "side": "long",
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "entry_index": index,
- "margin_used": equity,
- "stop_price": candle.open * (1 - stop_loss_pct),
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
- pending_entry = False
- current_equity = equity
- if position is not None and candle.low <= float(position["stop_price"]):
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=float(position["stop_price"]),
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- if position is not None:
- current_equity = mark_to_market(
- side="long",
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(candles) - 1 or equity <= 0.0:
- continue
- current_rsi = rsi_values[index]
- current_trend = trend[index]
- if current_rsi != current_rsi or current_trend != current_trend:
- continue
- if position is not None:
- held_bars = index - int(position["entry_index"])
- if current_rsi >= exit_rsi or held_bars >= max_hold_bars:
- pending_exit = True
- continue
- if candle.close > float(current_trend) and current_rsi <= rsi_threshold:
- pending_entry = True
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=(wins / trade_count) if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_rsi2_long_guarded_twap_segment(
- *,
- candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- trend_sma: int,
- rsi_threshold: float,
- exit_rsi: float,
- stop_loss_pct: float,
- max_hold_bars: int,
- entry_slices: int,
- ) -> SegmentResult:
- closes = pd.Series([candle.close for candle in candles], dtype=float)
- trend = closes.rolling(trend_sma).mean().tolist()
- rsi_values = _compute_rsi(closes, 2)
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry_slices = 0
- pending_exit = False
- for index in range(warmup_bars, len(candles)):
- candle = candles[index]
- if pending_exit and position is not None:
- equity, won = _close_partial_trade(
- trades=trades,
- exits=exits,
- position=position,
- account_equity=equity,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- pending_entry_slices = 0
- if pending_entry_slices and equity > 0.0:
- slice_margin = equity / entry_slices
- if position is None:
- position = {
- "side": "long",
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "entry_index": index,
- "margin_used": slice_margin,
- "stop_price": candle.open * (1 - stop_loss_pct),
- }
- else:
- old_margin = float(position["margin_used"])
- new_margin = old_margin + slice_margin
- entry_price = (float(position["entry_price"]) * old_margin + candle.open * slice_margin) / new_margin
- position["entry_price"] = entry_price
- position["margin_used"] = new_margin
- position["stop_price"] = entry_price * (1 - stop_loss_pct)
- entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
- pending_entry_slices -= 1
- current_equity = equity
- if position is not None and candle.low <= float(position["stop_price"]):
- equity, won = _close_partial_trade(
- trades=trades,
- exits=exits,
- position=position,
- account_equity=equity,
- candle=candle,
- exit_price=float(position["stop_price"]),
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- pending_entry_slices = 0
- if position is not None:
- position_equity = mark_to_market(
- side="long",
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- current_equity = equity - float(position["margin_used"]) + position_equity
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(candles) - 1 or equity <= 0.0:
- continue
- current_rsi = rsi_values[index]
- current_trend = trend[index]
- if current_rsi != current_rsi or current_trend != current_trend:
- continue
- if position is not None:
- held_bars = index - int(position["entry_index"])
- if current_rsi >= exit_rsi or held_bars >= max_hold_bars:
- pending_exit = True
- pending_entry_slices = 0
- continue
- if pending_entry_slices == 0 and candle.close > float(current_trend) and current_rsi <= rsi_threshold:
- pending_entry_slices = entry_slices
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=(wins / trade_count) if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_rsi2_long_guarded_price_twap_segment(
- *,
- candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- trend_sma: int,
- rsi_threshold: float,
- exit_rsi: float,
- stop_loss_pct: float,
- max_hold_bars: int,
- entry_offsets: tuple[float, ...],
- entry_valid_bars: int,
- fill_buffer: float,
- ) -> SegmentResult:
- closes = pd.Series([candle.close for candle in candles], dtype=float)
- trend = closes.rolling(trend_sma).mean().tolist()
- rsi_values = _compute_rsi(closes, 2)
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_limits: list[dict[str, float | int]] = []
- pending_exit = False
- for index in range(warmup_bars, len(candles)):
- candle = candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- pending_limits = []
- active_limits: list[dict[str, float | int]] = []
- for limit in pending_limits:
- if index > int(limit["expires_index"]):
- continue
- limit_price = float(limit["price"])
- if candle.low <= limit_price * (1.0 - fill_buffer) and equity > 0.0:
- slice_margin = equity / len(entry_offsets)
- if position is None:
- position = {
- "side": "long",
- "entry_time": candle.ts,
- "entry_price": limit_price,
- "entry_index": index,
- "margin_used": slice_margin,
- "stop_price": limit_price * (1 - stop_loss_pct),
- }
- else:
- old_margin = float(position["margin_used"])
- new_margin = old_margin + slice_margin
- entry_price = (float(position["entry_price"]) * old_margin + limit_price * slice_margin) / new_margin
- position["entry_price"] = entry_price
- position["margin_used"] = new_margin
- position["stop_price"] = entry_price * (1 - stop_loss_pct)
- entries.append({"ts": candle.ts, "price": limit_price, "side": "long"})
- else:
- active_limits.append(limit)
- pending_limits = active_limits
- current_equity = equity
- if position is not None and candle.low <= float(position["stop_price"]):
- equity, won = _close_partial_trade(
- trades=trades,
- exits=exits,
- position=position,
- account_equity=equity,
- candle=candle,
- exit_price=float(position["stop_price"]),
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- pending_limits = []
- if position is not None:
- position_equity = mark_to_market(
- side="long",
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- current_equity = equity - float(position["margin_used"]) + position_equity
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(candles) - 1 or equity <= 0.0:
- continue
- current_rsi = rsi_values[index]
- current_trend = trend[index]
- if current_rsi != current_rsi or current_trend != current_trend:
- continue
- if position is not None:
- held_bars = index - int(position["entry_index"])
- if current_rsi >= exit_rsi or held_bars >= max_hold_bars:
- pending_exit = True
- pending_limits = []
- continue
- if not pending_limits and candle.close > float(current_trend) and current_rsi <= rsi_threshold:
- pending_limits = [
- {
- "price": candle.close * (1.0 - offset),
- "expires_index": index + entry_valid_bars,
- }
- for offset in entry_offsets
- ]
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=(wins / trade_count) if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_ma_cross_segment(
- *,
- candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- fast: int,
- slow: int,
- side_mode: str,
- ) -> SegmentResult:
- closes = pd.Series([candle.close for candle in candles], dtype=float)
- fast_ma = closes.rolling(fast).mean().tolist()
- slow_ma = closes.rolling(slow).mean().tolist()
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry_side: str | None = None
- pending_exit = False
- for index in range(warmup_bars, len(candles)):
- candle = candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry_side is not None and position is None and equity > 0.0:
- position = {
- "side": pending_entry_side,
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "margin_used": equity,
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": pending_entry_side})
- pending_entry_side = None
- current_equity = equity
- if position is not None:
- current_equity = mark_to_market(
- side=str(position["side"]),
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(candles) - 1 or equity <= 0.0:
- continue
- current_fast = fast_ma[index]
- current_slow = slow_ma[index]
- previous_fast = fast_ma[index - 1]
- previous_slow = slow_ma[index - 1]
- if current_fast != current_fast or current_slow != current_slow or previous_fast != previous_fast or previous_slow != previous_slow:
- continue
- crossed_up = previous_fast <= previous_slow and current_fast > current_slow
- crossed_down = previous_fast >= previous_slow and current_fast < current_slow
- if position is not None:
- if (position["side"] == "long" and crossed_down) or (position["side"] == "short" and crossed_up):
- pending_exit = True
- continue
- if crossed_up and side_mode in {"both", "long"}:
- pending_entry_side = "long"
- elif crossed_down and side_mode in {"both", "short"}:
- pending_entry_side = "short"
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=wins / trade_count if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_trend_rsi_bb_long_segment(
- *,
- candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- trend_sma: int,
- band_length: int,
- std_multiplier: float,
- rsi_threshold: float,
- exit_rsi: float,
- stop_loss_pct: float,
- ) -> SegmentResult:
- closes = pd.Series([candle.close for candle in candles], dtype=float)
- trend = closes.rolling(trend_sma).mean().tolist()
- middle = closes.rolling(band_length).mean().tolist()
- stdev = closes.rolling(band_length).std(ddof=0).tolist()
- lower = [
- float("nan") if middle_value != middle_value or std_value != std_value else middle_value - std_multiplier * std_value
- for middle_value, std_value in zip(middle, stdev)
- ]
- rsi_values = _compute_rsi(closes, 2)
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry = False
- pending_exit = False
- for index in range(warmup_bars, len(candles)):
- candle = candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry and position is None and equity > 0.0:
- position = {
- "side": "long",
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "entry_index": index,
- "margin_used": equity,
- "stop_price": candle.open * (1 - stop_loss_pct),
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
- pending_entry = False
- current_equity = equity
- if position is not None and candle.low <= float(position["stop_price"]):
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=float(position["stop_price"]),
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- if position is not None:
- current_equity = mark_to_market(
- side="long",
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(candles) - 1 or equity <= 0.0:
- continue
- current_rsi = rsi_values[index]
- current_trend = trend[index]
- current_middle = middle[index]
- current_lower = lower[index]
- if current_rsi != current_rsi or current_trend != current_trend or current_middle != current_middle or current_lower != current_lower:
- continue
- if position is not None:
- if current_rsi >= exit_rsi or candle.close >= float(current_middle):
- pending_exit = True
- continue
- if candle.close > float(current_trend) and candle.close <= float(current_lower) and current_rsi <= rsi_threshold:
- pending_entry = True
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=wins / trade_count if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_regime_hybrid_segment(
- *,
- candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- trend_sma: int,
- regime_lookback: int,
- neutral_ma_distance: float,
- rsi_long_threshold: float,
- rsi_exit: float,
- bb_length: int,
- bb_std: float,
- bb_bandwidth_lookback: int,
- stop_loss_pct: float,
- ) -> SegmentResult:
- closes = pd.Series([candle.close for candle in candles], dtype=float)
- trend = closes.rolling(trend_sma).mean().tolist()
- rsi_values = _compute_rsi(closes, 2)
- middle = closes.rolling(bb_length).mean().tolist()
- stdev = closes.rolling(bb_length).std(ddof=0).tolist()
- upper = [
- float("nan") if middle_value != middle_value or std_value != std_value else middle_value + bb_std * std_value
- for middle_value, std_value in zip(middle, stdev)
- ]
- lower = [
- float("nan") if middle_value != middle_value or std_value != std_value else middle_value - bb_std * std_value
- for middle_value, std_value in zip(middle, stdev)
- ]
- bandwidth = [
- 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
- for upper_value, lower_value, middle_value in zip(upper, lower, middle)
- ]
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry: dict[str, object] | None = None
- pending_exit = False
- for index in range(warmup_bars, len(candles)):
- candle = candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry is not None and position is None and equity > 0.0:
- side = str(pending_entry["side"])
- position = {
- "side": side,
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "entry_index": index,
- "margin_used": equity,
- "stop_price": candle.open * (1 - stop_loss_pct if side == "long" else 1 + stop_loss_pct),
- "mode": str(pending_entry["mode"]),
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": side})
- pending_entry = None
- current_equity = equity
- if position is not None:
- stop_hit = (
- position["side"] == "long" and candle.low <= float(position["stop_price"])
- ) or (
- position["side"] == "short" and candle.high >= float(position["stop_price"])
- )
- if stop_hit:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=float(position["stop_price"]),
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- if position is not None:
- current_equity = mark_to_market(
- side=str(position["side"]),
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(candles) - 1 or equity <= 0.0:
- continue
- current_trend = trend[index]
- current_rsi = rsi_values[index]
- current_middle = middle[index]
- current_upper = upper[index]
- current_lower = lower[index]
- if (
- current_trend != current_trend
- or current_rsi != current_rsi
- or current_middle != current_middle
- or current_upper != current_upper
- or current_lower != current_lower
- ):
- continue
- if position is not None:
- if position["mode"] == "rsi" and (current_rsi >= rsi_exit or candle.close < float(current_trend)):
- pending_exit = True
- elif position["mode"] == "bbmr" and (
- (position["side"] == "long" and candle.close >= float(current_middle))
- or (position["side"] == "short" and candle.close <= float(current_middle))
- ):
- pending_exit = True
- continue
- regime_return = candle.close / candles[index - regime_lookback].close - 1.0
- ma_distance = candle.close / float(current_trend) - 1.0
- if candle.close > float(current_trend) and regime_return > 0.0 and current_rsi <= rsi_long_threshold:
- pending_entry = {"side": "long", "mode": "rsi"}
- continue
- previous_bandwidths = [value for value in bandwidth[max(0, index - bb_bandwidth_lookback) : index] if value == value]
- if abs(ma_distance) > neutral_ma_distance or len(previous_bandwidths) < bb_bandwidth_lookback:
- continue
- if bandwidth[index] == bandwidth[index] and bandwidth[index] <= pd.Series(previous_bandwidths, dtype=float).median():
- if candle.close < float(current_lower):
- pending_entry = {"side": "long", "mode": "bbmr"}
- elif candle.close > float(current_upper):
- pending_entry = {"side": "short", "mode": "bbmr"}
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=wins / trade_count if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_eth_btc_rsi_filter_segment(
- *,
- eth_candles: list[Candle],
- btc_candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- eth_trend_sma: int,
- eth_rsi_threshold: float,
- eth_exit_rsi: float,
- btc_trend_sma: int,
- btc_momentum_lookback: int,
- btc_min_momentum: float,
- ) -> SegmentResult:
- eth_closes = pd.Series([candle.close for candle in eth_candles], dtype=float)
- btc_closes = pd.Series([candle.close for candle in btc_candles], dtype=float)
- eth_trend = eth_closes.rolling(eth_trend_sma).mean().tolist()
- eth_rsi = _compute_rsi(eth_closes, 2)
- btc_trend = btc_closes.rolling(btc_trend_sma).mean().tolist()
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry = False
- pending_exit = False
- for index in range(warmup_bars, len(eth_candles)):
- candle = eth_candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry and position is None and equity > 0.0:
- position = {
- "side": "long",
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "margin_used": equity,
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
- pending_entry = False
- current_equity = equity
- if position is not None:
- current_equity = mark_to_market(
- side="long",
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(eth_candles) - 1 or equity <= 0.0:
- continue
- current_eth_trend = eth_trend[index]
- current_eth_rsi = eth_rsi[index]
- current_btc_trend = btc_trend[index]
- if current_eth_trend != current_eth_trend or current_eth_rsi != current_eth_rsi or current_btc_trend != current_btc_trend:
- continue
- if position is not None:
- if current_eth_rsi >= eth_exit_rsi or btc_candles[index].close < float(current_btc_trend):
- pending_exit = True
- continue
- btc_momentum = btc_candles[index].close / btc_candles[index - btc_momentum_lookback].close - 1.0
- btc_risk_on = btc_candles[index].close > float(current_btc_trend) and btc_momentum >= btc_min_momentum
- eth_pullback = candle.close > float(current_eth_trend) and current_eth_rsi <= eth_rsi_threshold
- if btc_risk_on and eth_pullback:
- pending_entry = True
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=wins / trade_count if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=eth_candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_eth_btc_shock_filter_segment(
- *,
- eth_candles: list[Candle],
- btc_candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- eth_trend_sma: int,
- eth_rsi_threshold: float,
- eth_exit_rsi: float,
- btc_trend_sma: int,
- btc_momentum_lookback: int,
- btc_min_momentum: float,
- btc_shock_lookback: int,
- btc_max_realized_vol: float,
- btc_max_drawdown: float,
- ) -> SegmentResult:
- eth_closes = pd.Series([candle.close for candle in eth_candles], dtype=float)
- btc_closes = pd.Series([candle.close for candle in btc_candles], dtype=float)
- eth_trend = eth_closes.rolling(eth_trend_sma).mean().tolist()
- eth_rsi = _compute_rsi(eth_closes, 2)
- btc_trend = btc_closes.rolling(btc_trend_sma).mean().tolist()
- btc_realized_vol = btc_closes.pct_change().rolling(btc_shock_lookback).std(ddof=1).tolist()
- btc_recent_high = btc_closes.rolling(btc_shock_lookback).max().tolist()
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry = False
- pending_exit = False
- for index in range(warmup_bars, len(eth_candles)):
- candle = eth_candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry and position is None and equity > 0.0:
- position = {
- "side": "long",
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "margin_used": equity,
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
- pending_entry = False
- current_equity = equity
- if position is not None:
- current_equity = mark_to_market(
- side="long",
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(eth_candles) - 1 or equity <= 0.0:
- continue
- current_eth_trend = eth_trend[index]
- current_eth_rsi = eth_rsi[index]
- current_btc_trend = btc_trend[index]
- current_btc_vol = btc_realized_vol[index]
- current_btc_high = btc_recent_high[index]
- if (
- current_eth_trend != current_eth_trend
- or current_eth_rsi != current_eth_rsi
- or current_btc_trend != current_btc_trend
- or current_btc_vol != current_btc_vol
- or current_btc_high != current_btc_high
- ):
- continue
- btc_drawdown = btc_candles[index].close / float(current_btc_high) - 1.0
- btc_shock_ok = current_btc_vol <= btc_max_realized_vol and btc_drawdown >= -btc_max_drawdown
- if position is not None:
- if current_eth_rsi >= eth_exit_rsi or btc_candles[index].close < float(current_btc_trend) or not btc_shock_ok:
- pending_exit = True
- continue
- btc_momentum = btc_candles[index].close / btc_candles[index - btc_momentum_lookback].close - 1.0
- btc_risk_on = (
- btc_candles[index].close > float(current_btc_trend)
- and btc_momentum >= btc_min_momentum
- and btc_shock_ok
- )
- eth_pullback = candle.close > float(current_eth_trend) and current_eth_rsi <= eth_rsi_threshold
- if btc_risk_on and eth_pullback:
- pending_entry = True
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=wins / trade_count if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=eth_candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_eth_btc_ratio_pullback_segment(
- *,
- eth_candles: list[Candle],
- btc_candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- btc_trend_sma: int,
- btc_momentum_lookback: int,
- btc_min_momentum: float,
- ratio_length: int,
- ratio_std: float,
- ratio_rsi_threshold: float,
- stop_loss_pct: float,
- ) -> SegmentResult:
- eth_closes = pd.Series([candle.close for candle in eth_candles], dtype=float)
- btc_closes = pd.Series([candle.close for candle in btc_candles], dtype=float)
- ratio = eth_closes / btc_closes
- btc_trend = btc_closes.rolling(btc_trend_sma).mean().tolist()
- ratio_middle = ratio.rolling(ratio_length).mean().tolist()
- ratio_stdev = ratio.rolling(ratio_length).std(ddof=0).tolist()
- ratio_lower = [
- float("nan") if middle != middle or stdev != stdev else middle - ratio_std * stdev
- for middle, stdev in zip(ratio_middle, ratio_stdev)
- ]
- ratio_rsi = _compute_rsi(ratio, 2)
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry = False
- pending_exit = False
- for index in range(warmup_bars, len(eth_candles)):
- candle = eth_candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry and position is None and equity > 0.0:
- position = {
- "side": "long",
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "entry_index": index,
- "margin_used": equity,
- "stop_price": candle.open * (1 - stop_loss_pct),
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
- pending_entry = False
- current_equity = equity
- if position is not None and candle.low <= float(position["stop_price"]):
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=float(position["stop_price"]),
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- if position is not None:
- current_equity = mark_to_market(
- side="long",
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(eth_candles) - 1 or equity <= 0.0:
- continue
- current_btc_trend = btc_trend[index]
- current_ratio_middle = ratio_middle[index]
- current_ratio_lower = ratio_lower[index]
- current_ratio_rsi = ratio_rsi[index]
- if (
- current_btc_trend != current_btc_trend
- or current_ratio_middle != current_ratio_middle
- or current_ratio_lower != current_ratio_lower
- or current_ratio_rsi != current_ratio_rsi
- ):
- continue
- btc_momentum = btc_candles[index].close / btc_candles[index - btc_momentum_lookback].close - 1.0
- btc_risk_on = btc_candles[index].close > float(current_btc_trend) and btc_momentum >= btc_min_momentum
- if position is not None:
- if not btc_risk_on or ratio.iloc[index] >= float(current_ratio_middle):
- pending_exit = True
- continue
- ratio_pullback = ratio.iloc[index] <= float(current_ratio_lower) or current_ratio_rsi <= ratio_rsi_threshold
- if btc_risk_on and ratio_pullback:
- pending_entry = True
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=wins / trade_count if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=eth_candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def run_btc_lead_eth_lag_segment(
- *,
- eth_candles: list[Candle],
- btc_candles: list[Candle],
- leverage: int,
- warmup_bars: int,
- lead_lookback: int,
- btc_return_threshold: float,
- lag_gap: float,
- max_hold_bars: int,
- stop_loss_pct: float,
- take_profit_pct: float,
- ) -> SegmentResult:
- equity = INITIAL_EQUITY
- ending_equity = equity
- peak_equity = equity
- max_drawdown = 0.0
- wins = 0
- trades: list[dict[str, object]] = []
- entries: list[dict[str, object]] = []
- exits: list[dict[str, object]] = []
- equity_curve: list[dict[str, float | int]] = []
- position: dict[str, object] | None = None
- pending_entry = False
- pending_exit = False
- for index in range(warmup_bars, len(eth_candles)):
- candle = eth_candles[index]
- if pending_exit and position is not None:
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=candle.open,
- leverage=leverage,
- )
- wins += 1 if won else 0
- position = None
- pending_exit = False
- if pending_entry and position is None and equity > 0.0:
- position = {
- "side": "long",
- "entry_time": candle.ts,
- "entry_price": candle.open,
- "entry_index": index,
- "margin_used": equity,
- "stop_price": candle.open * (1.0 - stop_loss_pct),
- "take_profit_price": candle.open * (1.0 + take_profit_pct),
- }
- entries.append({"ts": candle.ts, "price": candle.open, "side": "long"})
- pending_entry = False
- current_equity = equity
- if position is not None:
- if candle.low <= float(position["stop_price"]):
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=float(position["stop_price"]),
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- elif candle.high >= float(position["take_profit_price"]):
- equity, won = _trade(
- trades=trades,
- exits=exits,
- position=position,
- candle=candle,
- exit_price=float(position["take_profit_price"]),
- leverage=leverage,
- )
- wins += 1 if won else 0
- current_equity = equity
- position = None
- if position is not None:
- current_equity = mark_to_market(
- side="long",
- margin_used=float(position["margin_used"]),
- entry_price=float(position["entry_price"]),
- mark_price=candle.close,
- leverage=leverage,
- )
- peak_equity = max(peak_equity, current_equity)
- max_drawdown = max(max_drawdown, (peak_equity - current_equity) / peak_equity)
- equity_curve.append({"ts": candle.ts, "equity": current_equity, "close": candle.close})
- ending_equity = current_equity
- if index == len(eth_candles) - 1 or equity <= 0.0:
- continue
- if position is not None:
- if index - int(position["entry_index"]) >= max_hold_bars:
- pending_exit = True
- continue
- btc_return = btc_candles[index].close / btc_candles[index - lead_lookback].close - 1.0
- eth_return = candle.close / eth_candles[index - lead_lookback].close - 1.0
- if btc_return >= btc_return_threshold and btc_return - eth_return >= lag_gap:
- pending_entry = True
- trade_count = len(trades)
- return SegmentResult(
- trade_count=trade_count,
- total_return=(ending_equity - INITIAL_EQUITY) / INITIAL_EQUITY,
- win_rate=wins / trade_count if trade_count else 0.0,
- max_drawdown=max_drawdown,
- trades=trades,
- open_position=position,
- candles=eth_candles[warmup_bars:],
- equity_curve=equity_curve,
- entries=entries,
- exits=exits,
- )
- def build_candidates() -> list[Candidate]:
- candidates: list[Candidate] = []
- candidates.append(Candidate("bbmr-default", 69, lambda candles, leverage, warmup_bars: run_bbmr_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=BBMRConfig())))
- candidates.append(Candidate("bbsb-default", 69, lambda candles, leverage, warmup_bars: run_bbsb_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=BBSBConfig())))
- for entry_window in (8, 12, 20):
- for exit_window in (4, 6, 10):
- for stop in (0.004, 0.008, 0.012):
- config = DonchianConfig(entry_window=entry_window, exit_window=exit_window, stop_loss_pct=stop)
- 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)))
- for trend in (30, 50, 80):
- for long_threshold, short_threshold in ((8.0, 92.0), (12.0, 88.0), (18.0, 82.0)):
- config = RSI2Config(trend_sma=trend, rsi_length=2, rsi_long_threshold=long_threshold, rsi_short_threshold=short_threshold, exit_rsi=50.0)
- 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)))
- for fast, slow in ((8, 21), (13, 34), (20, 50)):
- for stop in (0.003, 0.006, 0.01):
- config = EMAPullbackConfig(fast_ema=fast, slow_ema=slow, stop_buffer_pct=stop)
- 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)))
- for lookback in (6, 10, 16):
- for take, stop in ((0.004, 0.003), (0.006, 0.004), (0.01, 0.005)):
- 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)))
- for window in (24, 48, 72):
- for entry_z in (1.5, 2.0, 2.5):
- 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)))
- return candidates
- def evaluate_candidate(candidate: Candidate, candles: list[Candle]) -> dict[str, object]:
- sampled = sample_segments(
- candles=candles,
- segments=SEGMENTS,
- window_size=WINDOW_SIZE,
- warmup_bars=candidate.warmup_bars,
- )
- results = [
- candidate.run(
- candles=candles[segment.context_start : segment.report_end],
- leverage=LEVERAGE,
- warmup_bars=candidate.warmup_bars,
- )
- for segment in sampled
- ]
- returns = [result.total_return for result in results]
- return {
- "name": candidate.name,
- "avg_return": sum(returns) / len(returns),
- "median_return": float(pd.Series(returns).median()),
- "worst_return": min(returns),
- "best_return": max(returns),
- "trades": sum(result.trade_count for result in results),
- "win_rate": sum(result.win_rate for result in results) / len(results),
- "max_drawdown": max(result.max_drawdown for result in results),
- }
- def evaluate_candidate_all_windows(
- *,
- candidate: Candidate,
- candles: list[Candle],
- window_size: int,
- leverage: int,
- ) -> dict[str, object]:
- rows = evaluate_candidate_window_rows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=leverage,
- )
- return summarize_window_rows(rows, candidate.name)
- def evaluate_candidate_window_rows(
- *,
- candidate: Candidate,
- candles: list[Candle],
- window_size: int,
- leverage: int,
- ) -> list[dict[str, object]]:
- block_size = candidate.warmup_bars + window_size
- context_starts = list(range(0, len(candles) - block_size + 1, window_size))
- rows: list[dict[str, object]] = []
- for start in context_starts:
- result = candidate.run(
- candles=candles[start : start + block_size],
- leverage=leverage,
- warmup_bars=candidate.warmup_bars,
- )
- report_start = start + candidate.warmup_bars
- report_end = start + block_size - 1
- rows.append(
- {
- "window_start_ts": candles[report_start].ts,
- "window_end_ts": candles[report_end].ts,
- "total_return": result.total_return,
- "trade_count": result.trade_count,
- "win_rate": result.win_rate,
- "max_drawdown": result.max_drawdown,
- "trades": result.trades,
- }
- )
- return rows
- def evaluate_pair_candidate_window_rows(
- *,
- candidate: PairCandidate,
- eth_candles: list[Candle],
- btc_candles: list[Candle],
- window_size: int,
- leverage: int,
- ) -> list[dict[str, object]]:
- block_size = candidate.warmup_bars + window_size
- context_starts = list(range(0, len(eth_candles) - block_size + 1, window_size))
- rows: list[dict[str, object]] = []
- for start in context_starts:
- result = candidate.run(
- eth_candles=eth_candles[start : start + block_size],
- btc_candles=btc_candles[start : start + block_size],
- leverage=leverage,
- warmup_bars=candidate.warmup_bars,
- )
- report_start = start + candidate.warmup_bars
- report_end = start + block_size - 1
- rows.append(
- {
- "window_start_ts": eth_candles[report_start].ts,
- "window_end_ts": eth_candles[report_end].ts,
- "total_return": result.total_return,
- "trade_count": result.trade_count,
- "win_rate": result.win_rate,
- "max_drawdown": result.max_drawdown,
- "trades": result.trades,
- }
- )
- return rows
- def summarize_window_rows(rows: list[dict[str, object]], name: str = "") -> dict[str, object]:
- returns = [float(row["total_return"]) for row in rows]
- trade_returns = [
- float(trade["return_pct"]) / 100.0
- for row in rows
- for trade in row["trades"]
- ]
- winning_trade_returns = [value for value in trade_returns if value > 0.0]
- losing_trade_returns = [value for value in trade_returns if value < 0.0]
- avg_win_return = sum(winning_trade_returns) / len(winning_trade_returns) if winning_trade_returns else 0.0
- avg_loss_return_abs = abs(sum(losing_trade_returns) / len(losing_trade_returns)) if losing_trade_returns else 0.0
- gross_profit = sum(winning_trade_returns)
- gross_loss_abs = abs(sum(losing_trade_returns))
- series = pd.Series(returns, dtype=float)
- sample_count = len(returns)
- std = float(series.std(ddof=1)) if sample_count > 1 else 0.0
- ci_half_width = 1.96 * std / sqrt(sample_count) if sample_count > 1 else 0.0
- return {
- "name": name,
- "sample_count": sample_count,
- "avg_return": float(series.mean()),
- "ci95_low": float(series.mean() - ci_half_width),
- "ci95_high": float(series.mean() + ci_half_width),
- "median_return": float(series.median()),
- "positive_window_rate": float((series > 0).mean()),
- "worst_return": float(series.min()),
- "p10_return": float(series.quantile(0.10)),
- "p90_return": float(series.quantile(0.90)),
- "best_return": float(series.max()),
- "trades": sum(int(row["trade_count"]) for row in rows),
- "avg_trades_per_window": sum(int(row["trade_count"]) for row in rows) / sample_count,
- "win_rate": sum(float(row["win_rate"]) for row in rows) / sample_count,
- "trade_win_rate": len(winning_trade_returns) / len(trade_returns) if trade_returns else 0.0,
- "avg_trade_return": sum(trade_returns) / len(trade_returns) if trade_returns else 0.0,
- "avg_win_return": avg_win_return,
- "avg_loss_return_abs": avg_loss_return_abs,
- "payoff_ratio": avg_win_return / avg_loss_return_abs if avg_loss_return_abs else 0.0,
- "profit_factor": gross_profit / gross_loss_abs if gross_loss_abs else 0.0,
- "expectancy_per_trade": sum(trade_returns) / len(trade_returns) if trade_returns else 0.0,
- "max_drawdown": max(float(row["max_drawdown"]) for row in rows),
- "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,
- }
- def sort_robust_results(frame: pd.DataFrame) -> pd.DataFrame:
- return frame.sort_values(["ci95_low", "avg_return"], ascending=False)
- def add_cost_metrics(frame: pd.DataFrame, roundtrip_cost_on_margin: float) -> pd.DataFrame:
- frame = frame.copy()
- cost = frame["avg_trades_per_window"] * roundtrip_cost_on_margin
- frame["roundtrip_cost_on_margin"] = roundtrip_cost_on_margin
- frame["net_avg_return"] = frame["avg_return"] - cost
- frame["net_ci95_low"] = frame["ci95_low"] - cost
- frame["net_ci95_high"] = frame["ci95_high"] - cost
- frame["breakeven_roundtrip_cost_on_margin"] = frame["avg_return"] / frame["avg_trades_per_window"]
- return frame
- def sort_cost_results(frame: pd.DataFrame) -> pd.DataFrame:
- return frame.sort_values(["net_ci95_low", "net_avg_return"], ascending=False)
- def max_drawdown_from_equity(equity_values: list[float]) -> float:
- peak = equity_values[0]
- max_drawdown = 0.0
- for equity in equity_values:
- peak = max(peak, equity)
- if peak > 0.0:
- max_drawdown = max(max_drawdown, (peak - equity) / peak)
- return max_drawdown
- def cost_adjusted_trade_equity_frame(result: SegmentResult, roundtrip_cost_on_margin: float) -> pd.DataFrame:
- rows = [{"ts": pd.to_datetime(result.equity_curve[0]["ts"], unit="ms", utc=True), "equity": INITIAL_EQUITY}]
- equity = INITIAL_EQUITY
- for trade in result.trades:
- equity *= 1.0 + float(trade["return_pct"]) / 100.0 - roundtrip_cost_on_margin * float(trade.get("cost_weight", 1.0))
- rows.append({"ts": pd.to_datetime(str(trade["exit_time"]), utc=True), "equity": equity})
- return pd.DataFrame(rows)
- def annualized_metrics_from_equity(frame: pd.DataFrame, first_ts: int, last_ts: int) -> dict[str, float]:
- years = (last_ts - first_ts) / 86_400_000 / 365
- total_return = float(frame["equity"].iloc[-1] / frame["equity"].iloc[0] - 1.0)
- annualized_return = (1.0 + total_return) ** (1.0 / years) - 1.0 if total_return > -1.0 and years > 0.0 else 0.0
- max_drawdown = max_drawdown_from_equity([float(value) for value in frame["equity"]])
- daily = frame.set_index("ts")["equity"].resample("1D").last().ffill()
- daily_returns = daily.pct_change().dropna()
- daily_std = float(daily_returns.std(ddof=1)) if len(daily_returns) > 1 else 0.0
- sharpe = float(daily_returns.mean()) / daily_std * sqrt(365) if daily_std else 0.0
- return {
- "net_total_return": total_return,
- "net_annualized_return": annualized_return,
- "net_max_drawdown": max_drawdown,
- "net_calmar": annualized_return / max_drawdown if max_drawdown else 0.0,
- "net_sharpe_daily": sharpe,
- }
- def recent_horizon_metrics_from_equity(
- frame: pd.DataFrame,
- last_ts: int,
- horizons: tuple[tuple[str, pd.DateOffset], ...],
- ) -> pd.DataFrame:
- rows: list[dict[str, object]] = []
- end_time = pd.to_datetime(last_ts, unit="ms", utc=True)
- for label, offset in horizons:
- cutoff = end_time - offset
- before_cutoff = frame[frame["ts"] <= cutoff]
- if len(before_cutoff):
- start_equity = float(before_cutoff["equity"].iloc[-1])
- start_time = cutoff
- after_cutoff = frame[frame["ts"] > cutoff]
- horizon_frame = pd.concat(
- [
- pd.DataFrame([{"ts": start_time, "equity": start_equity}]),
- after_cutoff[["ts", "equity"]],
- ],
- ignore_index=True,
- )
- else:
- horizon_frame = frame[["ts", "equity"]].copy()
- start_time = pd.Timestamp(horizon_frame["ts"].iloc[0])
- metrics = annualized_metrics_from_equity(
- horizon_frame,
- int(start_time.timestamp() * 1000),
- last_ts,
- )
- rows.append(
- {
- "horizon": label,
- "horizon_start": start_time.strftime("%Y-%m-%d %H:%M"),
- "horizon_end": end_time.strftime("%Y-%m-%d %H:%M"),
- "horizon_days": (end_time - start_time).total_seconds() / 86_400,
- **metrics,
- }
- )
- return pd.DataFrame(rows)
- def build_rsi2_candidate(trend: int, long_threshold: float, short_threshold: float) -> Candidate:
- config = RSI2Config(
- trend_sma=trend,
- rsi_length=2,
- rsi_long_threshold=long_threshold,
- rsi_short_threshold=short_threshold,
- exit_rsi=50.0,
- )
- return 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,
- ),
- )
- def build_rsi2_side_candidate(
- trend: int,
- long_threshold: float,
- short_threshold: float,
- exit_rsi: float,
- side_mode: str,
- ) -> Candidate:
- config = RSI2Config(
- trend_sma=trend,
- rsi_length=2,
- rsi_long_threshold=long_threshold,
- rsi_short_threshold=short_threshold,
- exit_rsi=exit_rsi,
- )
- return Candidate(
- f"rsi2-{side_mode}-t{trend}-l{long_threshold}-s{short_threshold}-x{exit_rsi}",
- max(trend, 3),
- lambda candles, leverage, warmup_bars, config=config, side_mode=side_mode: run_rsi2_side_segment(
- candles=candles,
- leverage=leverage,
- warmup_bars=warmup_bars,
- config=config,
- side_mode=side_mode,
- ),
- )
- def build_rsi2_long_guarded_candidate(
- trend: int,
- rsi_threshold: float,
- exit_rsi: float,
- stop_loss_pct: float,
- max_hold_bars: int,
- ) -> Candidate:
- return Candidate(
- f"rsi2-long-guarded-t{trend}-l{rsi_threshold}-x{exit_rsi}-sl{stop_loss_pct}-mh{max_hold_bars}",
- max(trend, 3),
- 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(
- candles=candles,
- leverage=leverage,
- warmup_bars=warmup_bars,
- trend_sma=trend,
- rsi_threshold=rsi_threshold,
- exit_rsi=exit_rsi,
- stop_loss_pct=stop_loss_pct,
- max_hold_bars=max_hold_bars,
- ),
- )
- def build_rsi2_long_guarded_twap_candidate(
- trend: int,
- rsi_threshold: float,
- exit_rsi: float,
- stop_loss_pct: float,
- max_hold_bars: int,
- entry_slices: int,
- ) -> Candidate:
- return Candidate(
- f"rsi2-long-guarded-twap{entry_slices}-t{trend}-l{rsi_threshold}-x{exit_rsi}-sl{stop_loss_pct}-mh{max_hold_bars}",
- max(trend, 3),
- 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(
- candles=candles,
- leverage=leverage,
- warmup_bars=warmup_bars,
- trend_sma=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,
- ),
- )
- def build_rsi2_long_guarded_price_twap_candidate(
- trend: int,
- rsi_threshold: float,
- exit_rsi: float,
- stop_loss_pct: float,
- max_hold_bars: int,
- entry_offsets: tuple[float, ...],
- entry_valid_bars: int,
- fill_buffer: float = 0.0,
- ) -> Candidate:
- offset_label = "-".join(f"{offset:.4f}" for offset in entry_offsets)
- buffer_label = f"-fb{fill_buffer:.4f}" if fill_buffer else ""
- return Candidate(
- 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}",
- max(trend, 3),
- 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(
- candles=candles,
- leverage=leverage,
- warmup_bars=warmup_bars,
- trend_sma=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,
- ),
- )
- def build_ma_cross_candidate(fast: int, slow: int, side_mode: str) -> Candidate:
- return Candidate(
- f"ma-cross-{side_mode}-f{fast}-s{slow}",
- slow,
- lambda candles, leverage, warmup_bars, fast=fast, slow=slow, side_mode=side_mode: run_ma_cross_segment(
- candles=candles,
- leverage=leverage,
- warmup_bars=warmup_bars,
- fast=fast,
- slow=slow,
- side_mode=side_mode,
- ),
- )
- def build_trend_rsi_bb_long_candidate(
- trend_sma: int,
- band_length: int,
- std_multiplier: float,
- rsi_threshold: float,
- exit_rsi: float,
- stop_loss_pct: float,
- ) -> Candidate:
- return Candidate(
- f"trend-rsi-bb-long-t{trend_sma}-b{band_length}-m{std_multiplier}-r{rsi_threshold}-x{exit_rsi}-sl{stop_loss_pct}",
- max(trend_sma, band_length, 3),
- 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(
- candles=candles,
- leverage=leverage,
- warmup_bars=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,
- ),
- )
- def build_regime_hybrid_candidate(
- trend_sma: int,
- regime_lookback: int,
- neutral_ma_distance: float,
- rsi_long_threshold: float,
- rsi_exit: float,
- bb_std: float,
- stop_loss_pct: float,
- ) -> Candidate:
- return Candidate(
- 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}",
- max(trend_sma, regime_lookback, 20, 50, 3),
- 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(
- candles=candles,
- leverage=leverage,
- warmup_bars=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_length=20,
- bb_std=bb_std,
- bb_bandwidth_lookback=50,
- stop_loss_pct=stop_loss_pct,
- ),
- )
- def build_eth_btc_rsi_filter_candidate(
- eth_trend_sma: int,
- eth_rsi_threshold: float,
- eth_exit_rsi: float,
- btc_trend_sma: int,
- btc_momentum_lookback: int,
- btc_min_momentum: float,
- ) -> PairCandidate:
- return PairCandidate(
- 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}",
- max(eth_trend_sma, btc_trend_sma, btc_momentum_lookback, 3),
- 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(
- eth_candles=eth_candles,
- btc_candles=btc_candles,
- leverage=leverage,
- warmup_bars=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,
- ),
- )
- def build_eth_btc_shock_filter_candidate(
- eth_trend_sma: int,
- eth_rsi_threshold: float,
- eth_exit_rsi: float,
- btc_trend_sma: int,
- btc_momentum_lookback: int,
- btc_min_momentum: float,
- btc_shock_lookback: int,
- btc_max_realized_vol: float,
- btc_max_drawdown: float,
- ) -> PairCandidate:
- return PairCandidate(
- 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}",
- max(eth_trend_sma, btc_trend_sma, btc_momentum_lookback, btc_shock_lookback + 1, 3),
- 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(
- eth_candles=eth_candles,
- btc_candles=btc_candles,
- leverage=leverage,
- warmup_bars=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,
- ),
- )
- def build_eth_btc_ratio_pullback_candidate(
- btc_trend_sma: int,
- btc_momentum_lookback: int,
- btc_min_momentum: float,
- ratio_length: int,
- ratio_std: float,
- ratio_rsi_threshold: float,
- stop_loss_pct: float,
- ) -> PairCandidate:
- return PairCandidate(
- 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}",
- max(btc_trend_sma, btc_momentum_lookback, ratio_length, 3),
- 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(
- eth_candles=eth_candles,
- btc_candles=btc_candles,
- leverage=leverage,
- warmup_bars=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,
- ),
- )
- def build_btc_lead_eth_lag_candidate(
- lead_lookback: int,
- btc_return_threshold: float,
- lag_gap: float,
- max_hold_bars: int,
- stop_loss_pct: float,
- take_profit_pct: float,
- ) -> PairCandidate:
- return PairCandidate(
- 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}",
- lead_lookback,
- 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(
- eth_candles=eth_candles,
- btc_candles=btc_candles,
- leverage=leverage,
- warmup_bars=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,
- ),
- )
- def history_bars_for_years(bar: str, years: float) -> int:
- if not bar.endswith("m"):
- raise ValueError("minute bar is required")
- minutes = int(bar[:-1])
- if minutes <= 0:
- raise ValueError("minute bar is required")
- return int(MINUTES_PER_YEAR * years / minutes)
- def build_strategy_timeframe_candidates() -> list[Candidate]:
- return [
- Candidate("bbmr-default", 69, lambda candles, leverage, warmup_bars: run_bbmr_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=BBMRConfig())),
- Candidate("bbsb-default", 69, lambda candles, leverage, warmup_bars: run_bbsb_segment(candles=candles, leverage=leverage, warmup_bars=warmup_bars, config=BBSBConfig())),
- 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))),
- build_rsi2_candidate(50, 3.0, 97.0),
- 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))),
- 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)),
- 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)),
- ]
- def summarize_periods(frame: pd.DataFrame, period: str, roundtrip_cost_on_margin: float) -> pd.DataFrame:
- period_frame = frame.copy()
- 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)
- grouped = (
- period_frame.groupby("period", as_index=False)
- .agg(
- window_count=("total_return", "size"),
- avg_return=("total_return", "mean"),
- median_return=("total_return", "median"),
- positive_window_rate=("total_return", lambda values: float((values > 0.0).mean())),
- worst_return=("total_return", "min"),
- best_return=("total_return", "max"),
- trades=("trade_count", "sum"),
- avg_trades_per_window=("trade_count", "mean"),
- avg_window_win_rate=("win_rate", "mean"),
- max_drawdown=("max_drawdown", "max"),
- )
- .sort_values("period")
- )
- grouped["net_avg_return"] = grouped["avg_return"] - grouped["avg_trades_per_window"] * roundtrip_cost_on_margin
- return grouped
- def summarize_equity_periods(result: SegmentResult, period: str) -> pd.DataFrame:
- frame = pd.DataFrame(result.equity_curve)
- frame["period"] = pd.to_datetime(frame["ts"], unit="ms", utc=True).dt.tz_localize(None).dt.to_period(period).astype(str)
- grouped = (
- frame.groupby("period", as_index=False)
- .agg(
- start_equity=("equity", "first"),
- end_equity=("equity", "last"),
- min_equity=("equity", "min"),
- max_equity=("equity", "max"),
- bars=("equity", "size"),
- )
- .sort_values("period")
- )
- grouped["return"] = grouped["end_equity"] / grouped["start_equity"] - 1.0
- grouped["drawdown_from_period_high"] = (grouped["max_equity"] - grouped["min_equity"]) / grouped["max_equity"]
- return grouped
- def summarize_cost_adjusted_trade_equity_periods(
- result: SegmentResult,
- period: str,
- roundtrip_cost_on_margin: float,
- ) -> pd.DataFrame:
- frame = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
- frame["period"] = frame["ts"].dt.tz_localize(None).dt.to_period(period).astype(str)
- grouped = (
- frame.groupby("period", as_index=False)
- .agg(
- start_equity=("equity", "first"),
- end_equity=("equity", "last"),
- min_equity=("equity", "min"),
- max_equity=("equity", "max"),
- trades=("equity", lambda values: max(len(values) - 1, 0)),
- )
- .sort_values("period")
- )
- grouped["return"] = grouped["end_equity"] / grouped["start_equity"] - 1.0
- grouped["drawdown_from_period_high"] = (grouped["max_equity"] - grouped["min_equity"]) / grouped["max_equity"]
- return grouped
- def add_market_regime_columns(candles: list[Candle], rows: list[dict[str, object]], roundtrip_cost_on_margin: float) -> pd.DataFrame:
- index_by_ts = {candle.ts: index for index, candle in enumerate(candles)}
- output_rows: list[dict[str, object]] = []
- closes = pd.Series([candle.close for candle in candles], dtype=float)
- long_ma = closes.rolling(240).mean().tolist()
- for row in rows:
- start = index_by_ts[int(row["window_start_ts"])]
- end = index_by_ts[int(row["window_end_ts"])]
- window_closes = closes.iloc[start : end + 1]
- returns = window_closes.pct_change().dropna()
- ma_value = long_ma[end]
- close_value = float(window_closes.iloc[-1])
- output_rows.append(
- {
- **{key: value for key, value in row.items() if key != "trades"},
- "net_return": float(row["total_return"]) - int(row["trade_count"]) * roundtrip_cost_on_margin,
- "market_return": close_value / float(window_closes.iloc[0]) - 1.0,
- "realized_vol": float(returns.std(ddof=1)) if len(returns) > 1 else 0.0,
- "ma240_distance": close_value / float(ma_value) - 1.0 if ma_value == ma_value and ma_value else 0.0,
- }
- )
- frame = pd.DataFrame(output_rows)
- frame["market_return_bucket"] = pd.qcut(frame["market_return"], 3, labels=["down", "flat", "up"], duplicates="drop")
- frame["realized_vol_bucket"] = pd.qcut(frame["realized_vol"], 3, labels=["low", "mid", "high"], duplicates="drop")
- frame["ma240_distance_bucket"] = pd.qcut(frame["ma240_distance"], 3, labels=["below", "near", "above"], duplicates="drop")
- return frame
- def summarize_regime_columns(frame: pd.DataFrame) -> pd.DataFrame:
- summaries: list[pd.DataFrame] = []
- for column in ("market_return_bucket", "realized_vol_bucket", "ma240_distance_bucket"):
- grouped = (
- frame.groupby(["symbol", "bar", "name", column], observed=True, as_index=False)
- .agg(
- sample_count=("net_return", "size"),
- avg_net_return=("net_return", "mean"),
- median_net_return=("net_return", "median"),
- positive_window_rate=("net_return", lambda values: float((values > 0.0).mean())),
- worst_net_return=("net_return", "min"),
- best_net_return=("net_return", "max"),
- avg_trades=("trade_count", "mean"),
- avg_market_return=("market_return", "mean"),
- avg_realized_vol=("realized_vol", "mean"),
- avg_ma240_distance=("ma240_distance", "mean"),
- )
- .rename(columns={column: "regime"})
- )
- grouped.insert(3, "regime_type", column.removesuffix("_bucket"))
- summaries.append(grouped)
- return pd.concat(summaries, ignore_index=True).sort_values(
- ["name", "regime_type", "avg_net_return"],
- ascending=[True, True, False],
- )
- def main() -> int:
- client = OkxClient()
- candidates = build_candidates()
- rows: list[dict[str, object]] = []
- for symbol in SYMBOLS:
- for bar in BARS:
- candles = get_candles_cached(client, symbol, bar, HISTORY_LIMIT)
- for candidate in candidates:
- metrics = evaluate_candidate(candidate, candles)
- rows.append({"symbol": symbol, "bar": bar, **metrics})
- print(f"done {symbol} {bar}")
- frame = pd.DataFrame(rows)
- frame["score"] = frame["avg_return"] - frame["max_drawdown"] * 0.25
- columns = ["symbol", "bar", "name", "avg_return", "median_return", "worst_return", "best_return", "trades", "win_rate", "max_drawdown", "score"]
- print(frame.sort_values(["avg_return", "median_return"], ascending=False)[columns].head(30).to_string(index=False))
- frame.to_csv("ultrashort-exploration.csv", index=False)
- return 0
- def focus_vwap() -> int:
- client = OkxClient()
- candles = get_candles_cached(client, "ETH-USDT-SWAP", "3m", HISTORY_LIMIT)
- rows: list[dict[str, object]] = []
- for seed in (3, 7, 11, 17, 23):
- for window in (56, 64, 72, 80, 96):
- for entry_z in (1.6, 1.8, 2.0, 2.2, 2.4):
- for stop in (0.004, 0.006, 0.008):
- warmup_bars = window * 2
- sampled = sample_segments(
- candles=candles,
- segments=SEGMENTS,
- window_size=WINDOW_SIZE,
- warmup_bars=warmup_bars,
- seed=seed,
- )
- results = [
- run_vwap_reversion_segment(
- candles=candles[segment.context_start : segment.report_end],
- leverage=LEVERAGE,
- warmup_bars=warmup_bars,
- window=window,
- entry_z=entry_z,
- exit_z=0.2,
- stop_loss_pct=stop,
- )
- for segment in sampled
- ]
- returns = [result.total_return for result in results]
- rows.append(
- {
- "seed": seed,
- "window": window,
- "entry_z": entry_z,
- "stop": stop,
- "avg_return": sum(returns) / len(returns),
- "median_return": float(pd.Series(returns).median()),
- "worst_return": min(returns),
- "best_return": max(returns),
- "trades": sum(result.trade_count for result in results),
- "win_rate": sum(result.win_rate for result in results) / len(results),
- "max_drawdown": max(result.max_drawdown for result in results),
- }
- )
- frame = pd.DataFrame(rows)
- grouped = (
- frame.groupby(["window", "entry_z", "stop"], as_index=False)
- .agg(
- avg_return=("avg_return", "mean"),
- median_return=("median_return", "mean"),
- worst_seed_avg=("avg_return", "min"),
- worst_window_return=("worst_return", "min"),
- trades=("trades", "mean"),
- win_rate=("win_rate", "mean"),
- max_drawdown=("max_drawdown", "max"),
- )
- .sort_values(["avg_return", "worst_seed_avg"], ascending=False)
- )
- print(grouped.head(30).to_string(index=False))
- grouped.to_csv("ultrashort-vwap-focus.csv", index=False)
- return 0
- def robust_vwap(history_limit: int, window_size: int) -> int:
- client = OkxClient()
- rows: list[dict[str, object]] = []
- candidates = [
- Candidate(
- f"vwap-revert-w{window}-z{entry_z}-sl{stop}",
- window * 2,
- lambda candles, leverage, warmup_bars, window=window, entry_z=entry_z, stop=stop: 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=stop,
- ),
- )
- for window in (56, 64, 72, 80, 96)
- for entry_z in (1.6, 1.8, 2.0, 2.2, 2.4)
- for stop in (0.004, 0.006, 0.008)
- ]
- for symbol in SYMBOLS:
- candles = get_candles_cached(client, symbol, "3m", history_limit)
- for candidate in candidates:
- metrics = evaluate_candidate_all_windows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- rows.append({"symbol": symbol, "bar": "3m", "history_bars": len(candles), **metrics})
- print(f"done robust {symbol} 3m {len(candles)} bars")
- frame = pd.DataFrame(rows)
- columns = [
- "symbol",
- "bar",
- "history_bars",
- "name",
- "sample_count",
- "avg_return",
- "ci95_low",
- "ci95_high",
- "median_return",
- "positive_window_rate",
- "worst_return",
- "p10_return",
- "p90_return",
- "best_return",
- "trades",
- "avg_trades_per_window",
- "win_rate",
- "trade_win_rate",
- "avg_trade_return",
- "avg_win_return",
- "avg_loss_return_abs",
- "payoff_ratio",
- "profit_factor",
- "expectancy_per_trade",
- "max_drawdown",
- "return_drawdown_ratio",
- ]
- frame = sort_robust_results(frame)
- print(GROSS_RETURN_NOTE)
- print(frame[columns].head(30).to_string(index=False))
- frame.to_csv("ultrashort-vwap-robust.csv", index=False)
- return 0
- def robust_all(history_limit: int, window_size: int) -> int:
- client = OkxClient()
- candidates = build_candidates()
- rows: list[dict[str, object]] = []
- for symbol in SYMBOLS:
- for bar in BARS:
- candles = get_candles_cached(client, symbol, bar, history_limit)
- for candidate in candidates:
- metrics = evaluate_candidate_all_windows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- rows.append({"symbol": symbol, "bar": bar, "history_bars": len(candles), **metrics})
- print(f"done robust all {symbol} {bar} {len(candles)} bars")
- frame = pd.DataFrame(rows)
- columns = [
- "symbol",
- "bar",
- "history_bars",
- "name",
- "sample_count",
- "avg_return",
- "ci95_low",
- "ci95_high",
- "median_return",
- "positive_window_rate",
- "worst_return",
- "p10_return",
- "p90_return",
- "best_return",
- "trades",
- "avg_trades_per_window",
- "win_rate",
- "trade_win_rate",
- "avg_trade_return",
- "avg_win_return",
- "avg_loss_return_abs",
- "payoff_ratio",
- "profit_factor",
- "expectancy_per_trade",
- "max_drawdown",
- "return_drawdown_ratio",
- ]
- frame = sort_robust_results(frame)
- print(GROSS_RETURN_NOTE)
- print(frame[columns].head(40).to_string(index=False))
- frame.to_csv("ultrashort-robust-all.csv", index=False)
- return 0
- def robust_rsi2_cost_search(history_limit: int, window_size: int, roundtrip_cost_on_margin: float) -> int:
- client = OkxClient()
- candidates = [
- build_rsi2_candidate(trend, long_threshold, short_threshold)
- for trend in (30, 50, 80, 120, 160, 240)
- for long_threshold, short_threshold in (
- (3.0, 97.0),
- (5.0, 95.0),
- (8.0, 92.0),
- (10.0, 90.0),
- (12.0, 88.0),
- (15.0, 85.0),
- (18.0, 82.0),
- )
- ]
- rows: list[dict[str, object]] = []
- for symbol in SYMBOLS:
- for bar in ("3m", "5m", "15m"):
- candles = get_candles_cached(client, symbol, bar, history_limit)
- for candidate in candidates:
- metrics = evaluate_candidate_all_windows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- rows.append({"symbol": symbol, "bar": bar, "history_bars": len(candles), **metrics})
- print(f"done robust rsi2 cost {symbol} {bar} {len(candles)} bars")
- frame = sort_cost_results(add_cost_metrics(pd.DataFrame(rows), roundtrip_cost_on_margin))
- columns = [
- "symbol",
- "bar",
- "history_bars",
- "name",
- "sample_count",
- "avg_return",
- "ci95_low",
- "avg_trades_per_window",
- "breakeven_roundtrip_cost_on_margin",
- "roundtrip_cost_on_margin",
- "net_avg_return",
- "net_ci95_low",
- "net_ci95_high",
- "median_return",
- "positive_window_rate",
- "worst_return",
- "trade_win_rate",
- "avg_trade_return",
- "avg_win_return",
- "avg_loss_return_abs",
- "payoff_ratio",
- "profit_factor",
- "expectancy_per_trade",
- "max_drawdown",
- "return_drawdown_ratio",
- ]
- print(GROSS_RETURN_NOTE)
- print(frame[columns].head(40).to_string(index=False))
- frame.to_csv("ultrashort-rsi2-cost-search.csv", index=False)
- return 0
- def rsi2_period_analysis(
- *,
- symbol: str,
- bar: str,
- history_limit: int,
- window_size: int,
- roundtrip_cost_on_margin: float,
- trend: int,
- long_threshold: float,
- short_threshold: float,
- ) -> int:
- client = OkxClient()
- candles = get_candles_cached(client, symbol, bar, history_limit)
- candidate = build_rsi2_candidate(trend, long_threshold, short_threshold)
- rows = evaluate_candidate_window_rows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- window_frame = pd.DataFrame(rows).drop(columns=["trades"])
- window_frame["window_start"] = pd.to_datetime(window_frame["window_start_ts"], unit="ms", utc=True).dt.strftime("%Y-%m-%d %H:%M")
- window_frame["window_end"] = pd.to_datetime(window_frame["window_end_ts"], unit="ms", utc=True).dt.strftime("%Y-%m-%d %H:%M")
- window_frame["net_return"] = window_frame["total_return"] - window_frame["trade_count"] * roundtrip_cost_on_margin
- window_frame.insert(0, "name", candidate.name)
- window_frame.insert(0, "history_bars", len(candles))
- window_frame.insert(0, "bar", bar)
- window_frame.insert(0, "symbol", symbol)
- monthly = summarize_periods(window_frame, "M", roundtrip_cost_on_margin)
- quarterly = summarize_periods(window_frame, "Q", roundtrip_cost_on_margin)
- full_result = candidate.run(candles=candles, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
- equity_monthly = summarize_equity_periods(full_result, "M")
- equity_quarterly = summarize_equity_periods(full_result, "Q")
- net_trade_equity_monthly = summarize_cost_adjusted_trade_equity_periods(full_result, "M", roundtrip_cost_on_margin)
- net_trade_equity_quarterly = summarize_cost_adjusted_trade_equity_periods(full_result, "Q", roundtrip_cost_on_margin)
- window_frame.to_csv("ultrashort-rsi2-window-distribution.csv", index=False)
- monthly.to_csv("ultrashort-rsi2-monthly.csv", index=False)
- quarterly.to_csv("ultrashort-rsi2-quarterly.csv", index=False)
- equity_monthly.to_csv("ultrashort-rsi2-equity-monthly.csv", index=False)
- equity_quarterly.to_csv("ultrashort-rsi2-equity-quarterly.csv", index=False)
- net_trade_equity_monthly.to_csv("ultrashort-rsi2-net-trade-equity-monthly.csv", index=False)
- net_trade_equity_quarterly.to_csv("ultrashort-rsi2-net-trade-equity-quarterly.csv", index=False)
- summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin)
- first_candle = _format_ts(candles[0].ts)
- last_candle = _format_ts(candles[-1].ts)
- print(f"actual data range UTC: {first_candle} -> {last_candle}; bars={len(candles)}")
- print(
- summary[
- [
- "sample_count",
- "avg_return",
- "ci95_low",
- "avg_trades_per_window",
- "roundtrip_cost_on_margin",
- "net_avg_return",
- "net_ci95_low",
- "positive_window_rate",
- "trades",
- "trade_win_rate",
- "avg_win_return",
- "avg_loss_return_abs",
- "payoff_ratio",
- "profit_factor",
- "max_drawdown",
- ]
- ].to_string(index=False)
- )
- print("monthly")
- print(monthly.to_string(index=False))
- print("quarterly")
- print(quarterly.to_string(index=False))
- print("equity monthly")
- print(equity_monthly.to_string(index=False))
- print("equity quarterly")
- print(equity_quarterly.to_string(index=False))
- print("cost-adjusted closed-trade equity monthly")
- print(net_trade_equity_monthly.to_string(index=False))
- print("cost-adjusted closed-trade equity quarterly")
- print(net_trade_equity_quarterly.to_string(index=False))
- return 0
- def strategy_timeframe_analysis(
- *,
- symbols: tuple[str, ...],
- bars: tuple[str, ...],
- years: float,
- window_size: int,
- roundtrip_cost_on_margin: float,
- period: str,
- ) -> int:
- client = OkxClient()
- candidates = build_strategy_timeframe_candidates()
- summary_rows: list[dict[str, object]] = []
- period_frames: list[pd.DataFrame] = []
- availability_rows: list[dict[str, object]] = []
- for symbol in symbols:
- for bar in bars:
- requested_bars = history_bars_for_years(bar, years)
- candles = get_candles_cached(client, symbol, bar, requested_bars)
- first_ts = candles[0].ts
- last_ts = candles[-1].ts
- availability_rows.append(
- {
- "symbol": symbol,
- "bar": bar,
- "requested_years": years,
- "requested_bars": requested_bars,
- "actual_bars": len(candles),
- "first_candle": _format_ts(first_ts),
- "last_candle": _format_ts(last_ts),
- "actual_days": (last_ts - first_ts) / 86_400_000,
- "complete_requested_range": len(candles) >= requested_bars,
- }
- )
- for candidate in candidates:
- rows = evaluate_candidate_window_rows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
- summary_rows.append(
- {
- "symbol": symbol,
- "bar": bar,
- "requested_bars": requested_bars,
- "actual_bars": len(candles),
- "first_candle": _format_ts(first_ts),
- "last_candle": _format_ts(last_ts),
- **summary,
- }
- )
- window_frame = pd.DataFrame(rows).drop(columns=["trades"])
- period_frame = summarize_periods(window_frame, period, roundtrip_cost_on_margin)
- period_frame.insert(0, "name", candidate.name)
- period_frame.insert(0, "bar", bar)
- period_frame.insert(0, "symbol", symbol)
- period_frames.append(period_frame)
- print(f"done strategy timeframe {symbol} {bar} {candidate.name} windows={len(rows)}")
- availability = pd.DataFrame(availability_rows)
- summary_frame = sort_cost_results(pd.DataFrame(summary_rows))
- period_summary = pd.concat(period_frames, ignore_index=True)
- availability.to_csv("ultrashort-strategy-timeframe-availability.csv", index=False)
- summary_frame.to_csv("ultrashort-strategy-timeframe-summary.csv", index=False)
- period_summary.to_csv("ultrashort-strategy-timeframe-periods.csv", index=False)
- print("availability")
- print(availability.to_string(index=False))
- print("summary")
- print(
- summary_frame[
- [
- "symbol",
- "bar",
- "name",
- "actual_bars",
- "sample_count",
- "trades",
- "net_avg_return",
- "net_ci95_low",
- "positive_window_rate",
- "trade_win_rate",
- "payoff_ratio",
- "profit_factor",
- "max_drawdown",
- ]
- ].head(50).to_string(index=False)
- )
- return 0
- def rsi2_variant_search(
- *,
- symbol: str,
- bar: str,
- years: float,
- window_size: int,
- roundtrip_cost_on_margin: float,
- ) -> int:
- client = OkxClient()
- requested_bars = history_bars_for_years(bar, years)
- candles = get_candles_cached(client, symbol, bar, requested_bars)
- candidates = [
- build_rsi2_side_candidate(trend, long_threshold, short_threshold, exit_rsi, side_mode)
- for trend in (50, 120, 240, 480)
- for long_threshold, short_threshold in (
- (2.0, 98.0),
- (3.0, 97.0),
- (5.0, 95.0),
- )
- for exit_rsi in (45.0, 55.0)
- for side_mode in ("both", "long", "short")
- ]
- rows: list[dict[str, object]] = []
- for candidate in candidates:
- metrics = evaluate_candidate_all_windows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- rows.append(
- {
- "symbol": symbol,
- "bar": bar,
- "requested_bars": requested_bars,
- "actual_bars": len(candles),
- "first_candle": _format_ts(candles[0].ts),
- "last_candle": _format_ts(candles[-1].ts),
- **metrics,
- }
- )
- print(f"done rsi2 variant {candidate.name}")
- frame = sort_cost_results(add_cost_metrics(pd.DataFrame(rows), roundtrip_cost_on_margin))
- frame.to_csv("ultrashort-rsi2-variant-search.csv", index=False)
- print(
- frame[
- [
- "symbol",
- "bar",
- "name",
- "sample_count",
- "trades",
- "net_avg_return",
- "net_ci95_low",
- "positive_window_rate",
- "trade_win_rate",
- "avg_win_return",
- "avg_loss_return_abs",
- "payoff_ratio",
- "profit_factor",
- "max_drawdown",
- ]
- ].head(50).to_string(index=False)
- )
- return 0
- def build_best_known_candidates() -> list[Candidate]:
- bbmr_config = BBMRConfig(band_length=20, std_multiplier=2.5, bandwidth_lookback=50, stop_loss_pct=0.005)
- return [
- Candidate(
- "bbmr-l20-m2.5-sl0.005",
- 50,
- lambda candles, leverage, warmup_bars, config=bbmr_config: run_bbmr_segment(
- candles=candles,
- leverage=leverage,
- warmup_bars=warmup_bars,
- config=config,
- ),
- ),
- build_rsi2_side_candidate(50, 3.0, 97.0, 45.0, "long"),
- build_rsi2_side_candidate(50, 3.0, 97.0, 55.0, "long"),
- build_rsi2_side_candidate(240, 2.0, 98.0, 55.0, "long"),
- build_trend_rsi_bb_long_candidate(480, 20, 2.0, 5.0, 45.0, 0.005),
- build_trend_rsi_bb_long_candidate(240, 20, 2.5, 5.0, 55.0, 0.008),
- build_regime_hybrid_candidate(240, 240, 0.015, 2.0, 55.0, 2.5, 0.008),
- build_regime_hybrid_candidate(50, 240, 0.02, 3.0, 55.0, 2.5, 0.005),
- ]
- def best_total_annualized(
- *,
- symbols: tuple[str, ...],
- bar: str,
- years: float,
- roundtrip_cost_on_margin: float,
- ) -> int:
- client = OkxClient()
- rows: list[dict[str, object]] = []
- horizon_rows: list[dict[str, object]] = []
- horizons = (
- ("3y", pd.DateOffset(years=3)),
- ("1y", pd.DateOffset(years=1)),
- ("6m", pd.DateOffset(months=6)),
- ("3m", pd.DateOffset(months=3)),
- )
- for symbol in symbols:
- candles = get_candles_cached(client, symbol, bar, history_bars_for_years(bar, years))
- for candidate in build_best_known_candidates():
- result = candidate.run(candles=candles, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
- net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
- metrics = annualized_metrics_from_equity(net_equity, candles[0].ts, candles[-1].ts)
- gross_years = (candles[-1].ts - candles[0].ts) / 86_400_000 / 365
- gross_annualized = (1.0 + result.total_return) ** (1.0 / gross_years) - 1.0 if result.total_return > -1.0 else 0.0
- rows.append(
- {
- "symbol": symbol,
- "bar": bar,
- "name": candidate.name,
- "first_candle": _format_ts(candles[0].ts),
- "last_candle": _format_ts(candles[-1].ts),
- "years": gross_years,
- "trades": result.trade_count,
- "gross_total_return": result.total_return,
- "gross_annualized_return": gross_annualized,
- "gross_max_drawdown_mark_to_market": result.max_drawdown,
- **metrics,
- }
- )
- horizon_frame = recent_horizon_metrics_from_equity(net_equity, candles[-1].ts, horizons)
- for horizon_row in horizon_frame.to_dict("records"):
- horizon_rows.append(
- {
- "symbol": symbol,
- "bar": bar,
- "name": candidate.name,
- "first_candle": _format_ts(candles[0].ts),
- "last_candle": _format_ts(candles[-1].ts),
- "trades": result.trade_count,
- **horizon_row,
- }
- )
- frame = pd.DataFrame(rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
- horizon_output = pd.DataFrame(horizon_rows)
- horizon_output["horizon"] = pd.Categorical(horizon_output["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
- horizon_output = horizon_output.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
- frame.to_csv("ultrashort-best-total-annualized.csv", index=False)
- horizon_output.to_csv("ultrashort-best-horizon-returns.csv", index=False)
- print(frame.to_string(index=False))
- print("recent horizon returns")
- print(horizon_output.to_string(index=False))
- return 0
- def ma_cross_search(
- *,
- symbols: tuple[str, ...],
- bar: str,
- years: float,
- window_size: int,
- roundtrip_cost_on_margin: float,
- ) -> int:
- client = OkxClient()
- rows: list[dict[str, object]] = []
- for symbol in symbols:
- candles = get_candles_cached(client, symbol, bar, history_bars_for_years(bar, years))
- candidates = [
- build_ma_cross_candidate(fast, slow, side_mode)
- for fast, slow in ((12, 48), (20, 80), (30, 120), (50, 200), (80, 320))
- for side_mode in ("both", "long", "short")
- ]
- for candidate in candidates:
- metrics = evaluate_candidate_all_windows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- rows.append(
- {
- "symbol": symbol,
- "bar": bar,
- "actual_bars": len(candles),
- "first_candle": _format_ts(candles[0].ts),
- "last_candle": _format_ts(candles[-1].ts),
- **metrics,
- }
- )
- print(f"done ma cross {symbol} {candidate.name}")
- frame = sort_cost_results(add_cost_metrics(pd.DataFrame(rows), roundtrip_cost_on_margin))
- frame.to_csv("ultrashort-ma-cross-search.csv", index=False)
- print(
- frame[
- [
- "symbol",
- "bar",
- "name",
- "sample_count",
- "trades",
- "net_avg_return",
- "net_ci95_low",
- "positive_window_rate",
- "trade_win_rate",
- "payoff_ratio",
- "profit_factor",
- "max_drawdown",
- ]
- ].head(40).to_string(index=False)
- )
- return 0
- def trend_rsi_bb_search(
- *,
- symbols: tuple[str, ...],
- bar: str,
- years: float,
- window_size: int,
- roundtrip_cost_on_margin: float,
- ) -> int:
- client = OkxClient()
- rows: list[dict[str, object]] = []
- candidates = [
- build_trend_rsi_bb_long_candidate(trend_sma, band_length, std_multiplier, rsi_threshold, exit_rsi, stop_loss_pct)
- for trend_sma in (120, 240, 480)
- for band_length in (20, 30)
- for std_multiplier in (2.0, 2.5)
- for rsi_threshold in (2.0, 3.0, 5.0)
- for exit_rsi in (45.0, 55.0)
- for stop_loss_pct in (0.005, 0.008)
- ]
- for symbol in symbols:
- candles = get_candles_cached(client, symbol, bar, history_bars_for_years(bar, years))
- for candidate in candidates:
- metrics = evaluate_candidate_all_windows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- rows.append(
- {
- "symbol": symbol,
- "bar": bar,
- "actual_bars": len(candles),
- "first_candle": _format_ts(candles[0].ts),
- "last_candle": _format_ts(candles[-1].ts),
- **metrics,
- }
- )
- print(f"done trend rsi bb {symbol} {candidate.name}")
- frame = sort_cost_results(add_cost_metrics(pd.DataFrame(rows), roundtrip_cost_on_margin))
- frame.to_csv("ultrashort-trend-rsi-bb-search.csv", index=False)
- print(
- frame[
- [
- "symbol",
- "bar",
- "name",
- "sample_count",
- "trades",
- "net_avg_return",
- "net_ci95_low",
- "positive_window_rate",
- "trade_win_rate",
- "avg_win_return",
- "avg_loss_return_abs",
- "payoff_ratio",
- "profit_factor",
- "max_drawdown",
- ]
- ].head(50).to_string(index=False)
- )
- return 0
- def regime_analysis(
- *,
- symbols: tuple[str, ...],
- bar: str,
- years: float,
- window_size: int,
- roundtrip_cost_on_margin: float,
- ) -> int:
- client = OkxClient()
- window_frames: list[pd.DataFrame] = []
- for symbol in symbols:
- candles = get_candles_cached(client, symbol, bar, history_bars_for_years(bar, years))
- for candidate in build_best_known_candidates():
- rows = evaluate_candidate_window_rows(
- candidate=candidate,
- candles=candles,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- frame = add_market_regime_columns(candles, rows, roundtrip_cost_on_margin)
- frame.insert(0, "name", candidate.name)
- frame.insert(0, "last_candle", _format_ts(candles[-1].ts))
- frame.insert(0, "first_candle", _format_ts(candles[0].ts))
- frame.insert(0, "bar", bar)
- frame.insert(0, "symbol", symbol)
- window_frames.append(frame)
- print(f"done regime {symbol} {candidate.name} windows={len(frame)}")
- windows = pd.concat(window_frames, ignore_index=True)
- summary = summarize_regime_columns(windows)
- windows.to_csv("ultrashort-regime-windows.csv", index=False)
- summary.to_csv("ultrashort-regime-summary.csv", index=False)
- print(
- summary[
- [
- "symbol",
- "bar",
- "name",
- "regime_type",
- "regime",
- "sample_count",
- "avg_net_return",
- "median_net_return",
- "positive_window_rate",
- "avg_trades",
- "avg_market_return",
- "avg_realized_vol",
- "avg_ma240_distance",
- ]
- ].to_string(index=False)
- )
- return 0
- def eth_btc_signal_search(
- *,
- bar: str,
- years: float,
- window_size: int,
- roundtrip_cost_on_margin: float,
- ) -> int:
- client = OkxClient()
- requested_bars = history_bars_for_years(bar, years)
- eth = get_candles_cached(client, "ETH-USDT-SWAP", bar, requested_bars)
- btc = get_candles_cached(client, "BTC-USDT-SWAP", bar, requested_bars)
- eth, btc = align_pair_candles(eth, btc)
- candidates = [
- build_eth_btc_rsi_filter_candidate(eth_trend, eth_rsi, eth_exit, btc_trend, btc_momentum, btc_min_momentum)
- for eth_trend in (50, 120)
- for eth_rsi in (2.0, 3.0, 5.0)
- for eth_exit in (45.0, 55.0)
- for btc_trend in (120, 240, 480)
- for btc_momentum in (96, 240)
- for btc_min_momentum in (0.0, 0.01)
- ]
- summary_rows: list[dict[str, object]] = []
- total_rows: list[dict[str, object]] = []
- horizon_rows: list[dict[str, object]] = []
- horizons = (
- ("3y", pd.DateOffset(years=3)),
- ("1y", pd.DateOffset(years=1)),
- ("6m", pd.DateOffset(months=6)),
- ("3m", pd.DateOffset(months=3)),
- )
- for candidate in candidates:
- rows = evaluate_pair_candidate_window_rows(
- candidate=candidate,
- eth_candles=eth,
- btc_candles=btc,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
- summary_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "actual_bars": len(eth),
- "first_candle": _format_ts(eth[0].ts),
- "last_candle": _format_ts(eth[-1].ts),
- **summary,
- }
- )
- result = candidate.run(eth_candles=eth, btc_candles=btc, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
- net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
- metrics = annualized_metrics_from_equity(net_equity, eth[0].ts, eth[-1].ts)
- years_actual = (eth[-1].ts - eth[0].ts) / 86_400_000 / 365
- gross_annualized = (1.0 + result.total_return) ** (1.0 / years_actual) - 1.0 if result.total_return > -1.0 else 0.0
- total_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "name": candidate.name,
- "first_candle": _format_ts(eth[0].ts),
- "last_candle": _format_ts(eth[-1].ts),
- "years": years_actual,
- "trades": result.trade_count,
- "gross_total_return": result.total_return,
- "gross_annualized_return": gross_annualized,
- "gross_max_drawdown_mark_to_market": result.max_drawdown,
- **metrics,
- }
- )
- horizon_frame = recent_horizon_metrics_from_equity(net_equity, eth[-1].ts, horizons)
- for horizon_row in horizon_frame.to_dict("records"):
- horizon_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "name": candidate.name,
- "trades": result.trade_count,
- **horizon_row,
- }
- )
- print(f"done eth btc signal {candidate.name}")
- summary = sort_cost_results(pd.DataFrame(summary_rows))
- totals = pd.DataFrame(total_rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
- horizon = pd.DataFrame(horizon_rows)
- horizon["horizon"] = pd.Categorical(horizon["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
- horizon = horizon.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
- summary.to_csv("ultrashort-eth-btc-signal-summary.csv", index=False)
- totals.to_csv("ultrashort-eth-btc-signal-total.csv", index=False)
- horizon.to_csv("ultrashort-eth-btc-signal-horizon.csv", index=False)
- print("window summary")
- print(
- summary[
- [
- "name",
- "sample_count",
- "trades",
- "net_avg_return",
- "net_ci95_low",
- "positive_window_rate",
- "trade_win_rate",
- "payoff_ratio",
- "profit_factor",
- "max_drawdown",
- ]
- ].head(30).to_string(index=False)
- )
- print("total")
- print(totals.head(30).to_string(index=False))
- print("horizon")
- print(horizon.head(60).to_string(index=False))
- return 0
- def eth_btc_shock_filter_search(
- *,
- bar: str,
- years: float,
- window_size: int,
- roundtrip_cost_on_margin: float,
- ) -> int:
- client = OkxClient()
- requested_bars = history_bars_for_years(bar, years)
- eth = get_candles_cached(client, "ETH-USDT-SWAP", bar, requested_bars)
- btc = get_candles_cached(client, "BTC-USDT-SWAP", bar, requested_bars)
- eth, btc = align_pair_candles(eth, btc)
- candidates = [
- build_eth_btc_shock_filter_candidate(
- eth_trend,
- eth_rsi,
- eth_exit,
- btc_trend,
- btc_momentum,
- btc_min_momentum,
- btc_shock_lookback,
- btc_max_realized_vol,
- btc_max_drawdown,
- )
- for eth_trend in (50,)
- for eth_rsi in (3.0,)
- for eth_exit in (45.0, 55.0)
- for btc_trend in (480,)
- for btc_momentum in (240,)
- for btc_min_momentum in (0.0, 0.01)
- for btc_shock_lookback in (96, 240)
- for btc_max_realized_vol in (0.006, 0.01)
- for btc_max_drawdown in (0.03, 0.05, 0.08)
- ]
- summary_rows: list[dict[str, object]] = []
- total_rows: list[dict[str, object]] = []
- horizon_rows: list[dict[str, object]] = []
- horizons = (
- ("3y", pd.DateOffset(years=3)),
- ("1y", pd.DateOffset(years=1)),
- ("6m", pd.DateOffset(months=6)),
- ("3m", pd.DateOffset(months=3)),
- )
- for candidate in candidates:
- rows = evaluate_pair_candidate_window_rows(
- candidate=candidate,
- eth_candles=eth,
- btc_candles=btc,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
- summary_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "actual_bars": len(eth),
- "first_candle": _format_ts(eth[0].ts),
- "last_candle": _format_ts(eth[-1].ts),
- **summary,
- }
- )
- result = candidate.run(eth_candles=eth, btc_candles=btc, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
- net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
- metrics = annualized_metrics_from_equity(net_equity, eth[0].ts, eth[-1].ts)
- years_actual = (eth[-1].ts - eth[0].ts) / 86_400_000 / 365
- gross_annualized = (1.0 + result.total_return) ** (1.0 / years_actual) - 1.0 if result.total_return > -1.0 else 0.0
- total_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "name": candidate.name,
- "first_candle": _format_ts(eth[0].ts),
- "last_candle": _format_ts(eth[-1].ts),
- "years": years_actual,
- "trades": result.trade_count,
- "gross_total_return": result.total_return,
- "gross_annualized_return": gross_annualized,
- "gross_max_drawdown_mark_to_market": result.max_drawdown,
- **metrics,
- }
- )
- horizon_frame = recent_horizon_metrics_from_equity(net_equity, eth[-1].ts, horizons)
- for horizon_row in horizon_frame.to_dict("records"):
- horizon_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "name": candidate.name,
- "trades": result.trade_count,
- **horizon_row,
- }
- )
- print(f"done eth btc shock filter {candidate.name}")
- summary = sort_cost_results(pd.DataFrame(summary_rows))
- totals = pd.DataFrame(total_rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
- horizon = pd.DataFrame(horizon_rows)
- horizon["horizon"] = pd.Categorical(horizon["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
- horizon = horizon.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
- summary.to_csv("ultrashort-eth-btc-shock-filter-summary.csv", index=False)
- totals.to_csv("ultrashort-eth-btc-shock-filter-total.csv", index=False)
- horizon.to_csv("ultrashort-eth-btc-shock-filter-horizon.csv", index=False)
- print("window summary")
- print(
- summary[
- [
- "name",
- "sample_count",
- "trades",
- "net_avg_return",
- "net_ci95_low",
- "positive_window_rate",
- "trade_win_rate",
- "payoff_ratio",
- "profit_factor",
- "max_drawdown",
- ]
- ].head(30).to_string(index=False)
- )
- print("total")
- print(totals.head(30).to_string(index=False))
- print("horizon")
- print(horizon.head(60).to_string(index=False))
- return 0
- def eth_btc_ratio_search(
- *,
- bar: str,
- years: float,
- window_size: int,
- roundtrip_cost_on_margin: float,
- ) -> int:
- client = OkxClient()
- requested_bars = history_bars_for_years(bar, years)
- eth = get_candles_cached(client, "ETH-USDT-SWAP", bar, requested_bars)
- btc = get_candles_cached(client, "BTC-USDT-SWAP", bar, requested_bars)
- eth, btc = align_pair_candles(eth, btc)
- candidates = [
- build_eth_btc_ratio_pullback_candidate(btc_trend, btc_momentum, btc_min_momentum, ratio_length, ratio_std, ratio_rsi, stop)
- for btc_trend in (480,)
- for btc_momentum in (96, 240)
- for btc_min_momentum in (0.0, 0.01)
- for ratio_length in (48, 96)
- for ratio_std in (1.5, 2.0)
- for ratio_rsi in (5.0,)
- for stop in (0.005, 0.008)
- ]
- summary_rows: list[dict[str, object]] = []
- total_rows: list[dict[str, object]] = []
- horizon_rows: list[dict[str, object]] = []
- horizons = (
- ("3y", pd.DateOffset(years=3)),
- ("1y", pd.DateOffset(years=1)),
- ("6m", pd.DateOffset(months=6)),
- ("3m", pd.DateOffset(months=3)),
- )
- for candidate in candidates:
- rows = evaluate_pair_candidate_window_rows(
- candidate=candidate,
- eth_candles=eth,
- btc_candles=btc,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
- summary_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "actual_bars": len(eth),
- "first_candle": _format_ts(eth[0].ts),
- "last_candle": _format_ts(eth[-1].ts),
- **summary,
- }
- )
- result = candidate.run(eth_candles=eth, btc_candles=btc, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
- net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
- metrics = annualized_metrics_from_equity(net_equity, eth[0].ts, eth[-1].ts)
- years_actual = (eth[-1].ts - eth[0].ts) / 86_400_000 / 365
- gross_annualized = (1.0 + result.total_return) ** (1.0 / years_actual) - 1.0 if result.total_return > -1.0 else 0.0
- total_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "name": candidate.name,
- "first_candle": _format_ts(eth[0].ts),
- "last_candle": _format_ts(eth[-1].ts),
- "years": years_actual,
- "trades": result.trade_count,
- "gross_total_return": result.total_return,
- "gross_annualized_return": gross_annualized,
- "gross_max_drawdown_mark_to_market": result.max_drawdown,
- **metrics,
- }
- )
- horizon_frame = recent_horizon_metrics_from_equity(net_equity, eth[-1].ts, horizons)
- for horizon_row in horizon_frame.to_dict("records"):
- horizon_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "name": candidate.name,
- "trades": result.trade_count,
- **horizon_row,
- }
- )
- print(f"done eth btc ratio {candidate.name}")
- summary = sort_cost_results(pd.DataFrame(summary_rows))
- totals = pd.DataFrame(total_rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
- horizon = pd.DataFrame(horizon_rows)
- horizon["horizon"] = pd.Categorical(horizon["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
- horizon = horizon.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
- summary.to_csv("ultrashort-eth-btc-ratio-summary.csv", index=False)
- totals.to_csv("ultrashort-eth-btc-ratio-total.csv", index=False)
- horizon.to_csv("ultrashort-eth-btc-ratio-horizon.csv", index=False)
- print("window summary")
- print(
- summary[
- [
- "name",
- "sample_count",
- "trades",
- "net_avg_return",
- "net_ci95_low",
- "positive_window_rate",
- "trade_win_rate",
- "payoff_ratio",
- "profit_factor",
- "max_drawdown",
- ]
- ].head(30).to_string(index=False)
- )
- print("total")
- print(totals.head(30).to_string(index=False))
- print("horizon")
- print(horizon.head(60).to_string(index=False))
- return 0
- def btc_lead_eth_lag_search(
- *,
- bar: str,
- years: float,
- window_size: int,
- roundtrip_cost_on_margin: float,
- ) -> int:
- client = OkxClient()
- requested_bars = history_bars_for_years(bar, years)
- eth = get_candles_cached(client, "ETH-USDT-SWAP", bar, requested_bars)
- btc = get_candles_cached(client, "BTC-USDT-SWAP", bar, requested_bars)
- eth, btc = align_pair_candles(eth, btc)
- candidates = [
- build_btc_lead_eth_lag_candidate(lead_lookback, btc_return_threshold, lag_gap, max_hold_bars, stop_loss_pct, take_profit_pct)
- for lead_lookback in (8, 16)
- for btc_return_threshold in (0.012, 0.018, 0.024)
- for lag_gap in (0.006, 0.012)
- for max_hold_bars in (8, 32)
- for stop_loss_pct in (0.006, 0.008)
- for take_profit_pct in (0.012, 0.018)
- ]
- summary_rows: list[dict[str, object]] = []
- total_rows: list[dict[str, object]] = []
- horizon_rows: list[dict[str, object]] = []
- horizons = (
- ("3y", pd.DateOffset(years=3)),
- ("1y", pd.DateOffset(years=1)),
- ("6m", pd.DateOffset(months=6)),
- ("3m", pd.DateOffset(months=3)),
- )
- for candidate in candidates:
- rows = evaluate_pair_candidate_window_rows(
- candidate=candidate,
- eth_candles=eth,
- btc_candles=btc,
- window_size=window_size,
- leverage=LEVERAGE,
- )
- summary = add_cost_metrics(pd.DataFrame([summarize_window_rows(rows, candidate.name)]), roundtrip_cost_on_margin).iloc[0].to_dict()
- summary_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "actual_bars": len(eth),
- "first_candle": _format_ts(eth[0].ts),
- "last_candle": _format_ts(eth[-1].ts),
- **summary,
- }
- )
- result = candidate.run(eth_candles=eth, btc_candles=btc, leverage=LEVERAGE, warmup_bars=candidate.warmup_bars)
- net_equity = cost_adjusted_trade_equity_frame(result, roundtrip_cost_on_margin)
- metrics = annualized_metrics_from_equity(net_equity, eth[0].ts, eth[-1].ts)
- years_actual = (eth[-1].ts - eth[0].ts) / 86_400_000 / 365
- gross_annualized = (1.0 + result.total_return) ** (1.0 / years_actual) - 1.0 if result.total_return > -1.0 else 0.0
- total_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "name": candidate.name,
- "first_candle": _format_ts(eth[0].ts),
- "last_candle": _format_ts(eth[-1].ts),
- "years": years_actual,
- "trades": result.trade_count,
- "gross_total_return": result.total_return,
- "gross_annualized_return": gross_annualized,
- "gross_max_drawdown_mark_to_market": result.max_drawdown,
- **metrics,
- }
- )
- horizon_frame = recent_horizon_metrics_from_equity(net_equity, eth[-1].ts, horizons)
- for horizon_row in horizon_frame.to_dict("records"):
- horizon_rows.append(
- {
- "symbol": "ETH-USDT-SWAP",
- "signal_symbol": "BTC-USDT-SWAP",
- "bar": bar,
- "name": candidate.name,
- "trades": result.trade_count,
- **horizon_row,
- }
- )
- print(f"done btc lead eth lag {candidate.name}")
- summary = sort_cost_results(pd.DataFrame(summary_rows))
- totals = pd.DataFrame(total_rows).sort_values(["net_calmar", "net_annualized_return"], ascending=False)
- horizon = pd.DataFrame(horizon_rows)
- horizon["horizon"] = pd.Categorical(horizon["horizon"], categories=["3y", "1y", "6m", "3m"], ordered=True)
- horizon = horizon.sort_values(["horizon", "net_annualized_return"], ascending=[True, False])
- output_prefix = f"ultrashort-btc-lead-eth-lag-{bar}"
- summary.to_csv(f"{output_prefix}-summary.csv", index=False)
- totals.to_csv(f"{output_prefix}-total.csv", index=False)
- horizon.to_csv(f"{output_prefix}-horizon.csv", index=False)
- print("window summary")
- print(
- summary[
- [
- "name",
- "sample_count",
- "trades",
- "net_avg_return",
- "net_ci95_low",
- "positive_window_rate",
- "trade_win_rate",
- "payoff_ratio",
- "profit_factor",
- "max_drawdown",
- ]
- ].head(30).to_string(index=False)
- )
- print("total")
- print(totals.head(30).to_string(index=False))
- print("horizon")
- print(horizon.head(60).to_string(index=False))
- return 0
- if __name__ == "__main__":
- parser = argparse.ArgumentParser()
- parser.add_argument("--focus-vwap", action="store_true")
- parser.add_argument("--robust-vwap", action="store_true")
- parser.add_argument("--robust-all", action="store_true")
- parser.add_argument("--rsi2-cost-search", action="store_true")
- parser.add_argument("--rsi2-period-analysis", action="store_true")
- parser.add_argument("--strategy-timeframe-analysis", action="store_true")
- parser.add_argument("--rsi2-variant-search", action="store_true")
- parser.add_argument("--best-total-annualized", action="store_true")
- parser.add_argument("--ma-cross-search", action="store_true")
- parser.add_argument("--trend-rsi-bb-search", action="store_true")
- parser.add_argument("--regime-analysis", action="store_true")
- parser.add_argument("--eth-btc-signal-search", action="store_true")
- parser.add_argument("--eth-btc-shock-filter-search", action="store_true")
- parser.add_argument("--eth-btc-ratio-search", action="store_true")
- parser.add_argument("--btc-lead-eth-lag-search", action="store_true")
- parser.add_argument("--history-limit", type=int, default=ROBUST_HISTORY_LIMIT)
- parser.add_argument("--window-size", type=int, default=WINDOW_SIZE)
- parser.add_argument("--roundtrip-cost-on-margin", type=float, default=0.0012)
- parser.add_argument("--symbol", default="BTC-USDT-SWAP")
- parser.add_argument("--symbols", default="BTC-USDT-SWAP")
- parser.add_argument("--bar", default="15m")
- parser.add_argument("--bars", default=",".join(ANALYSIS_BARS))
- parser.add_argument("--years", type=float, default=10.0)
- parser.add_argument("--period", default="Q")
- parser.add_argument("--rsi2-trend", type=int, default=50)
- parser.add_argument("--rsi2-long-threshold", type=float, default=3.0)
- parser.add_argument("--rsi2-short-threshold", type=float, default=97.0)
- args = parser.parse_args()
- if args.rsi2_period_analysis:
- raise SystemExit(
- rsi2_period_analysis(
- symbol=args.symbol,
- bar=args.bar,
- history_limit=args.history_limit,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- trend=args.rsi2_trend,
- long_threshold=args.rsi2_long_threshold,
- short_threshold=args.rsi2_short_threshold,
- )
- )
- if args.rsi2_cost_search:
- raise SystemExit(robust_rsi2_cost_search(args.history_limit, args.window_size, args.roundtrip_cost_on_margin))
- if args.best_total_annualized:
- raise SystemExit(
- best_total_annualized(
- symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
- bar=args.bar,
- years=args.years,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- )
- )
- if args.ma_cross_search:
- raise SystemExit(
- ma_cross_search(
- symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
- bar=args.bar,
- years=args.years,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- )
- )
- if args.trend_rsi_bb_search:
- raise SystemExit(
- trend_rsi_bb_search(
- symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
- bar=args.bar,
- years=args.years,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- )
- )
- if args.regime_analysis:
- raise SystemExit(
- regime_analysis(
- symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
- bar=args.bar,
- years=args.years,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- )
- )
- if args.eth_btc_signal_search:
- raise SystemExit(
- eth_btc_signal_search(
- bar=args.bar,
- years=args.years,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- )
- )
- if args.eth_btc_shock_filter_search:
- raise SystemExit(
- eth_btc_shock_filter_search(
- bar=args.bar,
- years=args.years,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- )
- )
- if args.eth_btc_ratio_search:
- raise SystemExit(
- eth_btc_ratio_search(
- bar=args.bar,
- years=args.years,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- )
- )
- if args.btc_lead_eth_lag_search:
- raise SystemExit(
- btc_lead_eth_lag_search(
- bar=args.bar,
- years=args.years,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- )
- )
- if args.rsi2_variant_search:
- raise SystemExit(
- rsi2_variant_search(
- symbol=args.symbol,
- bar=args.bar,
- years=args.years,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- )
- )
- if args.strategy_timeframe_analysis:
- raise SystemExit(
- strategy_timeframe_analysis(
- symbols=tuple(value.strip() for value in args.symbols.split(",") if value.strip()),
- bars=tuple(value.strip() for value in args.bars.split(",") if value.strip()),
- years=args.years,
- window_size=args.window_size,
- roundtrip_cost_on_margin=args.roundtrip_cost_on_margin,
- period=args.period,
- )
- )
- if args.robust_all:
- raise SystemExit(robust_all(args.history_limit, args.window_size))
- if args.robust_vwap:
- raise SystemExit(robust_vwap(args.history_limit, args.window_size))
- raise SystemExit(focus_vwap() if args.focus_vwap else main())
|