Menghadirkan Eksekusi Order Kripto yang Andal
Keandalan eksekusi order kripto berasal dari state machine, kunci idempotensi, dan rekonsiliasi tanpa henti. Pelajaran dari membangun MadaiOps.
Oleh Vitor Lima
Sebagian besar produk trading kripto menjual kecepatan dan sebuah grafik. Bagian yang sulit bukanlah menggambar candle. Bagian yang sulit adalah mampu menjawab, pada milidetik mana pun, sebuah pertanyaan yang tampak sederhana namun menipu: apa keadaan sebenarnya dari order saya saat ini? Ketika kami mulai membangun MadaiOps, aplikasi order dan operasi trading internal kami, pertanyaan itulah yang mengatur segalanya. Kecepatan adalah fitur. Kebenaran adalah produknya. Keandalan eksekusi order kripto adalah apa yang Anda miliki ketika aplikasi mampu menjawab pertanyaan itu dengan jujur bahkan saat bursa sedang menurun kualitasnya, dan aplikasi yang menampilkan sebuah fill yang tidak terjadi — atau menyembunyikan yang benar-benar terjadi — menghancurkan kepercayaan dengan cara yang tidak bisa ditebus oleh kemenangan latensi mana pun.
Tulisan ini membahas rekayasa yang membuat aplikasi order layak dipercaya: state machine, idempotensi, rekonsiliasi melalui WebSocket, dan mode kegagalan yang hanya muncul di bawah beban. Kami punya pendirian yang tegas soal semua itu, karena pilihan-pilihan yang membosankan itulah yang menjaga Anda tetap solven.
Modelkan order sebagai state machine yang eksplisit
Keputusan terbaik yang kami buat adalah berhenti memperlakukan order sebagai baris yang di-UPDATE dan mulai memperlakukannya sebagai finite state machine dengan transisi bernama yang terdaftar. Sebuah order hidup dalam keadaan seperti PENDING_NEW, WORKING, PARTIALLY_FILLED, FILLED, CANCEL_REQUESTED, CANCELED, REJECTED, dan EXPIRED. Setiap transisi adalah fungsi dari keadaan saat ini ditambah event yang masuk, dan transisi ilegal ditolak dengan lantang alih-alih diserap dalam diam.
Imbalannya: ambiguitas menjadi urusan waktu validasi alih-alih insiden jam 2 pagi. Sebuah order FILLED tidak akan pernah bisa kembali ke WORKING. Sebuah event fill yang tiba untuk order CANCELED bukanlah hal yang "mustahil" — itu adalah race yang nyata, dan state machine memaksa Anda memutuskan apa artinya. Biasanya itu berarti pembatalan kalah dalam race, order sebenarnya terisi, dan status lokal CANCELED Anda keliru. Modelkan order sebagai baris yang bisa diubah dan event itu sekadar menimpa sesuatu; Anda tidak akan pernah menyadarinya sampai rekonsiliasi — atau seorang pengguna — menangkapnya.
Intinya
Daftarkan seluruh keadaan Anda sebelum menulis satu pun panggilan API. Jika Anda tidak bisa menggambar diagramnya di papan tulis, berarti Anda belum memahami domain Anda, dan bursa akan mengajari Anda dengan cara yang keras. Simpan transisinya, bukan hanya keadaan saat ini. Log event yang bersifat append-only tentang apa yang terjadi mengalahkan satu kolom status tunggal yang bisa diubah, karena log bisa diputar ulang dan kolom hanyalah tebakan.
Idempotensi bukanlah pilihan
Jaringan melakukan retry. Logika retry Anda sendiri melakukan retry. Seorang pengguna menekan "Beli" dua kali. Salah satu dari ini bisa mengirim order yang sama dua kali, dan di sebuah bursa order duplikat adalah uang sungguhan. Pertahanannya adalah kunci idempotensi yang dibuat di sisi klien — kebanyakan bursa menyebutnya clientOrderId atau newClientOrderId — yang dilekatkan pada setiap order pada momen pengguna mengonfirmasi, sebelum permintaan pernah meninggalkan perangkat atau server.
Aturan yang kami tegakkan: kunci dibuat sekali, dipersistensi secara lokal sebelum panggilan jaringan, dan digunakan kembali persis pada setiap retry. Jika percobaan pertama timeout tanpa respons, retry membawa kunci yang sama, dan bursa akan membuat order sekali saja atau mengembalikan yang sudah ada. Bagaimanapun juga Anda konvergen pada satu order tunggal. Buat kunci setelah kegagalan dan Anda telah membangun mesin penghasil order duplikat.
Ini berlaku juga untuk pembatalan dan modifikasi. Permintaan pembatalan juga harus idempoten: membatalkan order yang sudah dibatalkan adalah no-op yang mengembalikan sukses, bukan error yang memicu loop retry lain. Kami memperlakukan "keadaan akhir yang diinginkan sudah tercapai" sebagai sukses di mana pun. Terdengar sepele. Namun ini menghilangkan seluruh kategori badai retry.
Intinya
Persistensikan kunci idempotensi sebelum Anda bertindak, bukan setelah Anda berhasil. Celah antara "saya sudah mengirimnya" dan "saya tahu itu berhasil" adalah persis tempat uang menghilang, dan kunci itu adalah satu-satunya hal yang menutup celah tersebut.
Rekonsiliasi tanpa henti: WebSocket adalah petunjuk, bukan kebenaran
Bursa mengirim update order melalui WebSocket, dan itu luar biasa ketika bekerja: fill di bawah satu detik, kuantitas partial-fill langsung, konfirmasi pembatalan. Namun WebSocket adalah aliran yang bisa Anda lewatkan. Anda akan terputus. Pesan tiba tidak berurutan, hilang saat reconnect, atau sekadar tidak pernah datang karena bursa sedang menurun kualitasnya. Jika keadaan order Anda hanya apa pun yang terakhir kali dikatakan socket, aplikasi Anda dengan percaya diri salah pada saat socket tersendat.
Jadi kami menjalankan dua loop. Loop cepat mengonsumsi WebSocket dan menerapkan event secara optimistis. Loop lambat melakukan polling API REST — GET /order dan endpoint open-orders — pada interval tertentu dan setelah setiap reconnect, lalu merekonsiliasi snapshot itu terhadap keadaan lokal. REST adalah sumber kebenaran; socket adalah petunjuk berlatensi rendah. Ketika keduanya tidak sepakat, REST menang dan kami mencatat divergensinya, karena sebuah divergensi adalah laporan bug yang ditulis oleh sistem untuk Anda.
Urutan itu penting. Kebanyakan bursa memberi cap update dengan nomor urut monoton atau updateTime. Kami membuang event apa pun yang lebih lama daripada yang terakhir diterapkan untuk order tersebut, yang membuat pengiriman tidak berurutan tidak berbahaya. Saat reconnect, langkah yang benar adalah: subscribe ulang, segera tarik snapshot REST penuh untuk mengisi celah, lalu percayai lagi aliran langsung. Lewati snapshot dan Anda melewatkan fill yang terjadi selama dua detik Anda offline.
Intinya
Jangan pernah biarkan feed real-time menjadi satu-satunya penulis keadaan Anda. Socket membuat Anda cepat; loop rekonsiliasi membuat Anda benar. Anda butuh keduanya, dan ketika keduanya berkonflik, benar mengalahkan cepat setiap kali.
Partial fill dan diskoneksi: dua hal yang menyingkap Anda
Partial fill adalah tempat aplikasi naif diam-diam berbohong. Order untuk 1,0 BTC yang terisi 0,3, lalu 0,4, kemudian menggantung bukanlah "terbuka" dan juga bukan "selesai." Itu adalah PARTIALLY_FILLED dengan 0,7 tereksekusi dan 0,3 tersisa, dan setiap angka itu harus dilacak dari event fill individual, bukan disimpulkan dari satu field status tunggal. Kami mengakumulasi kuantitas tereksekusi dan harga rata-rata tertimbang volume dari fill-fill diskret, sehingga posisi selalu bisa direkonstruksi dari primitif. Seorang pengguna yang melihat "terisi" perlu tahu terisi berapa banyak, pada harga rata-rata berapa. Kurang dari itu adalah setengah kebenaran.
Diskoneksi memperparah ini. Terputus offline di tengah fill dan WebSocket tidak pernah memberi tahu Anda tentang 0,4 itu, sehingga keadaan lokal Anda basi. Inilah tepatnya mengapa disiplin reconnect-lalu-snapshot ada: snapshot REST mengembalikan kuantitas tereksekusi kumulatif, dan loop rekonsiliasi menutup celah tanpa pengguna pernah melihat angka yang salah. Mode kegagalan yang kami rancang untuk dilawan bukanlah "aplikasi crash" — crash itu jujur. Melainkan "aplikasi terus berjalan dan menampilkan sesuatu yang palsu."
Intinya
Lacak kuantitas tereksekusi kumulatif sebagai fakta utama dan turunkan segala hal lain darinya. Jangan pernah menyimpan "sisa" sebagai field independen yang Anda ubah. Turunkan (dipesan − tereksekusi) sehingga tidak bisa menyimpang dari realitas.
Jujur di bawah tekanan mengalahkan pintar
Ada godaan yang terus-menerus untuk menjadi pintar: memprediksi fill sebelum konfirmasi, menyembunyikan error "sementara", memuluskan sebuah reconnect sehingga UI tidak pernah berkedip. Kami telah belajar untuk menahan sebagian besar godaan itu. Seorang operator trading tidak menginginkan kebohongan yang mulus; mereka ingin tahu, dengan jujur, bahwa koneksi baru saja terputus dan keadaan sedang direkonsiliasi. Keadaan "menyambung ulang, memverifikasi order" yang terlihat lebih berharga daripada UI mulus yang ternyata menampilkan kebenaran kemarin.
Secara konkret, kami memunculkan ketidakpastian alih-alih menutupinya. Ketika keadaan lokal dan bursa belum direkonsiliasi baru-baru ini, aplikasi mengatakannya. Ketika sebuah order berstatus CANCEL_REQUESTED tetapi belum dikonfirmasi dibatalkan, aplikasi menampilkan persis itu, bukan CANCELED yang prematur. UI optimistis boleh untuk aplikasi to-do. Untuk uang, optimisme adalah beban yang Anda bayar dalam bentuk tiket dukungan dan kepercayaan yang hilang.
Intinya
Ketika ragu, tampilkan keraguannya. Hal paling menenangkan yang bisa dilakukan aplikasi finansial di bawah tekanan adalah menolak untuk menebak.
Pikiran penutup
Tidak ada satu pun dari ini yang glamor. Tidak ada demo di mana "kunci idempotensi kami dipersistensi sebelum panggilan jaringan" mendapat tepuk tangan. Namun keandalan bukanlah fitur yang Anda tambahkan belakangan. Ia adalah rangkaian keputusan yang Anda buat di awal dan menolak untuk kompromi, lalu Anda pertahankan setiap kali sebuah jalan pintas tampak menggoda. Bangun sistem yang mengatakan kebenaran ketika segalanya sedang terbakar, dan bagian-bagian yang cepat dan cantik akan mengurus dirinya sendiri.