Model Pruning for Low-Latency Trading Inference
DeepLOB makalemiz dağıtım bölümünü üç maddeyle bitiriyor - ONNX artı TensorRT, INT8 nicemleme, FPGA - ve bunların hiçbirini ele almıyoruz. Bu makale, her üçünün de altında yatan ilk sorunun eksik çözümüdür: Model olması gerekenden daha büyük. Sinir ağı budaması gereksiz parametreleri ortadan kaldırır ve literatürdeki ilginç iddia, bunun hafıza tasarrufu sağladığı değil, ağırlıkların %10-20'sini tutan bir alt ağın yoğun modelin doğruluğunu eşleştirebileceğidir.
Gecikmenin önemli olduğu, blogun zaten yaptığı bir durumdur - mesajlaşma yolunda ZigBolt ve başa baş aritmetiğiyle IPC vergisi - ve spread modelleme zaten hızlı ama biraz daha kötü ile yavaş ama daha iyi arasında bir dengeye sahip ve çıkarım gecikmesi satırına sahip bir gradyan artırmaya karşı derin öğrenme tablosuyla tamamlanıyor. Hiçbirinin kapsamadığı şey, belirli bir modeli nasıl küçülteceğinizdir. Bir alıntılama döngüsündeki aşamalardan model çıkarımı tamamen bizim kontrolümüz altındadır; taşıma ayakları, algotrading için veri iletişimi bölümünde tekrarlanabilir p50/p95/p99 numaralarıyla kaplıdır.
Bu makalenin içeriği: büyüklük budama, yapılandırılmış budama, Yinelemeli Büyüklük Budama, hareket budama, bilgi damıtma ve bir ticari MLP'ye uygulanan NVIDIA 2:4 yarı yapılandırılmış seyreklik için matematik ve çalışma kodu.
Ne değildir: ölçülen bir sonuç. Bu blogdaki her ampirik makale bir kaynak çizgisi veya tamamlayıcı bir repo taşıyor ve bu makale henüz ikisini de taşımıyor. Azlık-doğruluk-gecikme eğrisi aşağıda alıntı yapılacak bir tablo olarak değil, çalıştırılacak bir deney olarak belirtilmiştir. Buradaki her şeyi yöntem olarak ve sayıları da beklemede olarak değerlendirin.
Budama Size Ne Kazandırır?

Kısıtlama bir boyuttadır. Orta frekanslı bir modeli düşünün - sipariş defteri özellikleri üzerinde 2048 gizli birime sahip 4 katmanlı bir MLP:
İçin , , , , bu kabaca 12,6 milyon parametre anlamına gelir - float32'de yaklaşık 48 MB. L2 genellikle 1-4 MB boyutunda olduğundan ağırlıklar uymuyor; her ileri geçişte daha uzaktan yayınlanırlar. Bunların %95'ini budadığınızda kabaca 630.000 etkin parametreye ve 2.4 MB'a ulaşırsınız ki bu da uygundur.
Bunun duvar saati zamanına dönüşüp dönüşmeyeceği, çekirdeğin hafızaya bağlı olup olmamasına bağlıdır ve bu, boyut sorusundan ziyade aritmetik yoğunluk sorusudur. Backtest motor hız merdiveni, bir ceza faktörü ileri sürmek yerine tavan hattı modelini (Williams, Waterman & Patterson) ölçülü bir örnek üzerinden çalıştırır; Aynı çerçeve burada da geçerlidir ve aynı disiplin şunları yapmalıdır: Hızlanma talebinde bulunmadan önce taşınan baytları ölçmelidir.
Budamanın Temelleri

