← Kembali ke artikel
July 6, 2026
5 menit baca

Jebakan Presisi GPU: Bagaimana Backtest fp32 di Apple Metal Diam-Diam Mengembalikan Sampah

Jebakan Presisi GPU: Bagaimana Backtest fp32 di Apple Metal Diam-Diam Mengembalikan Sampah
#algotrading
#backtest
#gpu
#apple-silicon
#floating-point
#stabilitas-numerik
#mlx
Part 8 of 10 · Collection
High-Performance Backtest Engines

Bagian dari seri "Backtest Tanpa Ilusi".

Kami mem-porting backtest parameter-sweep kami ke GPU dan ia menjadi 2.796× lebih cepat. Angka yang dikembalikannya tampak masuk akal. Namun, pada versi pertama yang berfungsi, angka-angka itu juga sepenuhnya sampah — meleset dengan faktor dua ratus — dan tidak ada yang crash, tidak ada peringatan, tidak ada yang tampak salah.

Inilah jebakan yang membuat backtesting GPU di Apple Silicon berbahaya dengan cara yang tidak dimiliki kode CPU: Metal tidak memiliki float64. Setiap angka yang disentuh backtest Anda di GPU Apple adalah float 32-bit, entah Anda memintanya atau tidak. Dan satu-satunya cara paling menggoda untuk memvektorisasi moving average — trik prefix-sum O(n) yang menjadi andalan setiap quant yang mementingkan performa — justru merupakan formulasi yang tidak dapat bertahan di fp32 pada skala harga. Ia tidak menghasilkan error. Ia berjalan dengan kecepatan penuh dan memberi Anda kurva ekuitas yang tampak masuk akal yang dibangun di atas moving average yang salah dengan faktor 211.

Solusinya adalah bagian yang menarik, karena solusinya bukan "gunakan lebih banyak bit" (Anda tidak bisa) dan bukan "lebih hati-hati" (kode naif itu sudah hati-hati). Solusinya adalah menghitung kuantitas matematis yang sama dengan penjumlahan yang berbeda — konvolusi berjendela langsung — yang menjaga setiap nilai antara tetap cukup kecil agar fp32 dapat merepresentasikannya secara persis. WMA yang sama, hasil yang sama hingga tujuh angka penting, dan 55,9× lebih cepat daripada numba single-thread di CPU. Artikel ini adalah otopsinya: mengapa Metal memaksa fp32 kepada Anda, di mana persisnya formulasi yang jelas itu overflow, mengapa yang benar tidak, dan bagaimana kami membuktikan — dengan menghitung trade, bukan dengan mengamati kurva secara sekilas — bahwa versi cepat dan versi fp64 yang jujur saling sepakat.

Semua angka di sini diukur pada Apple M2 Max, 150.000 bar × 80 kombinasi parameter, best-of-3, dari scripts/bench_param_sweep.py di repo (metode GPU M5, commit 97eadaf), scripts/engine_multitf_gpu.py (04d71e8), dan catatan desain di scripts/GPU_NOTES.md.

Metal tidak memiliki float64

Pipeline GPU Metal dari Apple dengan float64 dicoret — setiap lane dipaksa menjadi 32-bit, integer hanya persis hingga 1,6×10⁷

Pada CPU, backtest Python/numpy berjalan dalam double precision secara default. float64 memberi Anda mantissa 52-bit: integer direpresentasikan secara persis hingga 2⁵³ ≈ 9×10¹⁵, dan presisi relatif sekitar 1,1×10⁻¹⁶. Anda hampir tidak pernah memikirkannya, karena pada skala harga — sebuah close BTC sekitar 30.000, sebuah cumulative sum yang mencapai 10¹⁴ — fp64 memiliki margin yang berlimpah.

