money/main.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)
}
}