Yapılandırılmamış Budama
En basit yaklaşım: bireysel ağırlıkları büyüklüklerine göre sıfıra ayarlamak. Bir ağırlık matrisi verildiğinde , ikili maske oluşturun öyle ki:
Neresi İstenilen seyreklik seviyesine ulaşmak için seçilen bir eşiktir :
Budanmış matris , ile Hadamard ürünü. Sezgi, sıfıra yakın ağırlıkların katmanın çıktısına çok az katkıda bulunduğu yönündedir.
Sorun, seyreklik numarasını yanlış okumak kolay olduğu için açıkça ifade edilmiştir: yapılandırılmamış seyreklik, standart donanımda bir hızlanma anlamına gelmez. Seyrek çekirdeklere veya seyreklik desteğine sahip donanıma geçmediğiniz sürece, %90'ı sıfır olan bir matris, aynı sayıda çoklu birikimler yayınlamaya devam eder. Aşağıdaki kod yazdırıldığında Sparsity: 90.0%, bu bir sıfır sayısıdır - 10x herhangi bir şey değildir ve yoğun bir CPU GEMM'de de 1,01x değildir. Zaman kazandıran yollar, yapılandırılmış budama (daha küçük matrisler) ve 2:4 yarı yapılandırılmış seyrekliktir (donanım desteği), her ikisi de aşağıdadır.
Yapılandırılmış Budama
Yapılandırılmış budama tüm nöronları, kanalları veya dikkat kafalarını ortadan kaldırır. Doğrusal bir katman için ile , nöronun çıkarılması sıfırlar -inci sıra ve -th elemanı :
En küçük nöronlar -normal önce git. Bu, gerçekten daha küçük matrisler üreten varyanttır; ancak yalnızca katmanları gerçekten yeniden oluşturursanız. Satırları sıfırlamak ve tensörü orijinal şeklinde bırakmak FLOP sayısında hiçbir şeyi değiştirmez; uygulama bölümündeki yeniden oluşturma adımı, maskeyi bir maskeye dönüştüren adımdır. matris.
Evrişimli katmanlar için analog, filtre budamasıdır. Verilen çıkış filtresinin önemi şu:
Filtrenin çıkarılması FLOP'ları orantılı olarak azaltarak tüm çıkış kanalını ortadan kaldırır.
Piyango Bileti Hipotezi

2019'da Frankle ve Carbin, Piyango Bileti Hipotezini (LTH) tanıttı: Rastgele başlatılan yoğun bir ağ içinde, orijinal başlatmadan eğitilen, karşılaştırılabilir sayıda yinelemede tam ağın doğruluğuyla eşleşen seyrek bir alt ağ - bir "kazanan bilet" vardır.
Resmi olarak düşünün ile başlatıldı . Yakınsama eğitimi aldıktan sonra elde ederiz ve bir budama maskesi türetin . LTH var olduğunu belirtiyor öyle ki:
ile . Orijinal deneyler, kazanan biletlerin parametrelerin %10-20'sini koruduğu MNIST ve CIFAR-10 üzerinde yapıldı. Sipariş defteri verilerinin varsayım yoluyla aktarılmasıyla ilgili hiçbir şey yok - LOB özellikleri durağan değildir ve etiket neredeyse gürültüdür, bu da görüntü sınıflandırmasından tam olarak önemli olabilecek şekillerde farklı bir rejimdir.
Yinelemeli Büyüklük Budama (IMP)
Bilet IMP tarafından bulunur:
- Ağı şununla başlatın: .
- Yakınsama eğitimi, elde etme .
- Budama en küçük büyüklükteki ağırlıkların maske oluşturması .
- Hayatta kalan ağırlıkları değerlerine sıfırlayın (geri sarma).
- Maskeli ağ ile 2. adımdan itibaren tekrarlayın.
Her turda kuru erik fraksiyonu (genellikle %20), yani sonra turlar parametrelerden biri hayatta kalır. 10 turdan sonra kabaca %10,7'si kalıyor.
Ticaret Modelleriyle İlgili Üç Hipotez, Hiçbiri Test Edilmedi
LTH'nin özellikle piyasa verileri üzerinde iyi çalışması gerektiğini iddia etmek cazip geliyor. Bu tür üç argüman ortaya çıkıyor; üçü de hipotezdir ve bunları gerçek olarak belirtmek, bu blogun kaçınmak için var olduğu başarısızlık modudur.
- Finansal sinyaller seyrektir. Emir defteri anlık görüntülerinin çoğu gürültüden oluşur, dolayısıyla seyrek bir alt ağ, doğal olarak seyrek bir sinyalle hizalanabilir. Test edilebilir: IMP'yi aynı seyrekliğe sahip rastgele bir maskeyle karşılaştırın; eğer işi seyrekliğin kendisi yapıyorsa, rastgele maske çok geride olmamalıdır.
- Kazanan biletler rejimler arasında genelleşir. Bu, piyasalarla ilgili, arkasında herhangi bir alıntı bulunmayan ampirik bir iddiadır ve üçü arasında en ilginç olanıdır. HMM'lerle rejim tespiti adresinden rejim etiketlerine karşı doğrudan test edilebilir: A rejimindeki bileti bulun, B rejiminde yeniden eğitin ve B'de yerel olarak bulunan bir biletle karşılaştırın.
- Yetersizlik düzenli hale gelir. Daha düşük etkili kapasite, mikroyapı gürültüsüne uyumu azaltabilir; bu, budanmış modelin örnek dışı boşluğunun, yalnızca karşılaştırılabilir olmakla kalmayıp, yoğun modelinkinden daha küçük olmasıyla ortaya çıkar.
Uygulama: Ticaret MLP'sini Budama