GPU Apple tidak menawarkan margin itu, karena ia tidak menawarkan float64 sama sekali. Metal Shading Language tidak memiliki tipe double; MLX, PyTorch-MPS, dan setiap framework lain yang menargetkan Apple Silicon mewarisi hal ini. Tidak ada flag untuk mengaktifkannya dan tidak ada fallback yang lambat-tapi-benar. Jika komputasi Anda menyentuh GPU, ia terjadi dalam fp32:

  • Mantissa: 23 bit (24 dengan bit leading implisit). Integer hanya persis hingga 2²⁴ = 16.777.216 ≈ 1,6×10⁷.
  • Presisi relatif: ~1,2×10⁻⁷. Kira-kira tujuh angka desimal penting, dan tidak lebih.

Batas atas 1,6×10⁷ itu adalah keseluruhan ceritanya. Kedengarannya murah hati — enam belas juta — sampai Anda menyadari bahwa sebuah backtest secara rutin membangun kuantitas antara yang jauh lebih besar dari itu, dan pada saat sebuah kuantitas antara melewati 1,6×10⁷, fp32 tidak lagi bisa merepresentasikan integer yang berurutan sekalipun, apalagi struktur pecahan yang Anda butuhkan. Presisi itu tidak menurun secara mulus; ia jatuh dari tebing, dan tebing itu ada pada sebuah angka yang dilewati data Anda tanpa berpikir dua kali.

Jebakannya adalah bahwa fp32 hampir selalu baik-baik saja. Sebagian besar dari sebuah backtest — harga, return, PnL, Sharpe — hidup dengan nyaman dalam rentang di mana tujuh angka penting sudah lebih dari cukup. Jadi porting naif itu berfungsi, lolos smoke test, menghasilkan output yang tampak waras. Kegagalannya terlokalisasi pada persis satu operasi, satu tempat di mana sebuah kuantitas antara meledak melewati 10⁷, dan operasi itu adalah yang pertama kali divektorisasi oleh semua orang.

Formulasi yang menggoda: WMA dalam satu sapuan prefix-sum

Strategi kami bertumpu pada Hull moving average. Sebuah HMA adalah tiga weighted moving average yang disusun bersama; varian HMA3 adalah empat. Sebuah weighted moving average dengan kernel linear di atas jendela sepanjang p adalah

WMAt=k=1pkxtp+kp(p+1)/2.\mathrm{WMA}_t = \frac{\sum_{k=1}^{p} k \cdot x_{t-p+k}}{p(p+1)/2}.

Menyapu ribuan kombinasi parameter di atas 150 ribu bar, konvolusi WMA itulah biayanya. Jadi naluri — naluri yang benar, di CPU — adalah membuat setiap WMA menjadi O(n) alih-alih O(n·p) menggunakan prefix sum. Anda menghitung dua cumulative sum sekali di awal,

S1[t]=jtxj,S2[t]=jtjxj,S_1[t] = \sum_{j \le t} x_j, \qquad S_2[t] = \sum_{j \le t} j \cdot x_j,

dan kemudian jumlah tertimbang-linear dari jendela mana pun runtuh menjadi segenggam selisih dan pergeseran indeks dari S1 dan S2. Tidak ada loop per-jendela, tidak ada reduksi per-jendela — dua lintasan cumsum dan seluruh matriks WMA jatuh keluar dari aritmetika array. Ia memvektorisasi dengan indah, ia memetakan dengan sempurna ke primitif parallel-scan GPU, dan pada fp64 ia benar-benar tepat.

Ia juga merupakan satu-satunya hal terburuk yang dapat Anda lakukan di fp32, dan alasannya bersembunyi di dalam S2.

S2 = mx.cumsum(j * price)     # j is the global bar index: 0, 1, 2, ... , n-1

Suku j · price adalah masalahnya. Dengan j berjalan hingga 150.000 dan price sekitar 30.000, suku terakhir saja sudah 4,5×10⁹, dan S2 adalah jumlah berjalan dari 150.000 suku semacam itu. Ia tidak tetap pada skala harga. Ia menanjak ke wilayah di mana fp32 sudah berhenti dapat menghitung.

