Backtest tezligi zinapoyasi: laptop CPU'sida 298x, oxirgi bitimigacha bir xil PnL
"Illyuziyasiz backtestlar" turkumining bir qismi.
📄 Ushbu maqola tadqiqot ishiga aylandi. Bitta yo'l-bog'liq (path-dependent) backtest yadrosi besh xil usulda implementatsiya qilingan — sodda pandas'dan tortib parallel numba yadrosigacha — har bir pog'ona bir xil per-combo PnL ishlab chiqarishi tekshirilgan, shuning uchun farq qiladigan yagona narsa bu tezlik. Ishni onlayn o'qing (interaktiv versiya + PDF) speed-ladder.marketmaker.cc manzilida, kod va ma'lumotlar esa github.com/suenot/backtest-speed-ladder da.
Yetmish soniya. Sodda referens implementatsiyasi bitta moving-average strategiyasining 80 parametr kombinatsiyasini 150,000 barda sweep qilishga aynan shuncha vaqt sarflaydi: indikatorlar uchun pandas rolling().apply(), bitimlar uchun oddiy Python tsikli. Bu real dunyodagi tadqiqot kodining katta qismi ishlaydigan profil, chunki bu strategiyani eng ravshan yo'l bilan yozishdan kelib chiqadigan profildir.
Aynan o'sha sweep, aynan o'sha laptopda, har bir kombinatsiya uchun oxirgi bitimigacha bir xil PnL ishlab chiqaradi: 0.23 soniya.
Bu ikki raqam orasidagi tafovut — o'lchangan 298x — ushbu maqolaning mavzusidir. Uning birorta ham foizi yangi apparatdan kelmadi. GPU jalb qilinmadi (CUDA ma'nosida bu mashinada umuman mavjud emas). Zinapoyaning har bir pog'onasi bir xil strategiya, bir xil ma'lumot, bir xil komissiyalar, bir xil bitimlar soni bo'lib, agar biror implementatsiyaning per-combo natijalari farq qilsa, butun benchmark'ni ishdan chiqaradigan ekvivalentlik darvozasi orqali tasdiqlangan. O'zgargan yagona narsa bu ish qanday ifodalanishi: interpretatorda nima ishlaydi, kompilyatsiya qilingan holda nima ishlaydi, va parallel ravishda nima ishlaydi. Va ataylab sekin bazaviy variant har qanday sarlavha raqamini bezashi mumkin bo'lgani uchun, oldindan yana bitta ko'rsatkich: hatto layoqatli vektorlashtirilgan numpy implementatsiyasiga qarshi ham — kuchli numpy dasturchisi yetkazadigan kod — tayyor dvigatel hali ham taxminan 13x tezroq.
Parametr qidiruvi sekin bo'lganda, refleks kattaroq apparatga qo'l cho'zishdir — GPU, klaster, bulut byudjeti. Ushbu tajribaning o'lchangan haqiqati ancha kamroq maftunkor joyga ishora qiladi: bo'g'ilish dvigatelda edi (interpretatsiya qilingan ichki tsikl har bir oyna uchun Python chaqiruvlarini bajaradi) va orkestratsiyada (mustaqil kombinatsiyalarni bitta yadroda ketma-ket ishga tushirish). Ikkalasi ham natijalarga hech qanday o'zgartirishsiz, sizda allaqachon bor mashinada, bir kunduzda tuzatib bo'ladigan narsalar.
Butun zinapoya oldindan mana shu yerda. Quyidagi barcha narsa har bir qadamning anatomiyasidir.
| Pog'ona | Implementatsiya | Devor vaqti | Tezlashuv | Kombo/s |
|---|---|---|---|---|
| M0 | pandas: rolling.apply + Python bar tsikli |
69.92 s | 1.0x | 1.1 |
| M1 | numpy: sliding-window WMA + vektorlashtirilgan bitimlar | 3.07 s | 22.7x | 26.0 |
| M2 | numba: @njit WMA + @njit event tsikli |
1.98 s | 35.3x | 40.4 |
| M3 | numba prange: kombinatsiyalar bo'ylab threadlar |
0.32 s | 217.6x | 248.9 |
| M4 | process pool + numba: kombinatsiyalar bo'ylab jarayonlar | 0.23 s | 297.9x | 340.9 |
Apple M2 Max (12 yadro), Python 3.14.6, numpy 2.4.3, numba 0.64.0, BLAS (Accelerate) bitta threadga qadalgan, shuning uchun bir-threadli pog'onalar chindan ham bir-yadroli. 150,000 bar × 80 kombinatsiya, best-of-3 devor vaqti, JIT isinish istisno qilingan. Barcha pog'onalar — pandas bazaviy varianti ham — to'liq o'lchangan va barcha 80 kombinatsiyada bir xil per-combo PnL va bitimlar sonini ishlab chiqarishi tasdiqlangan.
Bitta yadro, beshta implementatsiya