Temel Model
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
from copy import deepcopy
class TradingMLP(nn.Module):
"""MLP for mid-price direction prediction from order book features."""
def __init__(self, input_dim=100, hidden_dim=2048,
num_layers=4, output_dim=3):
super().__init__()
layers = []
dims = [input_dim] + [hidden_dim] * (num_layers - 1) + [output_dim]
for i in range(len(dims) - 1):
layers.append(nn.Linear(dims[i], dims[i + 1]))
if i < len(dims) - 2:
layers.append(nn.BatchNorm1d(dims[i + 1]))
layers.append(nn.ReLU())
layers.append(nn.Dropout(0.1))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
def count_parameters(self):
return sum(p.numel() for p in self.parameters())
model = TradingMLP(input_dim=100, hidden_dim=2048,
num_layers=4, output_dim=3)
print(f"Total parameters: {model.count_parameters():,}")
Yapılandırılmamış Büyüklükte Budama
def apply_unstructured_pruning(model, sparsity=0.9):
"""Apply global unstructured L1 pruning to all Linear layers."""
parameters_to_prune = []
for name, module in model.named_modules():
if isinstance(module, nn.Linear):
parameters_to_prune.append((module, 'weight'))
prune.global_unstructured(
parameters_to_prune,
pruning_method=prune.L1Unstructured,
amount=sparsity,
)
return model
def compute_sparsity(model):
"""Fraction of zero weights. Note: a *count*, not a speedup."""
total, zeros = 0, 0
for name, param in model.named_parameters():
if 'weight' in name:
total += param.numel()
zeros += (param == 0).sum().item()
return zeros / total if total > 0 else 0
pruned_model = apply_unstructured_pruning(deepcopy(model), sparsity=0.9)
print(f"Sparsity: {compute_sparsity(pruned_model):.1%}")
Yapılandırılmış Budama, Onu Gerçek Hale Getiren Yeniden Yapılanmayla
Satırları maskelemek işin yarısıdır. Hızlanmayı sağlayan kısım, her katmanı küçültülmüş şekliyle yeniden oluşturmaktır; bu, kaldırma işleminin ileriye doğru yayılması anlamına gelir: satırın düşürülmesi katman sütunu da düşürür katman ve kanal herhangi birinin BatchNorm1d aralarında.
def apply_structured_pruning(model, fraction=0.75):
"""Mask entire neurons by L2-norm of their weight rows."""
for name, module in model.named_modules():
if isinstance(module, nn.Linear) and module.out_features > 10:
prune.ln_structured(
module, name='weight', amount=fraction, n=2, dim=0
)
return model
def rebuild_pruned_mlp(model):
"""
Physically shrink a structurally pruned TradingMLP.
Walks the Sequential once. For each Linear: drop the input columns
the previous layer no longer emits, then drop its own dead output
rows. BatchNorm1d channels follow the preceding Linear's survivors.
"""
new_layers = []
keep_in = None # surviving output indices of the previous Linear
for layer in model.network:
if isinstance(layer, nn.Linear):
if prune.is_pruned(layer):
prune.remove(layer, 'weight')
W, b = layer.weight.data, layer.bias.data
keep_out = (W.norm(dim=1) > 0).nonzero(as_tuple=True)[0]
W = W[keep_out]
if keep_in is not None:
W = W[:, keep_in]
new = nn.Linear(W.shape[1], W.shape[0])
new.weight.data = W.clone()
new.bias.data = b[keep_out].clone()
new_layers.append(new)
keep_in = keep_out
elif isinstance(layer, nn.BatchNorm1d):
new = nn.BatchNorm1d(len(keep_in))
new.weight.data = layer.weight.data[keep_in].clone()
new.bias.data = layer.bias.data[keep_in].clone()
new.running_mean = layer.running_mean[keep_in].clone()
new.running_var = layer.running_var[keep_in].clone()
new.num_batches_tracked = layer.num_batches_tracked.clone()
new_layers.append(new)
else: # ReLU, Dropout -- shape-agnostic, reuse as is
new_layers.append(layer)
rebuilt = deepcopy(model)
rebuilt.network = nn.Sequential(*new_layers)
return rebuilt
Buna güvenmeden önce, blogun geri kalanındaki denklik kapılarıyla aynı ruhla kontrol edilmesi gereken iki şey:
- Şekiller.
rebuiltgizli boyutları şu adreste göstermelidir: — 512 içinfraction=0.75, - ve iç matrislerin her iki boyutu da küçüldüğünden ikinci dereceden düşen bir parametre sayısı. - Çıkışlar. Giriş
eval()mod,rebuilt(x)maskeli modelinkiyle eşleşmelidirrebuilt-aynı partide kayan nokta toleransına ücretsiz çıktı. Aksi takdirde sütun yayılımı yanlıştır ve her alt sayı düşündüğünüzden farklı bir modeli ölçüyor demektir.
Satır hayatta kalma testi, maskelenmiş bir satırın tam olarak sıfır olduğunu ve canlı bir satırın olmadığını varsayar. Bu şu anlama gelir: ln_structured çıktı; Başka bir prosedürün gerçekten tamamen sıfır canlı bir nöron üretmesi durumunda bu geçerli olmayacaktır; bu nedenle, norm testine körü körüne güvenmek yerine, hayatta kalanın talep edilen fraksiyona göre sayıldığını iddia edin.
Yinelemeli Büyüklük Budama (Piyango Bileti Arama)
def lottery_ticket_search(model_cls, model_kwargs, train_fn, eval_fn,
rounds=10, prune_rate=0.2, device='cpu'):
"""
Iterative Magnitude Pruning to find a winning ticket.
Parameters
----------
model_cls : class -- model constructor
model_kwargs : dict -- constructor arguments
train_fn : callable -- train_fn(model) trains the model in-place
eval_fn : callable -- eval_fn(model) returns out-of-sample accuracy
rounds : int -- number of pruning rounds
prune_rate : float -- fraction of surviving weights pruned per round
"""
model_init = model_cls(**model_kwargs).to(device)
theta_0 = deepcopy(model_init.state_dict())
mask = {}
for name, param in model_init.named_parameters():
if 'weight' in name:
mask[name] = torch.ones_like(param, dtype=torch.bool)
results = []
for round_idx in range(rounds):
model = model_cls(**model_kwargs).to(device)
state = deepcopy(theta_0)
for name in mask:
state[name] = state[name] * mask[name].float()
model.load_state_dict(state)
train_fn(model)
acc = eval_fn(model)
surviving = sum(m.sum().item() for m in mask.values())
total = sum(m.numel() for m in mask.values())
sparsity = 1.0 - surviving / total
results.append({
'round': round_idx,
'accuracy': acc,
'sparsity': sparsity,
'surviving_params': int(surviving)
})
print(f"Round {round_idx}: acc={acc:.4f}, "
f"sparsity={sparsity:.1%}")
all_weights = []
for name, param in model.named_parameters():
if name in mask:
alive = param.data.abs()[mask[name]]
all_weights.append(alive.flatten())
all_weights = torch.cat(all_weights)
k = int(len(all_weights) * prune_rate)
if k == 0:
break
threshold = all_weights.kthvalue(k).values.item()
for name, param in model.named_parameters():
if name in mask:
mask[name] = mask[name] & (
param.data.abs() >= threshold
)
return results, mask
results bu makalenin size borçlu olduğu seyreklik-doğruluk eğrisinin hammaddesidir. eval_fn temizlenmiş bölünmelerde gerçekten örnek dışı olmalıdır - örnek içinde puanlanan bir IMP çalışması hiçbir şey ifade etmeyen güzel bir eğri rapor edecektir.
Ölçmek

