From 5b52cd031a3aaa65a24a81ab4d089be004baa3bb Mon Sep 17 00:00:00 2001 From: loweel Date: Tue, 14 Oct 2025 19:23:59 +0200 Subject: [PATCH] Correlazione legala al segno di binance corretta. --- dataset.go | 34 ++++++++++++++++++++++++++++- decision.go | 10 ++++++--- rnn.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 98 insertions(+), 7 deletions(-) diff --git a/dataset.go b/dataset.go index 1c8a37c..1f74b69 100644 --- a/dataset.go +++ b/dataset.go @@ -6,6 +6,12 @@ import ( ) // ================== Dataset ================== +// +// - Seqs: finestre di log-return (dimensione lookback) in z-score. +// - Labels: log-return successivo (t+1) in z-score. +// - Mean/Std: media e dev std calcolate SOLO sul training (sulle feature X), +// riutilizzate per normalizzare sia X che Y. In inferenza denormalizziamo +// con predLR = predZ*Std + Mean (vedi decision.go). type Dataset struct { Seqs [][]float64 @@ -14,10 +20,17 @@ type Dataset struct { Std float64 } +// buildDataset costruisce (train, val) a partire dalle Kline: +// 1) prezzi di chiusura -> log(p) +// 2) differenze -> log-return (lr[t] = log(p[t]) - log(p[t-1])) +// 3) finestre X = lr[i : i+lookback], label Y = lr[i+lookback] +// 4) split train/val e z-score usando Mean/Std del SOLO training (sulle X) func buildDataset(kl []Kline, lookback int, valSplit float64) (Dataset, Dataset) { if len(kl) < lookback+2 { return Dataset{}, Dataset{} } + + // 1) Close -> log price prices := make([]float64, len(kl)) for i, k := range kl { prices[i] = k.Close @@ -26,10 +39,14 @@ func buildDataset(kl []Kline, lookback int, valSplit float64) (Dataset, Dataset) for i, p := range prices { logp[i] = math.Log(p) } + + // 2) log-return lr := make([]float64, len(logp)-1) for i := 1; i < len(logp); i++ { lr[i-1] = logp[i] - logp[i-1] } + + // 3) finestre X e label Y (prossimo step) N := len(lr) - lookback if N <= 0 { return Dataset{}, Dataset{} @@ -42,15 +59,26 @@ func buildDataset(kl []Kline, lookback int, valSplit float64) (Dataset, Dataset) X = append(X, win) Y = append(Y, lr[i+lookback]) } + + // 4) split train/val valN := int(float64(len(X)) * valSplit) if valN < 1 { valN = 1 } trainN := len(X) - valN + if trainN < 1 { + // fallback minimo: tutto train meno 1 per val + trainN = max(1, len(X)-1) + valN = len(X) - trainN + } + + // 5) mean/std SOLO sul training (sulle feature) mean, std := meanStd(flatten(X[:trainN])) if std == 0 { std = 1e-6 } + + // 6) z-score per X e Y con la stessa mean/std zX := make([][]float64, len(X)) for i := range X { zX[i] = make([]float64, lookback) @@ -62,6 +90,7 @@ func buildDataset(kl []Kline, lookback int, valSplit float64) (Dataset, Dataset) for i := range Y { zY[i] = (Y[i] - mean) / std } + train := Dataset{Seqs: zX[:trainN], Labels: zY[:trainN], Mean: mean, Std: std} val := Dataset{Seqs: zX[trainN:], Labels: zY[trainN:], Mean: mean, Std: std} return train, val @@ -109,5 +138,8 @@ func printManualInstruction(fromLabel, toLabel string, amountFrom, expectedBps, maxFeeBps = 0 } maxFeeAbs := amountFrom * (maxFeeBps / 1e4) - return fmt.Sprintf("ISTRUZIONI MANUALI: esegui swap %s->%s amount=%.8f; procedi solo se feeTotali<=%.3f bps (stima THOR=%.3f bps) e costo<=~%.8f %s", fromS, toS, amountFrom, maxFeeBps, feeBps, maxFeeAbs, fromS) + return fmt.Sprintf( + "ISTRUZIONI MANUALI: esegui swap %s->%s amount=%.8f; procedi solo se feeTotali<=%.3f bps (stima THOR=%.3f bps) e costo<=~%.8f %s", + fromS, toS, amountFrom, maxFeeBps, feeBps, maxFeeAbs, fromS, + ) } diff --git a/decision.go b/decision.go index 5cc53d7..17c0674 100644 --- a/decision.go +++ b/decision.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "log" "math" "net/http" "net/url" @@ -35,9 +36,12 @@ func decideTrade(cfg Config, dsTrain, dsVal Dataset, model *LSTM, lastSeq []floa // ---- inferenza movimento previsto ---- model.resetState() - predZ, _ := model.forward(lastSeq) - predLR := predZ*dsTrain.Std + dsTrain.Mean - moveBps := (math.Exp(predLR) - 1) * 1e4 + predZ, _ := model.forward(lastSeq) // pred in z-score + predLR := predZ*dsTrain.Std + dsTrain.Mean // ritorno log previsto (scala reale) + moveBps := (math.Exp(predLR) - 1) * 1e4 // bps sul sottostante coerente con ThorFrom->ThorTo + + log.Printf("inferenza: predZ=%.6f predLR=%.6f moveBps=%.3f → direzione=%s", + predZ, predLR, moveBps, map[bool]string{true: "AB (From→To)", false: "BA (To→From)"}[moveBps >= 0]) // ---- confidenza valida? ---- valMAE := 0.0 diff --git a/rnn.go b/rnn.go index 6e46eb2..a5e23f4 100644 --- a/rnn.go +++ b/rnn.go @@ -8,7 +8,12 @@ import ( "gonum.org/v1/gonum/mat" ) -/* Dataset + LSTM + helper come elencato */ +/* LSTM con: + - training invariato (Huber) + - calibrazione automatica del segno alla 1ª epoca (flip Why/by) se anti-segnale + - utilità numeriche locali, no dipendenze “sparse” +*/ + // ================== LSTM (gonum) ================== type LSTM struct { @@ -59,6 +64,8 @@ func newLSTM(inputSize, hiddenSize int, lr float64, seed int64) *LSTM { func (m *LSTM) resetState() { m.h.Zero(); m.c.Zero() } +// --- utilità numeriche locali --- + func denseOf(a mat.Matrix) *mat.Dense { r, c := a.Dims() out := mat.NewDense(r, c, nil) @@ -95,6 +102,34 @@ func scaleM(a mat.Matrix, s float64) *mat.Dense { return applyM(a, func(v float64) float64 { return v * s }) } func sigmoid(x float64) float64 { return 1.0 / (1.0 + math.Exp(-x)) } +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + +// --- flip output (calibrazione segno) --- + +func (m *LSTM) flipOutputSign() { + m.Why.Scale(-1, m.Why) + m.by.Scale(-1, m.by) +} + +// Somma dei prodotti pred*label in z-score (misura grezza della direzionalità) +func (m *LSTM) directionSum(ds Dataset) float64 { + if len(ds.Seqs) == 0 { + return 0 + } + sum := 0.0 + for i := range ds.Seqs { + m.resetState() + pZ, _ := m.forward(ds.Seqs[i]) + yZ := ds.Labels[i] + sum += pZ * yZ + } + return sum +} type lstmCache struct { xs, hs, cs []*mat.Dense @@ -316,6 +351,8 @@ func (m *LSTM) train(dsTrain, dsVal Dataset, batchSize int, maxEpochs int, early } bestValMAE = math.Inf(1) var baseLoss float64 + calibrated := false + for epoch := 1; epoch <= maxEpochs; epoch++ { idx := indices(len(dsTrain.Seqs)) totalLoss := 0.0 @@ -333,7 +370,7 @@ func (m *LSTM) train(dsTrain, dsVal Dataset, batchSize int, maxEpochs int, early n++ } } - avgLoss := totalLoss / float64(max(1, n)) + avgLoss := totalLoss / float64(maxInt(n, 1)) curVal := valMAE() if epoch == 1 { firstLoss = avgLoss @@ -344,8 +381,26 @@ func (m *LSTM) train(dsTrain, dsVal Dataset, batchSize int, maxEpochs int, early } epochsRun = epoch log.Printf("epoca=%d avgLoss=%.6f firstLoss=%.6f valMAE=%.6f", epoch, avgLoss, firstLoss, curVal) + + // Calibrazione: dopo la prima epoca, se anti-segnale, flippa output una volta + if epoch == 1 && !calibrated { + baseDS := dsVal + if len(baseDS.Seqs) == 0 { + baseDS = dsTrain + } + dir := m.directionSum(baseDS) + if dir < 0 { + log.Printf("calibrazione: segno inverso rilevato (directionSum=%.6f) → flip output", dir) + m.flipOutputSign() + dir2 := m.directionSum(baseDS) + log.Printf("dopo flip: directionSum=%.6f", dir2) + } + calibrated = true + } + if avgLoss <= baseLoss*earlyStopFrac { - log.Printf("early-stopping: avgLoss=%.6f soglia=%.6f (%.2f%% di firstLoss) epoca=%d", avgLoss, baseLoss*earlyStopFrac, earlyStopFrac*100, epoch) + log.Printf("early-stopping: avgLoss=%.6f soglia=%.6f (%.2f%% di firstLoss) epoca=%d", + avgLoss, baseLoss*earlyStopFrac, earlyStopFrac*100, epoch) break } }