Di mana ia overflow: aritmetika dari jebakan itu

Dua jumlah fp32 raksasa berskala ~10¹⁴ dikurangkan untuk memulihkan sebuah nilai berjendela kecil yang lenyap di bawah error pembulatan mereka — pembatalan katastrofik

Mari kita letakkan orde besaran itu berdampingan, karena di situlah seluruh kegagalannya berada.

S2 = cumsum(j · price) mencapai kira-kira price · n²/2 ≈ 30.000 · (150.000)²/2 ≈ 3×10¹⁴. Sebut saja ~10¹⁴. Sekarang ingat kembali batas atas integer-persis fp32: ~1,6×10⁷. Jumlah berjalan itu melampaui integer terakhir yang dapat direpresentasikan fp32 secara persis dengan tujuh orde besaran.

Apa artinya itu secara konkret? Di sekitar 10¹⁴, celah antara dua angka fp32 yang dapat direpresentasikan — satu unit pada posisi terakhir, si ULP — adalah sekitar 2²³ ≈ 8×10⁶. Jadi begitu S2 berada di rentang 10¹⁴, ia hanya diketahui dalam batas ±8 juta. Setiap nilai yang disimpannya telah dibulatkan ke kelipatan terdekat dari ~8×10⁶.

Sekarang perhatikan apa yang dilakukan pemulihan WMA dengan itu. Untuk mengekstrak jumlah tertimbang dari satu jendela, Anda mengurangkan dua nilai S2 yang bertetangga (ditambah koreksi S1). Kedua nilai S2 itu masing-masing ~10¹⁴, masing-masing membawa noise pembulatan ±8×10⁶. Selisih sebenarnya mereka — kuantitas berjendela yang sebenarnya Anda inginkan — bersesuaian, setelah normalisasi, dengan sebuah WMA pada orde harga itu sendiri, ~3×10⁴. Jadi aritmetikanya adalah:

(1014±8×106)fp32(1014±8×106)fp32=sebuah kuantitas yang Anda butuhkan hingga beberapa bagian dalam 104jawaban sebenarnya\underbrace{(10^{14} \pm 8\times10^6)}_{\text{fp32}} - \underbrace{(10^{14} \pm 8\times10^6)}_{\text{fp32}} = \underbrace{\text{sebuah kuantitas yang Anda butuhkan hingga beberapa bagian dalam } 10^4}_{\text{jawaban sebenarnya}}

Inilah pembatalan katastrofik (catastrophic cancellation) dalam bentuknya yang paling murni: error pembulatan dari setiap operan (±8×10⁶) lebih besar daripada jawaban yang sedang Anda coba pulihkan. Sinyalnya lebih kecil daripada lantai noise (noise floor) dari angka-angka tempat ia diekstraksi. Bukan sekadar Anda kehilangan beberapa angka — Anda kehilangan semuanya, dan yang kembali didominasi oleh pembulatan akumulatif dari cumsum.

Konsekuensi yang terukur, dari GPU_NOTES.md: untuk sebuah WMA yang dihitung dengan cara ini pada 150 ribu bar dengan harga ~30.000, kesalahan relatif maksimum terhadap fp64 mencapai ~211. Bukan 211 persen — 211×. Moving average yang dihitung bisa berjarak dua orde besaran dari yang sebenarnya. Dan inilah bagian yang menjadikannya sebuah jebakan alih-alih sekadar bug: ia berjalan hingga selesai dan mengembalikan angka yang terhingga dan tampak masuk akal. Tidak ada overflow menuju tak hingga, tidak ada NaN, tidak ada exception. Sebuah moving average yang salah dengan faktor 211× masih tampak seperti moving average — ia mulus, ia terhingga, ia kira-kira berada di kisaran yang tepat pada bar-bar di mana pembatalan kebetulan ringan — sehingga ia lolos dari setiap pemeriksaan kewarasan yang bukan merupakan perbandingan langsung terhadap referensi terpercaya. Anda mendapatkan backtest yang lengkap, kurva ekuitas yang lengkap, satu set parameter "optimal" yang lengkap, semuanya dibangun di atas sebuah indikator yang fiktif.

