C++ ile Kripto Ticaret Botu Geliştirmek / Bölüm 4: Alım-Satım Stratejileri ve Karar Mekanizması
Serinin üçüncü bölümünde, statik kütüphane bağımlılıklarını dinamik CMake modülleri haline getirmiş, kurulumu bir kabuk betiği (install.sh) ile sadeleştirmiş ve sistemi Orange Pi Zero 2 üzerinde ayağa kaldırmıştık. Ancak o yazının sonunda belirttiğim gibi, işin en can alıcı ve belirsiz tarafı henüz ortada yoktu: Alış-satış karar mekanizması.
Bir ticaret botu yazarken en sık düşülen tuzak, doğrudan karmaşık makine öğrenmesi modellerine veya onlarca indikatörün kesiştiği aşırı uyarlanmış (overfitted) stratejilere yönelmektir. Bu bölümde; donanım kısıtlarını gözeterek geliştirdiğim alım-satım karar motorunun mimarisini, veri akışını ve strateji modellemesini nasıl kurguladığımı paylaşıyorum.
1. Veri Akışı ve Zaman Pencereleri (Tick vs Kline)
Bir ticaret motorunun karar verebilmesi için iki temel veri tipine ihtiyaç vardır:
- Anlık Fiyat Akışı (Ticker/OrderBook): WebSocket üzerinden milisaniyeler seviyesinde gelen anlık alım-satım emirleri.
- Zaman Serisi Mumları (Kline/Candlestick): Belirli periyotlarda (1m, 5m, 15m, 1h) oluşmuş Açılış, Yüksek, Düşük, Kapanış (OHLCV) değerleri.
Orange Pi üzerinde düşük işlemci ve bellek kullanımı hedeflediğim için her milisaniyelik fiyat dalgalanmasında işlem yapmak (High-Frequency Trading) yerine, 5 dakikalık ve 15 dakikalık mum kapanışları üzerinden karar veren bir mimari kurdum. Bu yaklaşım hem borsa API istek sınırlarına (Rate Limit) takılmayı engelliyor hem de ağ gecikmelerinden kaynaklanan kaymaları (slippage) minimize ediyor.
2. Mum Verisinin C++ Tarafında Modellenmesi
İlk adım olarak borsa REST API’sinden dönen JSON yanıtını ayrıştırıp bellekte tutacak hafif bir veri yapısı tanımladım:
#include <cstdint>
#include <string>
#include <vector>
struct Candle {
uint64_t open_time;
double open;
double high;
double low;
double close;
double volume;
uint64_t close_time;
};
// Sürekli bellek tahsisinden (reallocation) kaçınmak için dairesel tampon (ring buffer) mantığı
class CandleSeries {
public:
explicit CandleSeries(size_t max_size = 200) : max_size_(max_size) {
candles_.reserve(max_size_);
}
void add_candle(const Candle& c) {
if (candles_.size() >= max_size_) {
candles_.erase(candles_.begin());
}
candles_.push_back(c);
}
const std::vector<Candle>& get_candles() const { return candles_; }
size_t size() const { return candles_.size(); }
private:
size_t max_size_;
std::vector<Candle> candles_;
};
Burada bellek parçalanmasını (memory fragmentation) engellemek adına std::vector kapasitesini baştan reserve() ile ayırmak, uzun süre açık kalan bir gömülü cihazda kritik bir fark yaratıyor.
3. Strateji Motoru Arayüzü: IStrategy
Farklı stratejileri (SMA Kesişimi, RSI Aşırı Alım/Satım, Grid Trading) bot çekirdeğinden bağımsız olarak test edebilmek için soyut bir strateji arayüzü tanımladım:
enum class Signal {
HOLD,
BUY,
SELL
};
class IStrategy {
public:
virtual ~IStrategy() = default;
virtual Signal evaluate(const CandleSeries& series) = 0;
virtual const char* get_name() const = 0;
};
4. Uygulanan Örnek Strateji: Üstel Hareketli Ortalama (EMA Cross)
Basit Hareketli Ortalama (SMA) geçmiş verilere eşit ağırlık verirken, Üstel Hareketli Ortalama (EMA) son fiyatlara daha fazla ağırlık verir ve trend dönüşlerini daha erken yakalar.
Matematiksel formülü: $$\text{EMA}{\text{bugün}} = (\text{Fiyat}{\text{bugün}} \times \alpha) + (\text{EMA}_{\text{dün}} \times (1 – \alpha))$$ Burada ağırlık çarpanı $\alpha = \frac{2}{N + 1}$ şeklindedir ($N$: periyot).
C++ implementasyonu:
#include <numeric>
#include <cmath>
class EmaCrossoverStrategy : public IStrategy {
public:
EmaCrossoverStrategy(int short_period = 9, int long_period = 21)
: short_p_(short_period), long_p_(long_period) {}
const char* get_name() const override { return "EMA_Cross_9_21"; }
Signal evaluate(const CandleSeries& series) override {
const auto& candles = series.get_candles();
if (candles.size() < static_cast<size_t>(long_p_ + 2)) {
return Signal::HOLD; // Yeterli veri yok
}
double prev_short_ema = calculate_ema(candles, short_p_, 1);
double curr_short_ema = calculate_ema(candles, short_p_, 0);
double prev_long_ema = calculate_ema(candles, long_p_, 1);
double curr_long_ema = calculate_ema(candles, long_p_, 0);
// Alttan yukarı kesişim (Golden Cross) -> AL
if (prev_short_ema <= prev_long_ema && curr_short_ema > curr_long_ema) {
return Signal::BUY;
}
// Üstten aşağı kesişim (Death Cross) -> SAT
if (prev_short_ema >= prev_long_ema && curr_short_ema < curr_long_ema) {
return Signal::SELL;
}
return Signal::HOLD;
}
private:
int short_p_;
int long_p_;
double calculate_ema(const std::vector<Candle>& candles, int period, int offset) {
double multiplier = 2.0 / (period + 1);
size_t end_idx = candles.size() - 1 - offset;
size_t start_idx = end_idx - period + 1;
// Başlangıç için SMA hesapla
double ema = 0.0;
for (size_t i = start_idx - period; i < start_idx; ++i) {
ema += candles[i].close;
}
ema /= period;
// Ardından EMA'yı yürüt
for (size_t i = start_idx; i <= end_idx; ++i) {
ema = (candles[i].close - ema) * multiplier + ema;
}
return ema;
}
};
5. Risk Yönetimi ve Pozisyon Büyüklüğü (Capital Allocation)
Bir ticaret algoritmasında sinyalin kalitesi kadar önemli olan diğer unsur risk yönetimidir. Bot sinyal BUY verdiğinde tüm bakiyeyi tek seferde oyuna sokmak piyasa ani tersine döndüğünde tasfiyeye (liquidation) sebep olur.
Bu nedenle karar motoruna iki sabit kural ekledim:
- İşlem Başına Maksimum Risk: Toplam portföyün en fazla %2’si kadar zarar göze alınır.
- Stop-Loss / Take-Profit Seviyeleri: Alış fiyatının altına sabit %1.5 zarar durdurma ve %3.0 kâr al emri otomatik hesaplanarak hafızada tutulur.
6. Sırada Ne Var?
Karar mekanizmasının mantığını ve C++ tarafındaki sınıf hiyerarşisini oturttuktan sonra, bir sonraki aşama bu sinyallerin borsa tarafında gerçek ya da test emirlerine (Testnet) dönüştürülmesi ve veritabanı (SQLite) üzerinde işlem geçmişi olarak kaydedilmesidir.
Bir sonraki yazıda, bu sürecin genel bir muhasebesini yaparak C++ ile böyle bir proje geliştirmenin getirdiği artıları ve eksileri derli toplu şekilde ele alacağım.