Gecikme, blogun geri kalanıyla aynı koşum kuralıyla ölçülür - ısınma hariç, en iyisi N, ortalama yerine p50/p95/p99 rapor edilir - ve kodla birlikte bu protokol Polars vs pandas içindedir. Budamaya özgü üç nokta:
- Maskelenmiş olanı değil, yeniden oluşturulmuş modeli kıyaslayın. Parti boyutu 1'deki maskeli model, yoğun şekli ölçer.
- Toplu iş boyutunu bildirin. Grup 1 (alıntılama döngüsü) ve grup 256 (araştırma taraması), belleğe bağlı/hesaplamaya bağlı satırın farklı taraflarında bulunur ve budama onlara farklı şekilde yardımcı olur.
- Belirtilen etiket tanımıyla aynı ufukta, aynı bölünmede doğruluğu raporlayın. Eşleştirme doğruluğu sütununun bulunmadığı bir gecikme tablosu, modelin tamamen silinmesine yönelik bir argümandır.
İleri Teknikler

Bilgi Damıtmayla Budama
Tek başına budama ve ince ayar yapmak yerine, orijinal yoğun modeli bir öğretmen olarak kullanın. Budanan öğrenci, görev kaybı ve öğretmenin çıktı dağılımından KL farklılığının birleşimini en aza indirir:
Neresi Ve öğretmen ve öğrenci logitleri, sıcaklık ve hedefleri dengeler. faktör, aksi takdirde küçülen damıtma gradyanlarını yeniden ölçeklendirir. .
def distillation_loss(student_logits, teacher_logits, labels,
temperature=3.0, alpha=0.5):
"""Combined task + distillation loss."""
task_loss = nn.CrossEntropyLoss()(student_logits, labels)
soft_student = nn.functional.log_softmax(
student_logits / temperature, dim=-1
)
soft_teacher = nn.functional.softmax(
teacher_logits / temperature, dim=-1
)
kd_loss = nn.functional.kl_div(
soft_student, soft_teacher, reduction='batchmean'
)
return (1 - alpha) * task_loss + alpha * (temperature ** 2) * kd_loss
Hareket Budama
Hareket budaması (Sanh ve diğerleri, 2020), mutlak büyüklüğe göre budamak yerine, eğitim sırasında sıfıra doğru hareket eden ağırlıkları budamaktadır. Önem puanı, gradyan ağırlık çarpımını toplar:
Negatif puanlı ağırlıklar budanır. Büyüklük budama konusundaki argüman özellikle ince ayar ile ilgilidir: Önceden eğitilmiş bir modeli uyarladığınızda, büyüklük dağılımı ön eğitim görevi tarafından şekillendirilir, dolayısıyla büyüklük eski bir önem sinyalidir ve seyahat yönü daha yeni bir sinyaldir. Dönen pencerelerde yeniden eğitilen bir ticaret modeli için bu, sıfırdan eğitimden daha yaygın bir durumdur.
NVIDIA 2:4 Yapılandırılmış Seyreklik
Amper ve üzeri NVIDIA GPU'lar donanımda 2:4 yapısal seyrekliği destekler: her 4 bitişik ağırlıktan tam olarak 2'si sıfır olmalıdır.
Bu, donanımın gerçekten ödüllendirdiği ince taneli seyrekliğin bir biçimidir, bu nedenle yapılandırılmamış budamadaki %90 sıfır sayısından daha önemlidir. Kısıtlama küresel olmaktan ziyade yereldir - her dört kişiden hangisinin hayatta kalacağı umrunda değildir - dolayısıyla küresel bir maskeyi sabitlemekten çok daha zayıf bir kısıtlamadır, ancak sunulan tek seyreklik seviyesi %50'dir.
from torch.ao.pruning import WeightNormSparsifier
sparsifier = WeightNormSparsifier(
sparsity_level=0.5,
sparse_block_shape=(1, 4),
zeros_per_block=2,
)
sparsifier.prepare(
model, config=[{"tensor_fqn": "network.0.weight"}]
)
sparsifier.step()
sparsifier.squash_mask()
Hızlanmanın gerçekleştirilmesi, seyrek tensör çekirdeklerini kullanmak için çıkarım yolunu gerektirir (bir ONNX dışa aktarma artı TensorRT yapısı) veya torch.sparse.to_sparse_semi_structured. Yoğun bir çalışma süresi aracılığıyla 2:4 maskeli bir modeli dışa aktarmak size doğruluk maliyeti getirir ve hiçbir fayda sağlamaz.
Üretim Dağıtımı

