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) } }