Solusinya bukan presisi yang lebih tinggi — melainkan penjumlahan yang berbeda

Sebuah jendela geser pendek berisi ~200 suku berskala sama yang dijumlahkan secara langsung dengan konvolusi — setiap jumlah parsial tetap di dekat skala harga, di dalam rentang persis fp32

Refleks, begitu Anda melihat error itu, adalah meraih presisi yang lebih tinggi — mengakumulasi dalam fp64, atau menggunakan penjumlahan terkompensasi (Kahan). Di Metal, yang pertama sama sekali tidak tersedia. Namun Anda tidak membutuhkan keduanya, karena masalahnya tidak pernah pada jumlah bit. Masalahnya adalah formulasinya. Trik prefix-sum memproduksi kuantitas antara berskala 10¹⁴ dan kemudian menguranginya kembali; besaran yang ia ciptakan adalah artefak dari algoritma, bukan dari jawabannya. Pilih formulasi yang tidak pernah menciptakannya dan fp32 baik-baik saja.

Formulasi itu adalah definisinya sendiri: sebuah konvolusi berjendela langsung. Alih-alih dua cumulative sum global, geser kernel linear sepanjang p melintasi deret dan jumlahkan di tempat. Setiap output adalah jumlah dari paling banyak p ≈ 200 suku, dan setiap suku adalah weight × price di mana bobotnya dinormalisasi agar berjumlah 1 — sehingga setiap suku berada pada orde price / p, setiap jumlah parsial tetap di sekitar skala harga (~3×10⁴), dan tidak ada kuantitas antara yang pernah mendekati enam orde besaran dari batas atas fp32. Tidak ada yang perlu dibatalkan karena tidak ada yang pernah menggelembung.

Di MLX ini adalah satu primitif — mx.conv1d — yang justru merupakan hal yang dirancang GPU untuk lakukan dengan cepat:

def _mx_wma_valid(x, period):
    w = mx.arange(1, period + 1, dtype=mx.float32) / (period * (period + 1) / 2.0)
    return mx.conv1d(x.reshape(1, -1, 1), w.reshape(1, period, 1), padding=0).reshape(-1)

WMA yang sama, identik secara matematis dengan versi prefix-sum dan dengan vec_wma/nb_wma fp64 dari CPU. Namun kini kesalahan relatif maksimum terukur terhadap fp64 adalah 8,2×10⁻⁷ — tepat pada lantai noise fp32 ~1,2×10⁻⁷, tujuh angka penting kesepakatan. Formulasi yang tampak lebih lambat di atas kertas (O(n·p) alih-alih O(n)) adalah satu-satunya yang benar, dan — karena ia merupakan konvolusi padat yang diparalelkan GPU melintasi bar dan jendela sekaligus — ia juga sangat cepat. Kami beralih dari kesalahan relatif 211 menjadi 8×10⁻⁷ dengan mengubah cara kami menjumlahkan, bukan berapa banyak bit tempat kami menjumlahkan.

Dua catatan praktis yang muncul dari melakukannya dengan cara ini. Pertama, MLX tidak akan menyebarkan NaN melalui sebuah conv1d seperti yang dilakukan numpy, sehingga wilayah pemanasan (warm-up) (bar p−1 pertama, di mana sebuah average berjendela belum terdefinisi) tidak dapat ditandai dengan NaN di GPU. Kami tidak membutuhkannya demikian: awal valid dari setiap deret diketahui secara analitis, prefix yang tidak valid diisi dengan nol yang tidak pernah dibaca, dan padding NaN dipulihkan di CPU setelahnya — mask validitas yang bit-demi-bit sama seperti versi tervektorisasi dan versi numba. Kedua, keseluruhan sweep berbagi satu deret cand_close dan menggunakan kembali jendela secara intensif di seluruh kombinasi, sehingga satu conv1d yang ter-batch dengan banyak output channel menghitung setiap WMA unik yang dibutuhkan sweep dalam satu panggilan GPU, dimaterialisasi dengan satu mx.eval().