Tezlik taqqoslashi biror ma'no anglatishi uchun, hisoblanayotgan narsa aniq qadalgan bo'lishi kerak, va har bir implementatsiya uni hisoblashi isbotlangan bo'lishi kerak. Shuning uchun tajriba bitta strategiya yadrosini o'rnatadi va uni barcha beshta pog'ona bo'ylab o'zgarmas saqlaydi.
Yadro HMA/HMA3 cross — ikkita Hull uslubidagi moving average asosidagi stop-and-reverse tizim. Qurilish bloki bu weighted moving average:
Hull Moving Average kechikishni kamaytirish uchun ularning uchtasini birlashtiradi:
va HMA3 taxminan , va dagi WMA'lardan qurilgan, yana bir bor silliqlangan yumshoqroq qarindoshdir. Har bir parametr kombinatsiyasi uchun bu oltita alohida oyna uzunligi bo'yicha yettita WMA o'tishi — o'yinchoq emas, real indikator steki.
Savdo qoidasi ataylab, foydali tarzda holatli (stateful): HMA HMA3 dan past bo'lganda yo'nalish long, aks holda short; birinchi aniqlangan yo'nalishda pozitsiya ochish; har bir cross'da pozitsiyani yopish, PnL'ni 0.09% round-trip komissiyasini chegirib qayd qilish, va teskarilash. Pozitsiya barlar bo'ylab ko'chib yuradi — bar da nima qilishingiz oxirgi cross'dan beri to'plangan holatga bog'liq. Ushbu yo'l-bog'liqlik tajribaning butun mohiyati: bu backtestlarni umumiy dataframe pipeline'laridan farqli qiladigan xususiyat, va (biz o'lchaganidek) u GPU savolini murakkablashtiradi — garchi, ma'lum bo'lishicha, folklor aytadigan tarzda emas.
Qolgan sozlamalar, siz raqamlarni baholay olishingiz uchun:
- Ma'lumot: 150,000 bar sintetik geometrik Brownian harakat, seedli (
seed=42). Bu yerda unumdorlik massiv o'lchami va oyna uzunliklariga bog'liq, uni qaysi narx yo'li bilan oziqlantirishingizga emas — va sintetik seriya butun tajribani determinlangan va har kim tomonidan takrorlanadigan qiladi. - Grid: oralig'ida tarqalgan 80 alohida HMA uzunligi — shuning uchun sweep ham arzon qisqa-oynali kombinatsiyalarni, ham qimmat uzun-oynali kombinatsiyalarni o'z ichiga oladi, xuddi real grid kabi.
- Vaqt o'lchash: devor-soati, har bir pog'ona uchun best-of-3, JIT kompilyatsiya taymerdan tashqarida isitiladi va pool workerlar soat boshlanishidan oldin isitiladi. Har bir pog'ona — pandas bazaviy varianti ham — barcha 80 kombinatsiya bo'ylab to'liq o'lchanadi. BLAS (Apple'ning Accelerate'i) bitta threadga qadalgan, shuning uchun bir-threadli pog'onalar chindan ham bir-yadroli: numpy pog'onasi taqqoslash ortida yashirincha o'zining matvec'larini multithreading qilmayapti.
- Ekvivalentlik darvozasi: vaqt o'lchashdan keyin, har bir pog'onaning per-combo (PnL, bitimlar soni) vektori referensga qarshi taqqoslanadi — bitimlar soni aniq mos kelishi kerak, PnL esa absolyut foiz nuqtasi ichida. Qayd qilingan run har bir pog'ona uchun
all_ok: truexabar beradi, pandas bazaviy varianti ham, barcha 80 kombinatsiyada. Agar ushbu darvoza muvaffaqiyatsiz bo'lsa, benchmark yo'q — faqat besh xil narsani besh xil tezlikda hisoblayotgan beshta dastur bor, "bizning dvigatelimiz 100x tezroq" da'volarining ko'pchiligi jimgina shunday ishlaydi.
Ekvivalentlik blokidan bitta raqam bir lahza halollikka arziydi: birinchi kombinatsiya uchun barmoq izi 57,029 bitim bo'ylab −5165.58 foiz nuqtasi PnL'dir. Bu uyaladigan strategiya natijasi emas — bu eng qisqa HMA uzunligi (6) tasodifiy yurishning deyarli har bir chayqalishida aylanib, har safar 0.09% to'laydi, xuddi kerak bo'lganidek. Bu to'g'rilik barmoq izi, savdoga yaroqli backtest emas. Undan alpha o'qimang; undan determinlashuvni o'qing — beshta implementatsiya bir xil 57,029 bitimga va oltita o'nlik kasrgacha bir xil PnL'ga tushishi bu yerda "bir xil" degan ma'noni anglatadi.
Buni o'rnatgach, quyidagi har bir tezlashuv sof tezlik. Hech narsa taxminan tashlanib yuborilmadi.
Pog'ona M0: sodda pandas profili — 69.9 s

