171 lines
6.1 KiB
Go
171 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// helper per inviare testo semplice come bundle (es. errori/avvisi)
|
|
func sendText(cfg Config, text string) {
|
|
mb := MessageBundle{
|
|
DecisionLine: strings.TrimSpace(text),
|
|
Instructions: "",
|
|
QuoteValidUntil: time.Now().UTC().Add(24 * time.Hour), // fallback sensato per messaggi non legati a quote
|
|
}
|
|
notifyMatrix(cfg, mb)
|
|
}
|
|
|
|
// ================== run (una esecuzione) ==================
|
|
|
|
func runOnce(cfg Config) {
|
|
log.Printf("avvio: symbol=%s from=%s to=%s lookback=%d lr=%.5f batch=%d valSplit=%.2f maxEpochs=%d earlyStopPct=%.2f safetyBps=%.2f minMoveBps=%.2f refFrom=%.3f refTo=%.3f valConfZ=%.2f",
|
|
cfg.BinanceSymbol, cfg.ThorFrom, cfg.ThorTo, cfg.Lookback, cfg.LearningRate, cfg.BatchSize, cfg.ValSplit, cfg.MaxEpochs, cfg.EarlyStopFrac, cfg.SafetyBps, cfg.MinMoveBps, cfg.RefFrom, cfg.RefTo, cfg.ValConfZ)
|
|
|
|
_ = mustMkdirAll(cfg.DataDir)
|
|
_ = mustMkdirAll(cfg.StateDir)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Binance
|
|
fresh, err := fetchBinanceKlines(ctx, cfg.BinanceSymbol, 300)
|
|
if err != nil {
|
|
log.Printf("errore download da Binance (%s): %v", cfg.BinanceSymbol, err)
|
|
sendText(cfg, fmt.Sprintf("ERRORE: download Binance fallito symbol=%s err=%v", cfg.BinanceSymbol, err))
|
|
return
|
|
}
|
|
log.Printf("binance: scaricati %d giorni per %s", len(fresh), cfg.BinanceSymbol)
|
|
|
|
dataPath := filepath.Join(cfg.DataDir, strings.ToLower(cfg.BinanceSymbol)+".csv")
|
|
existing, err := loadLocalCSV(dataPath)
|
|
if err != nil {
|
|
log.Printf("errore lettura CSV locale: %v", err)
|
|
sendText(cfg, fmt.Sprintf("ERRORE: lettura CSV fallita file=%s err=%v", dataPath, err))
|
|
return
|
|
}
|
|
merged := mergeKlines(existing, fresh)
|
|
if err := saveLocalCSV(dataPath, merged); err != nil {
|
|
log.Printf("errore salvataggio CSV: %v", err)
|
|
sendText(cfg, fmt.Sprintf("ERRORE: salvataggio CSV fallito file=%s err=%v", dataPath, err))
|
|
return
|
|
}
|
|
log.Printf("dataset locale aggiornato: righe=%d file=%s", len(merged), dataPath)
|
|
|
|
// Dataset + training
|
|
train, val := buildDataset(merged, cfg.Lookback, cfg.ValSplit)
|
|
if len(train.Seqs) == 0 || len(val.Seqs) == 0 {
|
|
log.Printf("dataset insufficiente per training: train=%d val=%d", len(train.Seqs), len(val.Seqs))
|
|
sendText(cfg, fmt.Sprintf("ERRORE: dataset insufficiente train=%d val=%d", len(train.Seqs), len(val.Seqs)))
|
|
return
|
|
}
|
|
model := newLSTM(1, 64, cfg.LearningRate, time.Now().UnixNano())
|
|
firstLoss, bestValMAE, epochs := model.train(train, val, cfg.BatchSize, cfg.MaxEpochs, cfg.EarlyStopFrac, time.Now().UnixNano())
|
|
log.Printf("training completato: epoche=%d firstLoss=%.6f bestValMAE=%.6f", epochs, firstLoss, bestValMAE)
|
|
|
|
lastSeq := train.Seqs[len(train.Seqs)-1]
|
|
|
|
// ===== Consiglio (Decision + Bundle) =====
|
|
dec, err := decideTrade(cfg, train, val, model, lastSeq)
|
|
if err != nil {
|
|
log.Printf("nessun consiglio a causa di un errore esterno (THORNode/Binance): %v", err)
|
|
sendText(cfg, fmt.Sprintf("ERRORE: consiglio non preso per errore esterno: %v", err))
|
|
return
|
|
}
|
|
|
|
// Log sintetico leggibile (usa firma esistente: humanAction(dec, cfg))
|
|
actStr := humanAction(dec, cfg)
|
|
log.Printf("consiglio: azione=%s expectedBps=%.3f feeBps=%.3f netBps=%.3f confOK=%t motivo=%s",
|
|
actStr, dec.ExpectedBps, dec.FeeBps, dec.NetBps, dec.ConfidenceOK, sanitize(dec.Reason))
|
|
|
|
// Crea il bundle (con expiry reale del quote) e invia (anche HOLD)
|
|
mb := BuildMessageBundle(dec, cfg)
|
|
notifyMatrix(cfg, mb)
|
|
|
|
// ===== Stato & Istruzioni aggiuntive (solo se non HOLD) =====
|
|
statePath := filepath.Join(cfg.StateDir, "balance.json")
|
|
st, _ := loadState(statePath)
|
|
fromKey := assetShort(cfg.ThorFrom)
|
|
toKey := assetShort(cfg.ThorTo)
|
|
|
|
switch dec.Action {
|
|
case "SWAP_AB":
|
|
// Istruzioni operative aggiuntive
|
|
opText := printManualInstruction(cfg.ThorFrom, cfg.ThorTo, cfg.RefFrom, dec.ExpectedBps, dec.FeeBps, cfg.SafetyBps)
|
|
log.Printf("%s | motivo=%s", opText, sanitize(dec.Reason))
|
|
opBundle := MessageBundle{
|
|
DecisionLine: "", // già inviato il riepilogo sopra
|
|
Instructions: fmt.Sprintf("%s | motivo=%s", opText, sanitize(dec.Reason)),
|
|
QuoteValidUntil: dec.QuoteValidUntil,
|
|
}
|
|
notifyMatrix(cfg, opBundle)
|
|
|
|
if manualModeEnabled() {
|
|
return
|
|
}
|
|
want := cfg.RefFrom
|
|
avail := st.Balances[fromKey]
|
|
if avail < want {
|
|
msg := fmt.Sprintf("SALDO INSUFFICIENTE su %s: disp=%.8f richiesto=%.8f, nessuna istruzione emessa", fromKey, avail, want)
|
|
log.Printf("%s", msg)
|
|
sendText(cfg, msg)
|
|
return
|
|
}
|
|
st.Balances[fromKey] = avail - want
|
|
st.Locks = append(st.Locks, Lock{TS: time.Now().UTC(), From: fromKey, To: toKey, AmountFrom: want, Status: "pending"})
|
|
_ = saveState(statePath, st)
|
|
log.Printf("prenotazione: asset=%s amount=%.8f nuovoSaldo=%.8f stato=pending", fromKey, want, st.Balances[fromKey])
|
|
|
|
case "SWAP_BA":
|
|
opText := printManualInstruction(cfg.ThorTo, cfg.ThorFrom, cfg.RefTo, math.Abs(dec.ExpectedBps), dec.FeeBps, cfg.SafetyBps)
|
|
log.Printf("%s | motivo=%s", opText, sanitize(dec.Reason))
|
|
opBundle := MessageBundle{
|
|
DecisionLine: "",
|
|
Instructions: fmt.Sprintf("%s | motivo=%s", opText, sanitize(dec.Reason)),
|
|
QuoteValidUntil: dec.QuoteValidUntil,
|
|
}
|
|
notifyMatrix(cfg, opBundle)
|
|
|
|
if manualModeEnabled() {
|
|
return
|
|
}
|
|
fromInv := toKey // vendi l'asset "to"
|
|
toInv := fromKey
|
|
want := cfg.RefTo
|
|
avail := st.Balances[fromInv]
|
|
if avail < want {
|
|
msg := fmt.Sprintf("SALDO INSUFFICIENTE su %s: disp=%.8f richiesto=%.8f, nessuna istruzione emessa", fromInv, avail, want)
|
|
log.Printf("%s", msg)
|
|
sendText(cfg, msg)
|
|
return
|
|
}
|
|
st.Balances[fromInv] = avail - want
|
|
st.Locks = append(st.Locks, Lock{TS: time.Now().UTC(), From: fromInv, To: toInv, AmountFrom: want, Status: "pending"})
|
|
_ = saveState(statePath, st)
|
|
log.Printf("prenotazione: asset=%s amount=%.8f nuovoSaldo=%.8f stato=pending", fromInv, want, st.Balances[fromInv])
|
|
|
|
default:
|
|
log.Printf("nessuna azione consigliata (HOLD)")
|
|
}
|
|
}
|
|
|
|
// ================== main (daemon: subito + ogni 24 ore) ==================
|
|
|
|
func main() {
|
|
log.SetFlags(log.Ldate | log.Lmicroseconds)
|
|
cfg := loadConfig()
|
|
|
|
// esegui subito
|
|
runOnce(cfg)
|
|
|
|
// poi ogni 24 ore
|
|
for {
|
|
time.Sleep(24 * time.Hour)
|
|
runOnce(cfg)
|
|
}
|
|
}
|