Membuktikan Anda tidak terjebak: paritas berdasarkan jumlah trade

Inilah pertanyaan yang tidak nyaman yang seharusnya diangkat oleh bagian terakhir: jika sebuah WMA yang salah dengan faktor 211× masih tampak seperti WMA, bagaimana Anda tahu bahwa versi 8×10⁻⁷ benar-benar tepat dan bukan sekadar salah dengan cara yang lebih halus? Anda tidak dapat mengamatinya secara sekilas. Anda membutuhkan sebuah invarian yang diekspos oleh bagian hilir (downstream) yang diskret dari pipeline — dan sebuah backtest memberi Anda invarian yang sempurna: trade-nya.

Metode GPU lain dalam tangga (M0–M4) berjalan sepenuhnya dalam fp64, sehingga kami menahannya pada assert ekuivalensi yang ketat — jumlah trade yang identik, PnL yang cocok hingga atol=1e-6. Metode GPU fp32 (M5) tidak dapat lolos itu secara konstruksi, dan diam-diam melonggarkan assert untuk semua orang demi mengakomodasinya justru merupakan jenis ketidakjujuran yang menjadi alasan keberadaan seri ini untuk dilawan. Jadi M5 mendapatkan laporan paritas kuantitatifnya sendiri, report_equiv_fp32, yang membandingkan trade yang diekstraknya terhadap referensi fp64.

Mekanisme dari setiap ketidaksepakatan sisa layak disebutkan secara persis, karena ia bukan katastrofe pembatalan — ia adalah pembulatan fp32 yang biasa dan sangat kecil yang memang Anda harapkan. Strategi ini terpicu (fire) pada perpotongan (crossover) dua Hull average, h vs h3. Kesalahan relatif ~1×10⁻⁶ pada sebuah indikator dengan harga ~30.000 adalah goyangan absolut sebesar ~0,03. Pada sebagian besar bar, kedua kurva berjarak lebih jauh dari itu dan crossover-nya tidak ambigu. Namun pada bar yang ambang (borderline) — di mana h − h3 itu sendiri berada dalam 0,03 dari nol — goyangan itu dapat membalik tanda perbandingan, menggeser satu crossover sebesar satu bar, menambah atau menghapus satu trade.

Inilah sebabnya "fraksi kombinasi yang berbeda" adalah metrik kesehatan yang tak bernilai, dan pemeriksaan paritas pertama kami mempermalukan dirinya sendiri dengan menggunakannya. Pada 150 ribu bar, setiap kombinasi memiliki ribuan crossover, sehingga setidaknya satu bar ambang muncul pada hampir setiap kombinasi — 37 dari 80 kombinasi "berbeda," yang terdengar mengkhawatirkan dan tidak berarti apa-apa. Metrik yang penting adalah seberapa besar:

  • Delta PnL di seluruh 80 kombinasi: maks |Δ| = 1,843 poin persentase, maks relatif = 1,25×10⁻²; ambang crash 5 p.p.
  • Drift jumlah-trade per kombinasi: maks |Δn| = 4 trade dari ribuan, maks relatif = 2,5×10⁻³; ambang crash 1%.
  • Secara agregat: 90 trade yang bergeser dari 479.016 — 0,019%.