Bazaviy variant xas-cho'p emas. Bu siz WMA'ni pandas hujjatlari taklif qiladigan tarzda va event tsiklini strategiya tavsifi o'qilgan tarzda yozganingizda oladigan koddir:
def pd_wma(s: pd.Series, period: int) -> np.ndarray:
w = np.arange(1, period + 1, dtype=np.float64)
w /= w.sum()
return s.rolling(period).apply(lambda x: np.dot(x, w), raw=True).to_numpy()
def run_pandas_one(close, length):
h, h3 = pd_hma(close, length), pd_hma3(close, length) # 7 rolling.apply WMAs
total, ntr, prev_dir, entry, pos = 0.0, 0, 0, 0.0, 0
for i in range(len(close)): # Python bar loop
if np.isnan(h[i]) or np.isnan(h3[i]):
continue
d = 1 if h[i] < h3[i] else -1
if prev_dir == 0:
prev_dir, pos, entry = d, d, close[i]
continue
if d != prev_dir: # cross: close + reverse
pnl = ((close[i] - entry) if pos == 1
else (entry - close[i])) / entry * 100 - FEE
total += pnl
ntr += 1
pos, entry, prev_dir = d, close[i], d
return total, ntr
Nega bu sekin? pandas "yomon" bo'lgani uchun emas — balki iteratsiya qayerda yashagani uchun. rolling(period).apply(lambda ...) bu vektorlashtirilgan kostyum kiygan Python-darajasidagi tsikldir. 150,000 barning har biri uchun pandas oynani materializatsiya qiladi, C/Python chegarasini kesib o'tadi, Python chaqiriladigan obyektini chaqiradi, va natijani qutiga soladi. Hatto raw=True bilan ham (bu hech bo'lmaganda lambda'ga Series o'rniga yalang'och ndarray beradi), har bir chaqiruv interpretator overhead'i oyna aslida talab qiladigan ~o'nlab-yuzlab FLOP'larni juda ortda qoldiradi. Buni har bir kombinatsiya uchun yettita WMA o'tishiga ko'paytiring, va faqat indikator steki millionlab interpretator borish-kelishi bo'ladi. Keyin bar tsikli har bir kombinatsiya uchun yana 150,000 interpretatsiya qilingan iteratsiyani ishga tushiradi, har biri numpy skalyarlarida chegara-tekshirilgan indekslash qiladi, float'larni qutiga soladi, va interpretator har safar qaytadan kashf qiladigan turlar bo'yicha dinamik ravishda dispatch qiladi.
Natija: sweep uchun 69.92 s, har bir kombinatsiya uchun taxminan 0.87 s, sekundiga 1.1 kombinatsiya o'tkazuvchanligi. 80-kombinatsiyali gridda yelka qisib bir daqiqa kutasiz. Muammo shundaki, hech kim uzoq vaqt 80-kombinatsiyali grid ishlatmaydi — va bu narx abadiy chiziqli o'sadi. Bunga qaytamiz.
Pog'ona M1: numpy — tsiklda Python chaqirishni to'xtating — 3.07 s, 22.7x
Birinchi pog'ona ikkala interpretator tsiklini bir vaqtning o'zida yo'q qiladi, va ikkala hiylani ajratib ko'rsatishga arziydi, chunki ular juda xilma-xil umumiylikka ega.
Indikator tomoni oson, to'liq umumiy bo'lganidir. Barcha oynalar bo'yicha weighted moving average bu kirishning strided ko'rinishiga qarshi shunchaki matritsa–vektor ko'paytmasidir — nusxalarsiz, bitta BLAS chaqiruvi:
def vec_wma(x: np.ndarray, period: int) -> np.ndarray:
w = np.arange(1, period + 1, dtype=np.float64)
win = np.lib.stride_tricks.sliding_window_view(x, period) # zero-copy view
out = np.full(len(x), np.nan)
out[period - 1:] = win @ w / w.sum() # one matvec
return out
sliding_window_view bir xil xotiraning (n − p + 1, p) ko'rinishini quradi, va win @ w har bir oynaning skalyar ko'paytmasini kompilyatsiya qilingan kodda hisoblaydi. Million lambda chaqiruvi bitta kutubxona chaqiruviga aylanadi.
Savdo tomoni qiziqarli bo'lganidir, chunki event tsikli holatli — va shunga qaramay, ushbu yadro uchun u vektorlashadi. Idrok shundaki, istalgan bardagi pozitsiya faqat HMA − HMA3 belgisiga bog'liq, biror bitim natijasiga emas. Holat hech qachon qarorlarga qayta oziqlanmaydi. Shuning uchun butun tsikl "belgi almashishlarini top, o'sha indekslardagi narxlarni yig'" ga qulaydi:
d = np.where(h[idx] < h3[idx], 1, -1) # direction per valid bar
flips = np.flatnonzero(np.diff(d) != 0) + 1 # bars where it crosses
cross = idx[np.concatenate(([0], flips))] # entry/exit indices
side = d[np.concatenate(([0], flips))]
entries, exits, s = close[cross[:-1]], close[cross[1:]], side[:-1]
pnl = np.where(s == 1, (exits - entries) / entries,
(entries - exits) / entries) * 100 - FEE
return float(pnl.sum()), int(pnl.size)
3.07 s, 22.7x tezlashuv, sekundiga 26.0 kombinatsiya — bitta yadroda, BLAS bitta threadga qadalgan holda. Ushbu pog'ona yorliqqa loyiq: bu layoqatli bazaviy variant, kuchli numpy dasturchisi yetkazadigan implementatsiya, va undan yuqoridagi hamma narsa uchun adolatli o'lchov. Ammo ushbu pog'onaga ikkita halol ogohlantirish hamroh bo'ladi.
Birinchidan, bu vektorlashtirish strategiyaga xos analitik qayta yozish, mexanik transformatsiya emas. U yadro stop-and-reverse bo'lgani uchun mavjud — stoplarsiz, trailing chiqishlarsiz, joriy PnL'ga bog'liq pozitsiya o'lchamsiz. Stop-loss qo'shing — tasavvur qilinadigan eng oddiy xususiyat — va bar dagi chiqish bar da qaysi kirish mavjudligini o'zgartiradi, holat yo'lga qayta oziqlanadi, va yopiq shakl bug'lanib ketadi. Ko'pchilik production yadrolar o'sha chiziqning noto'g'ri tomonida yashaydi.
Ikkinchidan, bu to'g'rilik o'ladigan pog'ona. Flip-indeks hisob-kitobi (bu yerda +1, u yerda [:-1], birinchi-yo'nalish seedlanishi) aynan off-by-one ijro xatolarini keltirib chiqaradigan kod turidir — bizning look-ahead taksonomiyamiz ko'rsatgan, shovqindan 15 li Sharpe ishlab chiqarishi mumkin bo'lgan bir xil turdagi xato. Ekvivalentlik darvozasi ushbu pog'onada formallik emas; unga ishonishning yagona sababi shu. Ahmoqona referens implementatsiyaga qarshi ekvivalentlik tekshiruvisiz aqlli vektorlashtirilgan qayta yozishlar dvigatellarning o'zi test qildim deb da'vo qilgan strategiyadan chetga siljishi shundaydir.
Pog'ona M2: numba — siz aslida yozmoqchi bo'lgan tsiklni kompilyatsiya qiling — 1.98 s, 35.3x

M2 pog'onasi qarama-qarshi falsafani oladi: algoritmni vektorlashtirilgan primitivlarga moslashtirish uchun buramaslik, balki sodda tsikllarni yozing — va ularni kompilyatsiya qiling. Numba (Lam, Pitrou & Seibert, 2015) Python'ning raqamli qism to'plamini LLVM orqali mashina kodiga JIT-kompilyatsiya qiladi:
@njit(cache=True)
def nb_wma(x, period):
n = x.shape[0]
out = np.full(n, np.nan)
wsum = period * (period + 1) / 2.0
for i in range(period - 1, n): # the "slow" loop, now machine code
s = 0.0
for j in range(period):
s += x[i - period + 1 + j] * (j + 1)
out[i] = s / wsum
return out
@njit(cache=True)
def nb_sweep(close, half, full, sq, p3, p2, pi, fee):
h = nb_wma(2.0 * nb_wma(close, half) - nb_wma(close, full), sq)
a = 3.0 * nb_wma(close, p3) - nb_wma(close, p2) - nb_wma(close, pi)
h3 = nb_wma(a, pi)
nb_sweep ichidagi event tsikli matnli ravishda M0 tsikli. Branchlar, continue, lokallarda ko'chib yuradigan holat — hammasi. @njit ostida bu lokallar registrlarda yashaydi, branchlar haqiqiy jump instruksiyalari, va har bir iteratsiya narxi interpretator dispatch'ining mikrosekundlaridan nanosekundlargacha tushadi.
1.98 s — pandas'dan 35.3x, ammo numpy'dan atigi taxminan 1.6x (chiqarilgan: 3.07/1.98). Bu kamtarona qadam o'zi ibratlidir: numpy'ning ichki tsikllari allaqachon kompilyatsiya qilingan edi, shuning uchun numba'ning xususiyat matematikasidagi yutug'i oyna materializatsiyasi va oraliq massivlarni o'tkazib yuborish bilan cheklangan. Transformatsion qism boshqa joyda:
- Event tsikli endi bepul — va "bepul" ritorika emas, o'lchangan. M1 o'z zukkoligini savdo mantig'ini vektorlashtiriladigan qilishga sarfladi. M2 o'sha zukkolikni keraksiz qiladi — sodda, auditga yaroqli, o'zgartirish oson tsikl mashina tezligida ishlaydi. Ushbu kompilyatsiya qilingan yadro ichida xususiyat bosqichini savdo tsiklidan alohida o'lchash uning vaqtining 99.3% ini WMA xususiyat matematikasiga va atigi 0.7% ini holatli event tsikliga bog'laydi. Ertaga tadqiqot loyihasisiz stop-loss qo'shishingiz mumkin — va bu bo'linishni yodda tuting; u quyidagi GPU argumentini qayta hal qiladi.
- U keyingi ikkita pog'onani ochadi. Kompilyatsiya qilingan, GIL-bo'shatuvchi, kam-allokatsiyali yadro parallel orkestratsiya talab qiladigan ish birligidir. M0'ni samarali parallel qilib bo'lmaydi — sekinning o'n ikki nusxasi hali ham sekin, faqat issiqroq.
Bitta metodologik izoh: numba birinchi chaqiruvda kompilyatsiya qiladi, va o'sha kompilyatsiya (yuzlab millisekundlar) taymer ichida bo'lmasligi kerak — harness o'lchashdan oldin JIT'ni 500-barli bo'lakda isitadi, va cache=True kompilyatsiya qilingan yadrolarni jarayon ishga tushirishlari bo'ylab saqlaydi. Ushbu tafsilotni "unutadigan" benchmarklar yo adolatsiz yomon (sovuq kompilyatsiya kiritilgan), yo takrorlanmaydigan numba raqamlarini ishlab chiqaradi.
Pog'ona M3: prange — sizda allaqachon bor bo'lgan parallellik — 0.32 s, 217.6x

Ommaviy parametr qidiruvini alohida qiladigan kuzatuv mana shu: 80 kombinatsiya butunlay mustaqil. Umumiy holat yo'q, tartib yo'q, aloqa yo'q. Bu M0–M2 pog'onalar sof odat tufayli o'n ikkitadan bitta yadroda ishga tushirayotgan uyaltiruvchi darajada parallel ishdir.
Numba tuzatishni deyarli sintaktik qiladi — kombinatsiya tsiklining range'ini prange'ga almashtiring:
@njit(parallel=True, cache=True)
def nb_sweep_all(close, params, fee):
N = params.shape[0]
totals = np.empty(N, dtype=np.float64)
ntrs = np.empty(N, dtype=np.int64)
for k in prange(N): # threads across combos
t, ntr = nb_sweep(close, params[k, 0], params[k, 1], params[k, 2],
params[k, 3], params[k, 4], params[k, 5], fee)
totals[k] = t
ntrs[k] = ntr
return totals, ntrs
nb_sweep nopython-kompilyatsiya qilingani uchun, u GIL ushlamaydi, va numba'ning threading qatlami iteratsiyalarni barcha 12 yadro bo'ylab tarqatadi. Faqat-o'qish uchun close massivi barcha threadlar tomonidan nol narxda ulashiladi.
0.32 s — pandas'dan 217.6x, sekundiga 248.9 kombinatsiya. Bir-threadli M2 ustidagi qadam 12 yadroda taxminan 6.2x (chiqarilgan: 1.98/0.32), va "ideal 12x" dan kamomad yashirish o'rniga halol bo'lishga arziydi: M2 Max'ning 12 yadrosi 8 performance + 4 efficiency yadro, shuning uchun nominal shift hech qachon 12x bo'lmagan; 80 kombinatsiyaning narxlari juda notekis (uzunligi-6 HMA uzunligi-200 dan ancha arzon), shuning uchun threadlar notekis tugaydi; va har bir yadro chaqiruvi o'zining oraliq massivlarini ulashilgan allokatordan ajratadi. Real mashinalarda parallel tezlashuvlar shunday ko'rinadi. Heterogen vazifalar uchun toza Nx-on-N-yadro deb kim keltirsa, u sintetik narsani o'lchamoqda.
Pog'ona M4: oxirgi uchdan biri uchun process pool — 0.23 s, 297.9x
Yakuniy pog'ona threadlarni jarayonlar bilan almashtiradi — bir xil kompilyatsiya qilingan yadro, ProcessPoolExecutor tomonidan orkestrlangan:
with ProcessPoolExecutor(max_workers=12, initializer=_init_worker,
initargs=(close,)) as ex: # ship data ONCE
list(ex.map(_warmup_worker, range(12 * 3))) # JIT-warm every worker
results = list(ex.map(_run_one_combo, grid, chunksize=1))
0.23 s — pandas'dan 297.9x, sekundiga 340.9 kombinatsiya. O'sha o'tkazuvchanlikni qayta o'qing: bu laptop endi sekundiga taxminan 340 ta to'liq 150,000-barli backtest ishga tushirmoqda, har biri yettita weighted moving average hisoblaydi va o'n minglab holatli bitimlarni simulyatsiya qiladi.
prange ustidagi ustunlik haqiqiy, ammo kamtarona — taxminan 1.4x (chiqarilgan: 0.32/0.23) — va ehtimoliy mexanika bu rejalashtirish va xotira izolyatsiyasi: chunksize=1 bilan pool kombinatsiyalarni birma-bir tarqatadi, shuning uchun arzon va qimmat oynalarning notekis aralashmasi asimmetrik yadrolar bo'ylab dinamik ravishda yuk-balanslanadi, va har bir worker jarayoni o'zining allokatorini oladi, per-combo vaqtinchalik obyektlaridagi raqobatni chetlab o'tadi. Biz buni o'lchov bilan mos keladigan mexanika sifatida xabar qilamiz, alohida isbotlangan faktlar sifatida emas.
Jarayonlar bepul emas, va harness ularning narxlarini halol ravishda taymerdan tashqarida to'laydi, u yerda ular bir martalik narxlar (worker ishga tushishi, close'ni initializer orqali har bir workerga jo'natish, per-worker JIT isinishi) — chunki real qidiruvda bu narxlar sakson emas, minglab kombinatsiya bo'ylab amortizatsiya qilinadi. Halol umumiy tavsiya: prange soddaroq va odatda yetarli; process pool vazifalar yirik bo'lganda, grid katta bo'lganda, yoki per-combo ishingiz numba yeta olmaydigan biror joyda GIL'ni ushlab turganda g'olib bo'ladi.
Va shu bilan, zinapoya toza xulosaga bo'linadi. M0 dan M2 ga — dvigatel: bitta yadroda 35.3x, iteratsiyani interpretatordan chiqarishdan. M2 dan M4 ga — orkestratsiya: yana 8.4x (chiqarilgan: 1.98/0.23), allaqachon mavjud bo'lgan yadrolardan foydalanishdan. Ko'paytirilgan: 298x. Yangi apparat yo'q, bir xil natijalar. Va layoqatli M1 bazaviy variantidan o'lchanganda, sodda variantdan emas, tayyor dvigatel hali ham taxminan 13x yuqori turadi (chiqarilgan: 3.07/0.23) — zinapoya sekin boshlang'ich nuqtani tanlashning artefakti emas.
Nega GPU emas — halol versiya

"Uni shunchaki GPU'ga port qiling" sekin parametr sweep'iga eng keng tarqalgan javob, shuning uchun ushbu tajriba o'sha suhbat boshlanishi kerak bo'lgan ikki raqamni o'lchaydi — va ikkalasi ham har qaysi javobning dangasa versiyasini qo'llab-quvvatlamaydi.
Roofline modeli (Williams, Waterman & Patterson, 2009) yadroni uning arifmetik intensivligi bo'yicha tasniflaydi — ko'chirilgan bayt uchun FLOP'lar. Ushbu sweep'dagi WMA xususiyat steki uchun, har bar uchun har bir uzunligi bo'lgan oyna uchun FLOP'ni bar uchun bitta 8-baytli o'qishga qarshi hisoblab, butun 80-kombinatsiyali sweep 576 MB oqim bo'ylab taxminan 6.2 GFLOP chiqadi:
(Bu har bir kombinatsiya uchun oltita alohida WMA oynasi bo'yicha ideallashtirilgan hisob; yettita o'tishni aslida bajarilganidek hisoblash 11.07 FLOP/byte beradi. Har ikki holatda ham bir xil xulosa.)
Bu raqam u istisno qiladigan narsa tufayli muhim: backtest matematikasi "xotira-bog'liq, shuning uchun GPU'lar yordam bera olmaydi" degan ommabop da'vo bu yerda noto'g'ri. ~10.8 FLOP/byte da xususiyat matematikasi shubhasiz hisob-og'ir — odatiy apparat o'tkazuvchanlik-cheklangan bo'lishni to'xtatadigan tizma nuqtasidan ancha o'tgan. GPU mutlaqo 80 kombinatsiya × 7 WMA o'tishini bir hovuch katta yadroga batchlashi va arifmetikani chaynab tashlashi mumkin. Agar xususiyat steki butun muammo bo'lsa, GPU holati hurmatga loyiq bo'lardi.
Ikkinchi o'lchangan raqam boshqa dangasa javobni o'ldiradi — biz o'zimiz qo'l cho'zgan bo'lardik. Kompilyatsiya qilingan yadro ichida xususiyat bosqichini savdo tsiklidan alohida o'lchash 99.3% xususiyatlar, 0.7% event tsikli bo'linishini beradi. Vasvasali argument — "backtestlarda holatli, branchli event tsikli bor, va aynan shu GPU'ni bloklaydi" — bu yerda miqdoran noto'g'ri: CPU o'z vaqtining deyarli hammasini aynan GPU batchlashi mumkin bo'lgan qismda sarflaydi. 80 kombinatsiya × 7 WMA o'tishini katta batchlangan konvolyutsiyalar sifatida qayta shakllantiring va sizda mukammal darajada mantiqiy tensor ish yuki bor. Shunday qilib, halol savol ishni GPU'ga jo'natish mumkinmi degan emas — ko'pchiligini mumkin. Savol shundaki, sayohat o'zini oqlaydimi, va bu sweep uchun oqlamaydi, ikki aniq sabab uchun:
1. Foydalanish mumkin bo'lgan kenglik 80 kombinatsiya — va GPU bu kenglik mashinasi. Parametr sweep'idagi yagona halol parallellik o'qi bu gridning o'zi: kombinatsiya ichida, 150,000-barli yo'l ketma-ketdir. GPU o'z laynalarini to'ldirish va kechikishni yashirish uchun o'n minglab mustaqil ish elementini xohlaydi; bu sweep sakson taklif qiladi. O'n ikki CPU yadrosi o'sha kenglikni allaqachon to'yintiradi — bu tom ma'noda M3–M4 pog'onalar o'lchagan narsa. GPU kengligi hatto ishga tusha boshlaydigan kombinatsiya sonlari uchun, CPU zinapoyasi allaqachon sekundiga yuzlab to'liq backtest yetkazadi.
2. Butun ish 0.23 soniya. M4 tezligida kombinatsiya taxminan 2.9 ms turadi (chiqarilgan: 0.23 s / 80). O'sha byudjetga qarshi, kernel-launch kechikishlari va qurilma sinxronizatsiya nuqtalari amortizatsiya qilinadigan yaxlitlash xatolari emas — ular ishning muhim qismidir. (Ushbu unified-memory Apple mashinasida, host-to-device o'tkazish kichik tashvish; alohida-GPU CUDA qutisida u ham hisobga qo'shiladi.) Klassik GPU yutug'i belgilangan overheadlarni katta ish batchlari bo'ylab amortizatsiya qiladi; sekunddan kam sweep hech qachon bittasini ishlab chiqarmaydi.
Va event tsikli? Bu batchlanmaydigan yagona qism — ketma-ket, branchli, yo'l-bog'liq, kombinatsiya ichida hech qanday apparat parallel qila olmaydigan 150,000 bar uzunlikdagi tsikl-ko'chirilgan bog'liqlik, aynan SIMT laynalari yomon ko'radigan divergent branchlar bilan. GPU porti uni CPU'da qoldirar yoki har bir kombinatsiya uchun bitta laynda ishga tushirar edi. Ammo yadroning 0.7% da, bu biror narsani hal qilish uchun juda kichik Amdahl termi. Bu bormaydigan qism; bu bormaslik uchun sabab emas. (M1 pog'onasidan eslang, feedback-siz yadrolar uchun tsikl hatto analitik ravishda vektorlashishi mumkin — strategiya stop o'stiradigan lahzada yo'qotadigan qayta yozish.)
To'liqlik uchun bitta platforma izohi: bu mashinada (Apple Silicon) GPU yo'li MLX yoki PyTorch-MPS bo'lardi, CUDA emas — cupy va CUDA ekotizimi shunchaki qo'llanilmaydi — va ikkalasi ham tajribani sinab ko'rish uchun issiq yo'lni tensor dialektida qayta yozishni talab qilardi. Bu, yuqoridagi tahlilga ko'ra, ushbu sweep shakli uchun aniqlangan foydasiz haqiqiy narx. Bu yerdagi GPU muhokamasi analitik, o'lchangan arifmetik intensivlik va o'lchangan xususiyat/tsikl bo'linishiga asoslangan, va biz uni shunday belgilaymiz: hech qanday CUDA run bajarilmadi, chunki oshkor qilingan apparatda hech biri mumkin emas edi.
Ko'rib chiqishda himoya qiladigan xulosa jumla: bu ishning deyarli hammasi GPU'ga jo'natilishi mumkin; bu sweep sayohat o'zini oqlashi uchun juda tor va juda qisqa. Va buni ikkala yo'nalishda ham o'qing — bu hisobdan chiqarish emas. Batchlangan "katta-matritsa" qayta shakllantirishi — sweep'ni bir vaqtning o'zida minglab kombinatsiya bo'ylab katta tensor operatsiyalari sifatida qayta shakllantirish, yoki oxiridan-oxirigacha batchlaydigan chindan feedback-siz yadro — bu ishdan bo'yin tovlash emas, alohida tadqiqotga loyiq haqiqiy va istiqbolli yo'nalish. 80 kombinatsiya va 0.23 soniyada, u hali shunchaki chiptani qozonmadi. Agar ish yukingizda o'sha kenglik bo'lsa, arifmetika o'zgaradi, va siz uni qayta bajarishingiz kerak, bizni keltirmasligingiz kerak.
Haqiqiy bo'g'ilish qayerda: dvigatel va orkestratsiya

Sakson kombinatsiya bu demonstratsiya gridi. Real parametr qidiruvi bu omillar akademik bo'lishni to'xtatadigan joy, chunki gridlar multiplikativ o'sadi: har biri o'n qiymatli to'rt parametr kombinatsiya; o'n ikkita fold bilan walk-forward validatsiya qo'shing va siz biror narsani o'rganmasdan oldin to'liq backtestda bo'lasiz. Bu o'lchamlilik la'nati, va shuning uchun qidiruv strategiyasi — Optuna, coordinate descent, Sobol — shuncha e'tibor oladi: aqlliroq qidiruv kamroq nuqta ziyorat qiladi.
Ammo zinapoya tenglamaning boshqa, kamroq muhokama qilingan yarmini fosh qiladi: ziyorat qilingan nuqta uchun narx. O'lchangan o'tkazuvchanliklarni chiziqli ekstrapolyatsiya qilish (kombinatsiyalar mustaqil, shuning uchun bu arifmetika, modellashtirish emas):
| Grid o'lchami | M0 da (1.1 kombo/s) | M4 da (340.9 kombo/s) |
|---|---|---|
| 10,000 kombinatsiya | ~2.4 soat | ~30 soniya |
| 100,000 kombinatsiya | ~24 soat | ~5 daqiqa |
Sodda dvigatelda tunlik batch ishi bo'lgan bir xil tajriba sozlangan dvigatelda interaktiv so'rovdir. O'sha farq devor-soati jadvallari kam baholaydigan tarzda kuchayadi: sweep uchun 5 daqiqada siz iteratsiya qilasiz — tuzatilgan sizib chiqish bilan qayta ishga tushirasiz, fold qo'shasiz, gridni kengaytirasiz, tushlik paytida sizga kelgan g'oyani sinaysiz. Sweep uchun 24 soatda, buni qilmaysiz. Dvigatel tezligi tadqiqot tsiklining temposini o'rnatadi, va tadqiqot tsiklining temposi haqiqiy mahsulotdir.
Butun zinapoyaning Amdahl-qonuni o'qilishi ham bor:
Har qanday bitta bosqich 'ni faktor bilan tezlashtirish siz sekin qoldirgan boshqa hamma narsa bilan chegaralanadi. Zinapoya o'sha tartibni hurmat qildi: 35.3x dvigatel yutug'i dominant qilgan termga hujum qildi (interpretatsiya qilingan iteratsiya, xususiyat stekida ham, tsiklda ham), va 8.4x orkestratsiya yutug'i undan keyin dominant qilgan termga hujum qildi (o'n bitta bo'sh yadro). Xususiyat/tsikl bo'linishi kichiklashtirilgan bir xil saboq — biz vaqt aslida qayerga ketganini o'lchamasdan GPU argumentining haqiqiy shaklini nomlashimiz mumkin emas edi. Profil qiling, keyin optimizatsiya qiling — o'sha tartibda. Bir xil mantiq dvigateldan yuqoridagi ma'lumot qatlamini boshqaradi: bizning Polars vs pandas benchmarklarimiz stekning load-and-transform yarmi uchun bir xil naqshni topdi (grouped rolling pipeline'larda 10–3500x), va bir xil gibrid xulosa — pipeline uchun ustunli dvigatellar, yo'l-bog'liq simulyatsiya uchun kompilyatsiya qilingan yadro.
Umumiylik bo'yicha tsiklni yopish uchun ikkita halollik izohi. Birinchidan, ushbu tajriba ataylab o'z-o'zicha yopiq va sintetik — seedli ma'lumot, bitta yadro, bitta oshkor qilingan mashina — shuning uchun har kim fenomenni determinlangan holda takrorlashi mumkin; devor-soati raqamlari sizning apparatingizda farq qiladi, ammo ekvivalentlik va zinapoyaning yo'nalishi farq qilmaydi. Ikkinchidan, fenomen sintetik sozlamaning artefakti emas: bizning production HMA dvigatelimizning benchmark'i (bench_param_sweep.py, to'liq production komissiya va fill modeli bilan real birja ma'lumotlarida ishga tushirilgan) bir xil zinapoya shaklini ko'rsatadi, numba yo'li sodda pandas profilidan taxminan 100–200x yuqori tushadi. O'z-o'zicha yopiq tajriba bizning production raqamlarimizni ishonchga olishingiz shart bo'lmasligi uchun mavjud.
Asosiy xulosalar
- Zinapoya 298x, va u faktorlashadi: 35.3x dvigatel × 8.4x orkestratsiya. Iteratsiyani interpretatordan chiqarish (pandas → numba) va mustaqil kombinatsiyalarni yadrolar bo'ylab tarqatish (bitta → o'n ikki) o'zgarmagan laptopda uch-tartibli-yaqin tezlashuvga ko'paytdi. 69.92 s → 0.23 s; 1.1 → 340.9 kombo/s. Va bu sekin-bazaviy artefakt emas: layoqatli vektorlashtirilgan numpy implementatsiyasiga qarshi, tayyor dvigatel hali ham ~13x.
- Tezlikni hayratlantirishdan oldin ekvivalentlikni talab qiling. Bu yerdagi har bir pog'ona bir xil per-combo PnL va bitimlar sonini ishlab chiqaradi, barcha 80 kombinatsiyada avtomatik darvozalangan (PnL'da absolyut tolerantlik, bitimlarda aniq). Nozik farq qiladigan narsani hisoblaydigan tez dvigatel tez emas — u yuqori o'tkazuvchanlikda noto'g'ri, va vektorlashtirilgan qayta yozishlar noto'g'rilik odatda yashirincha kiradigan joy.
- Holatli mantiq uchun
@njitaqlli vektorlashtirishdan ustun. numpy pog'onasi stop-loss qo'shgan lahzangizda o'ladigan strategiyaga xos yopiq shaklga muhtoj edi. numba pog'onasi sodda, auditga yaroqli tsiklni kompilyatsiya qiladi — bir xil tezlik klassi, mo'rtlikning hech biri, va bu parallellashadigan birlik. - GPU javobi "bu sweep uchun emas" — nomlashingiz kerak bo'lgan sabablar uchun. Xususiyat matematikasi hisob-og'ir (10.78 FLOP/byte) va u kompilyatsiya qilingan yadroning 99.3% i, shuning uchun na "backtestlar xotira-bog'liq" na "holatli tsikl dominant" o'lchovda omon qolmaydi. Halol sabablar bu kenglik va byudjet: 12 CPU yadrosi allaqachon to'yintiradigan 80 kombinatsiyali foydalanish mumkin bo'lgan parallellik, va launch va sinxronizatsiya overhead'i yeb qo'yadigan 0.23 s umumiy ish. Real kenglikdagi batchlangan katta-matritsa qayta shakllantirishi rad etilgan emas, istiqbolli yo'nalish bo'lib qoladi.
- Dvigatel tezligi bu tadqiqot temposi. Sodda-dvigatel o'tkazuvchanligida, 100,000-backtestli qidiruv bir kun; zinapoya-tepasi o'tkazuvchanligida u besh daqiqa. Apparat sotib olishdan yoki klaster ijaraga olishdan oldin, bo'g'ilishingiz umuman kremniymi tekshiring — bizniki
rolling.applyichidagilambdava o'n bitta bo'sh yadro edi.
To'liq tajriba — barcha beshta implementatsiya, ekvivalentlik harness'i, roofline hisobi, va ushbu maqoladagi har bir raqam bitta determinlangan skriptdan qayta hosil qilinadigan — hamroh ishda speed-ladder.marketmaker.cc manzilida, kod va ma'lumotlar esa github.com/suenot/backtest-speed-ladder da.
Yetmish soniya olgan sweep bitta soniyaning chorak qismini oladi. Bir xil bitimlar, bir xil PnL, bir xil laptop. Siz talab qilmoqchi bo'lgan GPU kutishi mumkin; siz yetkazmoqchi bo'lgan interpretator tsikli kuta olmaydi.
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.