No-code bir trading botunun vaadi basittir: bir formda stratejinizi tarif edersiniz, yazılım da onu sizin yerinize çalıştırır. Zor olan kısım form değil. Zor olan şu: kullanıcıya sunduğunuz her kolaylığın bedeli sistemin bir yerinde, genellikle beklemediğiniz bir yerde ödenir.
Bu konuda yanıldığım üç şey ve her birinin bize gerçekte ne öğrettiği aşağıda. Onları, ne kadar yanlış değerlendirdiğime göre sıraladım.
Önemli Noktalar
- Borsa API'leri, gürültülü şekilde başarısız olmaktan çok daha sık sessizce başarısız olur. İstediğinizden daha az veriyle gelen bir HTTP 200, karşısında tasarım yapmanız gereken başarısızlık türüdür.
- Simülatörün kendisini test etmeden hiçbir geriye dönük test sonucuna güvenmeyin. Temiz görünen bir sermaye eğrisi, bozuk bir çalışmadan da çıkabilir.
- Emir miktarları üzerinde yapılan kayan nokta aritmetiği, yalnızca belirli çiftlerde ortaya çıkan bir adım kayması hatası üretir.
- Kullanıcı fonlarını asla tutmamaya karar vermek, koca bir problem sınıfını ortadan kaldırır ve yerine daha küçük ama daha keskin bir tane koyar: sisteminiz hiçbir şeyi geri alamadan doğru çalışmak zorundadır.
- Kullanıcıya sunduğunuz her kolaylık, motorun bir yerinde bir kısıta dönüşür. Bu takas genellikle yapmaya değer, ama hiçbir zaman bedava değildir.
1. Başarısız bir isteğin başarısızlık gibi görüneceğini varsaydım
En çok zaman kaybettiren buydu ve herhangi bir geliştiriciyi ilk uyaracağım şey de bu.
Geriye dönük testleri çalıştırmak için borsalardan geçmiş mumları çekiyoruz. Naif zihinsel model şudur: bir istek ya başarılı olur ya hata verir. Ya verinizi alırsınız ya 200 dışı bir yanıt alıp onu ele alırsınız. Gerçek borsa API'leri yük altında böyle davranmaz. İstediğinizden daha az mumla HTTP 200 döndürürler ve bir şeyin ters gittiğine dair hiçbir işaret vermezler.
Kayıtlarımızdan gerçek bir vaka, sayılar da tam olarak bu: Uzun periyotlu bir geriye dönük test yaklaşık 536.000 mum istedi. Saat 16:34'te 58.000 mum aldı. Durum 200. Hata alanı yok. Uyarı yok. Aynı istek, hiç değiştirilmeden, saat 17:54'te tam 535.000 mumu getirdi.
Kontrol etmiyorsanız bunun ne anlama geldiğini düşünün. Motor, hedeflenen geçmişin yüzde on birlik kısmı üzerinde stratejiyi mutlulukla simüle eder, makul görünen bir sermaye eğrisi üretir ve bir sonuç raporlar. Kullanıcı, çıktının hiçbir yerinde belli olmayan bir şekilde yanlış olan bir rakam okur. Bu, çökmekten daha kötüdür. Çökmek dürüsttür.
Çözüm, kavram olarak sıkıcı ve atlanması kolaydı: veriyi çektikten sonra, aldığınız verinin gerçekten istediğiniz dönemi kapsayıp kapsamadığını doğrulayın ve taşıma katmanı her şeyin yolunda olduğunu söylese bile eksik veriyi bir başarısızlık olarak ele alın. Kapsam, varsayabileceğiniz değil, doğrulamanız gereken bir özelliktir.
İlkinin arkasında saklanan ikinci bir ders vardı. Kesintiyi tespit etmeye başladığımızda bariz hamle, çalışmayı başarısız saymaktı. Ama yukarıdaki sayılar bu kesintilerin geçici olduğunu gösteriyor: aynı istek bir saat sonra eksiksiz veri döndürdü. Baştan başarısız saymak, esasen borsa tarafındaki geçici bir aksama için kullanıcının kotasını boşa harcatırdı. Bu yüzden doğru davranış, başarısız saymadan önce bir kez tekrar denemektir. Bu da başarısızlık yolunu yeniden ele almayı gerektirdi, çünkü tekrar denenebilir bir durumun çıkışta kalıcı olarak başarısız işaretlenmemesi gerekiyordu.
Bundan iki kural çıktı ve ikisini de üçüncü taraf bir API'yle konuşan herhangi bir projeye taşırdım:
- Yalnızca durum kodunu değil, başarılı bir yanıtın biçimini de doğrulayın. Durum kodu size isteğin tamamlandığını söyler. Size yanıtın istediğiniz şey olduğunu söylemez.
- Ne yapacağınıza karar vermeden önce geçici başarısızlıkları kalıcı olanlardan ayırın. Oluştukları anda birbirinin aynısı görünürler ama zıt yanıtları hak ederler.
2. Bir geriye dönük test sonucunun gerçekten sonuç olduğunu varsaydım
İlişkili, ama ayrı bir hata. Ve kesinti probleminden daha derine iniyor.
Bir simülatör kurduğunuzda, test çabanızı stratejilere harcarsınız. DCA mantığı doğru mu davranıyor? Çıkışlar olması gereken yerde mi tetikleniyor? İlginç soru budur, o yüzden dikkat doğal olarak oraya gider.
Sorun şu ki, bozuk bir simülatör tam da çalışan bir simülatörün ürettiği gibi görünen çıktı üretir. Verinin yalnızca bir kısmını sessizce işleyen bir çalışma yine de bir rakam verir. Çıkış koşulları hiç değerlendirilmemiş bir çalışma yine bir rakam verir, üstelik şüpheli derecede düz bir rakam. Sonuçların tam olarak sıfır işlemle +0.00% olarak döndüğünü gördük. Bu, makinede bir şeylerin ters gittiğine dair olabilecek en yüksek sesli sinyaldir ve "bu strateji hiçbir şey yapmıyor" diye yanlış okunması en kolay olandır.
Bunu düzelten değişim, neyi test ettiğimizi değiştirmekti. Stratejiyi değil, simülatörü test edin. Somut olarak bu, stratejinin değiştirememesi gereken şeyler üzerinde doğrulama yapmak demektir: işlenen mum sayısının çekilen mum sayısıyla eşleşmesi, bilinen bir dönem üzerindeki bir çalışmanın gerçekten o dönemi kapsaması, tetiklenmesi garanti bir yapılandırmanın işlem üretmesi. Bu değişmezler geçerliyse, tuhaf görünen bir sonuç strateji hakkında gerçek bir bulgudur. Geçerli değillerse, sonuç takım elbise giymiş gürültüden ibarettir.
Simüle eden her şey için bunun genel biçimi şudur: gürültülü şekilde başarısız olamayan bir simülatör sessizce başarısız olur ve rakam üreten bir sistemde sessiz başarısızlık, var olan en kötü başarısızlık türüdür. İnsanlar rakamlara göre hareket eder. Yığın izlerine (stack trace) göre değil.
3. Emir boyutlandırmanın gerçekte ne kadar hassasiyet işi olduğunu küçümsedim
Bu, entelektüel olarak zor değil. Yalnızca göründüğünden çok daha büyük ve hataların pahalıya patladığı bir yerde saklanıyor.
Borsalar keyfi miktarları kabul etmez. Her işlem çiftinin miktar için bir adım büyüklüğü (step size) ve fiyat için bir tik büyüklüğü (tick size) vardır ve hizalanmayan emirler reddedilir. Bu yüzden sistemin hesapladığı her miktar, dışarı çıkmadan önce geçerli bir adıma yuvarlanmalıdır. Kulağa tek bir yardımcı fonksiyon gibi geliyor.
Sonra kayan nokta devreye girer. 0.29'u 0.01'lik bir adıma yuvarlamayı düşünün. Değeri ölçekleyip yukarı çıkarınca IEEE-754 size 28.999999999999996 verir. Bu 28'e yuvarlanır ve geri ölçeklenince 0.28 olur. Değer zaten adıma hizalıydı. Aritmetik onu tam bir adım aşağı taşıdı ve bunu sessizce, daha küçük bir emir yönünde yaptı. Düzeltmesi, ölçekleme adımında bir epsilon eklemektir. Bu epsilon, 0.289999 gibi gerçekten sınırın altındaki bir değerin hâlâ doğru şekilde aşağı yuvarlanacağı kadar küçük, ama gösterim gürültüsünü soğuracak kadar büyük olmalı.
Bu, bulunması gerçek bir hata ayıklama gerektiren tek satırlık bir düzeltmedir, çünkü belirti bir istisna değildir. Belirti, adım büyüklüğünün ikili gösterime göre nereye düştüğüne bağlı olarak bazı çiftlerde çıkan, bazılarında çıkmayan, amaçlanandan biraz daha küçük bir emirdir.
Daha geniş nokta ve bunu küçümsediğim bir şey olarak listelememin nedeni şu: bir trading sisteminde hassasiyet bir yardımcı fonksiyon değildir, bir iş kategorisidir. Adım büyüklükleri ve tik büyüklükleri çiftten çifte, borsadan borsaya ve spot ile futures arasında farklılık gösterir. Minimum emir değerleri, arayıp koda gömebileceğiniz tek bir rakam değildir. Borsadan sembol bazında gelir ve bazı borsalarda güncel fiyata bağlıdır. Borsanın çift bazında tanımladığı bir şey için tek bir sabit yazdığınızı fark ettiğiniz her an, gecikmeli fitili olan bir hata yazıyorsunuzdur.
Bunu kullanıcılardan saklamak yerine onlara gösteriyoruz, çünkü alternatifi, göremedikleri nedenlerle reddedilen bir emirdir.
4. Sevmeyi beklemediğim kısıt
Sonuncusu bir hata değil. Sonuçlarını iyi anlamda küçümsediğim bir karar.
Freya, kullanıcının trading fonlarını asla tutmaz. Botlar, borsa API anahtarları üzerinden işlem yapar. Bu anahtarların okuma ve işlem izni vardır, para çekme izni asla yoktur. Para, baştan sona kullanıcının kendi borsa hesabında kalır.
Bunun bir pazarlama noktası olacağını sanıyordum. Mimari bir nokta olduğu ortaya çıktı ve sistemi küçülttü. Bir saklama (custody) defteri yok, bir dışarıdakiyle mutabakat kurulacak dahili bir bakiye yok, para çekme kuyruğu yok, trading sermayesi için soğuk cüzdan politikası yok. Finansal yazılımdaki en zor problemlerin koca bir kategorisi tamamen yok, çünkü onları yaratan şeyi hiç üstlenmedik.
Onun yerine geleni daha dar ve daha keskin: sisteminiz gerçek zamanlı olarak doğru çalışmak zorundadır, çünkü hiçbir şeyi geri alamaz. Saklamayla, ilkesel olarak, dahili bir transferi tersine çevirebilirsiniz. Bir başkasının borsa hesabında emir vermek, gerçekleştiği anda kesindir. Bu, tüm mühendislik ağırlığını, sonradan mutabakat kurmaya değil, istek dışarı çıkmadan önce doğruluğa yükler. Bu takası tekrar yapardım.
Başta kendime söyleyeceklerim
- Her üçüncü taraf API'nin bazen yanlış şekilde başarılı olacağını varsayın ve ona ihtiyaç duymadan önce tespit mekanizması tasarlayın.
- Stratejiler hakkında doğrulamalar yazmadan önce simülatörünüz için değişmezlik testleri yazın. Simülatör testleri geçmeden strateji testleri değersizdir.
- Hassasiyeti bir yardımcı değil, bir alan olarak ele alın.
- Kısıtlarınızı erken seçin. Baştan kabul ettikleriniz, sonradan keşfettiklerinizden çok daha ucuzdur.
Bunların hiçbiri egzotik problem değildi. Planlama sırasında "küçük" diye kenara yazdığım sıradan mühendislik problemleriydi. İşte tam olarak bu yüzden pahalıya patladılar.