Sembilan puluh trade dari hampir setengah juta, setiap satunya adalah crossover ambang yang terdorong oleh goyangan pembulatan yang lebih kecil daripada satu price tick, tidak satu pun yang mendekati ambang crash. Itulah tanda tangan dari sebuah metode fp32 yang benar — ketidaksepakatan yang kecil, terbatas, dan dapat dijelaskan — dan ia merupakan makhluk yang sepenuhnya berbeda dari kesalahan relatif 211. Ambang-ambang itu ada untuk menangkap formulasi yang rusak yang menyamar sebagai "yah, ini kan cuma fp32"; delta yang sebenarnya datang dengan orde besaran di bawahnya. Jumlah trade adalah orakel yang menolak ditawarkan oleh kurva ekuitas.

Hasilnya, dan di mana GPU berhenti membantu

Dengan kebenaran yang sudah ditetapkan, kecepatannya layak dinyatakan — dan kemudian dikualifikasi secara jujur, karena keunggulan GPU tidaklah seragam di seluruh pipeline.

Pada konvolusi WMA murni secara terpisah — operasi yang menjadi alasan keberadaan seluruh metode ini untuk diakselerasi — batch conv1d fp32 berjalan 55,9× lebih cepat daripada numba single-thread, pada kesalahan relatif 8,2×10⁻⁷ itu. Itulah angka GPU-vs-CPU yang bersih dan apel-ke-apel: matematika yang sama, satu thread kode CPU terkompilasi melawan GPU Metal.

Namun sebuah sweep bukan hanya konvolusi. Begitu matriks HMA/HMA3 dihitung di GPU, trade masih harus diekstraksi — sebuah penelusuran O(n) di atas crossover setiap kombinasi — dan itu kami lakukan di CPU dalam fp64, menggunakan kembali semantik trade yang persis dari metode lain alih-alih mengimplementasikannya ulang di GPU. Angka timed() end-to-end mencakup segalanya: warm-up kernel dikecualikan (simetris dengan mengecualikan waktu kompilasi numba), tetapi transfer GPU→CPU dan ekstraksi trade CPU disertakan. Pada 150 ribu bar × 80 kombinasi, best-of-3, M2 Max:

Metode Wall Speedup vs baseline kombinasi/s
M0 pandas + Python loop* 287,08s 1,0× 0,3
M1 vectorized numpy 3,14s 91,5× 25,5
M2 numba (serial) 2,02s 142,3× 39,7
M3 multiprocess + vectorized 0,50s 570,2× 158,9
M4 multiprocess + numba (12 core) 0,33s 882,5× 245,9
M5 MLX GPU (fp32) 0,10s 2796,0× 779,2

*M0 diekstrapolasi dari sampel 5-kombinasi yang seragam.

M5 full-engine melakukan 779 kombinasi/s — 2.796× di atas baseline pandas, 19,6× di atas numba serial, dan 3,2× di atas seluruh kolam CPU 12-core yang menjalankan numba (M4). Satu GPU mengalahkan setiap core CPU yang dimiliki mesin itu, tiga kali lipat.

Kini kualifikasi yang jujur: perhatikan bahwa keunggulan GPU end-to-end (19,6× di atas M2) lebih kecil daripada keunggulan hanya-konvolusi (55,9× di atas numba). Kesenjangan itu adalah hukum Amdahl yang tiba sesuai jadwal. GPU menghancurkan konvolusi itu begitu tuntas sehingga mereka berhenti menjadi bottleneck; yang tersisa — ekstraksi trade CPU O(n) yang tidak dipercepat oleh GPU sedikit pun — kini mendominasi wall time M5. Ini adalah pelajaran yang sama yang terus dijatuhkan oleh potongan speed-ladder dan pajak-IPC dalam seri ini: melewati suatu titik, kemenangannya bukanlah "membuat bagian yang cepat menjadi lebih cepat," melainkan orkestrasi — di mana data berada, tahap mana yang kini serial, apa yang Anda bayar untuk memindahkan antara device dan host. Mengejar M6 hipotetis yang mendorong ekstraksi trade ke dalam kernel Metal kustom hanya akan merebut kembali irisan CPU yang menyusut, itulah sebabnya kami tidak membangunnya.