Doğrulama
Budanmış bir model, sıkıştırılmış eski bir model değil, yeni bir modeldir ve diğer adaylarla aynı kabul kapısından geçer: [ileriye doğru ilerleme optimizasyonu](/tr/blog/post/ileriye doğru yürüme optimizasyonu) başına yuvarlanan yeniden eğitim ve örnek dışı yeniden doğrulama, sönük Sharpe oranından seçim etkisi düzeltmesi ile. Bu düzeltme burada isteğe bağlı değildir - IMP, aday modellerin bir dizisini oluşturur, bu nedenle arama altında on turda en iyi görünen seyreklik düzeyi seçildi ve Sharpe'ın etkin deneme sayısı kadar söndürülmesi gerekiyor. "Sharpe %5'ten fazla düşerse reddet" gibi düz bir kural bu aritmetikte geçerli değildir, bu yüzden bu makalede böyle bir kural bulamazsınız.
Niceleme Yığınlama
Budama kuantizasyonla oluşur. %90 seyrek ve INT8'e nicemlenmiş bir modelin sıkıştırma oranı şu şekildedir:
48 MB'lık bir model 1,2 MB olur. Bu bir depolama iddiasıdır ve başka bir şey değildir. 1,2 MB modelinin aynı kararları üretip üretmediği, kendi cevabı olan ayrı bir sorudur ve bunu sormanın nedeni GPU hassas tuzağı'tir: bu blogda, tamamen makul görünen bir geriye dönük test hesaplamasında tek başına fp32'nin 211'lik göreceli bir hata ürettiği gösterilmiştir. INT8 bundan çok daha agresif bir indirgemedir. Ölçülen ve budanan bir modeli, fp32 yoğun modele karşı yalnızca niceliksel bir eşlik kapısının arkasına gönderin - karar anlaşması oranı ve ertelenmiş bir dönemde PnL deltası, bir garanti değil.
İzleme
Budanmış modeller dağıtım değişimine daha duyarlı olabilir. İzlemeye değer:
- Aktivasyon seyrekliği: Hayatta kalan nöronlar çoğunlukla sıfırlar yayarsa, etkili model amaçlanandan daha küçüktür ve muhtemelen bozulur.
- Yeniden eğitim sırasındaki eğim normları: Patlayan eğimler, hayatta kalan alt ağdan, kaldırılanları çok agresif bir şekilde telafi etmesinin istendiğini gösteriyor.
- Tahmin entropisi: Gürültülü mikro yapı verilerine aşırı güvenen, budanmış bir model muhtemelen eğitim rejimine uygundur.
Sonuç