Pelajaran umum: sampah numerik yang diam-diam adalah default

Mundur sejenak dari Hull average dan MLX, karena jebakan ini menggeneralisasi jauh melampaui satu indikator ini.

Bujukan yang menggiurkan dari backtesting GPU adalah "satu matriks besar": tumpuk setiap kombinasi parameter ke dalam sebuah tensor, jalankan seluruh sweep sebagai segenggam operasi array yang padat, biarkan hardware melahapnya. Bujukan itu nyata — speedup di atas nyata. Namun ia diam-diam mengubah regime numerik di bawah Anda, dan perubahan itu tak terlihat dalam kode. Pada CPU, default Anda melindungi Anda: fp64, propagasi NaN, sebuah cumsum yang dapat berjalan hingga 10¹⁴ tanpa menyadarinya. Pindahkan ekspresi array yang sama ke Metal dan Anda berada di fp32 dengan batas atas integer keras 1,6×10⁷, dan baris kode yang identikcumsum(j * price) — beralih dari persis menjadi sampah. Tidak ada dalam sintaksnya yang memperingatkan Anda. Compiler-nya senang. Output-nya terhingga dan masuk akal. fp32 tidak gagal dengan lantang; ia gagal dengan sopan, dengan angka.

Tiga kebiasaan yang benar-benar melindungi Anda itu murah:

  1. Ketahui di mana kuantitas antara Anda berada, bukan hanya input dan output Anda. Input (harga ~10⁴) dan output (sebuah WMA ~10⁴) keduanya berada dengan nyaman di dalam rentang persis fp32. Bencananya sepenuhnya berada dalam sebuah kuantitas antara yang tersembunyi (S2 ~10¹⁴) yang tidak dibuat terlihat oleh API maupun tipenya. Sebelum mempercayai reduksi fp32 apa pun, tanyakan berapa nilai jumlah parsial terbesar yang dicapai — dan jika ia melewati ~10⁷, ubah formulasinya.
  2. Utamakan formulasi yang menjaga besaran tetap terbatas. Konvolusi langsung di atas prefix sum; jendela lokal di atas scan global; centering/differencing sebelum penjumlahan alih-alih sesudahnya. Besar-lalu-batalkan adalah anti-pola. Algoritma yang benar sering kali adalah yang tampak lebih buruk secara asimtotik di atas kertas tetapi tidak pernah memproduksi kuantitas yang harus ia batalkan.
  3. Validasi terhadap orakel fp64 melalui sebuah invarian diskret. Jangan membandingkan kurva; bandingkan sesuatu yang terkuantisasi dan hilir — jumlah trade, jumlah crossover, event perubahan posisi. Sebuah invarian diskret mengubah kesalahan 211× yang diam menjadi kegagalan assertion yang berteriak, dan mengubah kesalahan 8×10⁻⁷ yang dapat diterima menjadi delta yang kecil, terbatas, dan dapat dijelaskan. Ini adalah disiplin yang sama seperti uji pergeseran satu bar untuk look-ahead bias: sebuah diagnostik murah yang mengubah kegagalan tak terlihat menjadi terlihat.

Tidak ada satu pun dari ini yang merupakan analisis numerik eksotis. Ia adalah kebersihan (hygiene) biasa dari tidak mempercayai backtest yang cepat sampai sebuah backtest yang lambat dan terpercaya telah meratifikasinya — diperluas ke satu tempat di mana bahasa berhenti memperingatkan Anda bahwa presisi telah diam-diam turun sebesar dua belas orde besaran.

Kesimpulan penting

Pivot satu baris: cumsum(j·price) dicoret pada kesalahan 211×, mx.conv1d di sampingnya pada 8×10⁻⁷ — WMA yang sama, penjumlahan yang berbeda

  1. GPU Apple tidak memiliki float64 — setiap angka GPU dalam backtest Anda adalah fp32. Integer hanya persis hingga ~1,6×10⁷ dan presisinya ~1,2×10⁻⁷. Tidak ada flag, tidak ada fallback. Sebagian besar dari sebuah backtest selamat dari ini; persis satu operasi biasanya tidak.
  2. WMA prefix-sum adalah jebakannya. cumsum(j · price) menanjak hingga ~10¹⁴, tujuh orde melewati batas atas persis fp32, dan memulihkan sebuah jendela memaksa Anda mengurangkan dua angka semacam itu yang error pembulatannya (±8×10⁶) sudah mengerdilkan jawabannya. Kesalahan relatif maksimum terukur: 211×. Ia tidak pernah crash — ia mengembalikan sampah yang tampak masuk akal.
  3. Solusinya adalah penjumlahan yang berbeda, bukan lebih banyak bit. Sebuah konvolusi berjendela langsung (mx.conv1d) menjaga setiap jumlah parsial tetap di dekat skala harga, sehingga fp32 mempertahankan tujuh angka yang jujur: kesalahan relatif 8,2×10⁻⁷, dan 55,9× lebih cepat daripada numba single-thread. Anda tidak dapat membeli fp64 di Metal, dan Anda tidak membutuhkannya.
  4. Verifikasi dengan invarian diskret, jangan pernah dengan kurva. Paritas jumlah-trade menangkapnya: conv1d fp32 tidak sepakat dengan fp64 pada 90 dari 479.016 trade (0,019%), semuanya crossover ambang, semuanya jauh di bawah ambang crash — tanda tangan dari sebuah metode yang benar, tak salah lagi berbeda dari kesalahan 211×. "Fraksi kombinasi yang berbeda" adalah metrik umpan (decoy); ukurlah seberapa besar.
  5. Sweep penuh melakukan 779 kombinasi/s — 2.796× di atas baseline pandas, 3,2× di atas seluruh kolam CPU 12-core — tetapi kemenangan end-to-end (19,6× di atas numba serial) lebih kecil daripada kemenangan hanya-konvolusi (55,9×) karena ekstraksi trade CPU kini menjadi bottleneck. Melewati suatu titik, kecepatan adalah orkestrasi, bukan aritmetika.

Porting GPU itu 2.796× lebih cepat dan, pada versi pertamanya yang berfungsi, sepenuhnya salah — dan kedua fakta itu tidak ada hubungannya satu sama lain. Kecepatannya nyata. Sampahnya adalah sebuah kuantitas antara tersembunyi berskala 10¹⁴ yang tidak dapat ditampung fp32 dan tidak akan disebutkan oleh pesan error apa pun. Jika sebuah backtest menjadi jauh lebih cepat dan angka-angkanya masih tampak baik-baik saja, itu bukan konfirmasi. Di Metal, "tampak baik-baik saja" adalah persis seperti apa kesalahan relatif 211 terlihat.

Ini adalah anak tangga GPU dari tangga yang telah didaki seri ini: tangga kecepatan backtest-engine, pajak IPC dari multiprocessing, taksonomi look-ahead dari kebocoran, dan desain fungsi-objektif yang menentukan apa arti "bagus" itu sendiri. Kecepatan tak bernilai jika ia cepat dalam menghitung angka yang salah.

Penafian: Informasi yang disediakan dalam artikel ini hanya untuk tujuan edukasi dan informasi serta tidak merupakan nasihat keuangan, investasi, atau trading. Trading mata uang kripto mengandung risiko kerugian yang signifikan.

Penulis

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Selangkah Lebih Maju dari Pasar

Berlangganan newsletter kami untuk wawasan AI trading eksklusif, analisis pasar, dan pembaruan platform.

Kami menghormati privasi Anda. Berhenti berlangganan kapan saja.