Yöntemler iyice yerleşmiştir ve tarama tamamlanana kadar bu makalenin iddia ettiği tek şey budur. Yapılandırılmamış budama size bir seyreklik numarası verir ve hız vermez. Yapılandırılmış budama, ancak ve ancak katmanları maskelemek yerine yeniden oluşturursanız size hız kazandırır. Piyango Bileti Hipotezi, kompakt modelin aşırı parametreli modelin içinde zaten mevcut olduğunu öne sürüyor; ancak bu, sipariş defteri verilerinde değil, görüntü kıyaslamalarında gösterilmiştir ve yukarıda verilen piyasa verileri üzerinde "çalışması" gereken üç neden, bulgular değil, deneyler içeren hipotezlerdir.
Literatürden elde edilen pratik buluşsal yöntem, başlangıçtan itibaren küçük tasarlamak yerine, büyük olanı eğitmek ve budamaktır: büyük model, kayıp manzarasını daha etkili bir şekilde araştırır ve budama, önemli olan yolları korur. Bunun bir ticaret modeli için geçerli olup olmadığı, ne kadar seyrek ve ne kadar doğruluk maliyetiyle bir IMP süpürmesidir - ve bu makale, bu taramadan sonra, içindeki rakamlarla birlikte tekrar okunmalıdır.
Yazarlar
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.