- Se avete bisogno di importare le quotazioni dei vostri titoli azionari o ETF in Google Sheets in modo che si aggiornino automaticamente, potete usare l’apps script qua sotto.
- Per farlo funzionare occorre semplicemente aprire un foglio di lavoro su https://docs.google.com/spreadsheets/ e andare su Estensioni e Apps Script.
- Cliccare sul + a fianco di File e selezionare Script.
- Dare un titolo a piacere al file (non implica nulla sul funzionamento).
- Elimina il contenuto già presente e incolla lo script che trovi qua di seguito.
- Salva cliccando sull’icona del floppydisk o premi ctrl+s.
- Ora torna nel tuo foglio di spreadsheets e utilizzando le varie funzioni di esempio, avrai il risultato voluto.
Ho aggiunto che
/*************************************************
* FUNZIONI FINANZA – VERSIONE CORRETTA - Cluade 02/04/2026
*
* Correzioni rispetto alla versione 13/03/2026:
* - VARGIORN: usa meta.chartPreviousClose (o previousClose) direttamente
* dai meta della chiamata live, evitando l'euristica sulle daily bars
* che causava il calcolo errato del prevClose.
* - _getPrevCloseFromDailyBars: riscritta per preferire chartPreviousClose
* dai meta invece della logica con tolleranza 0.3% (troppo stretta).
*
* =ULTIMO("SWDA.MI")
* =VARGIORN("SWDA.MI")
* =YTD("SWDA.MI")
* =VARPER("SWDA.MI"; 30)
* =MAXDATE("SWDA.MI"; "1y")
* =MAXVAL("SWDA.MI"; "5y")
* =MAXINFO("SWDA.MI"; "7y")
* =MINVAL("SWDA.MI"; "6mo")
* =MINDATE("SWDA.MI"; "1y")
* =MININFO("SWDA.MI"; "3y")
* =CAMBIO("EURUSD")
* =CAMBIO_PCT("EURUSD")
* =DEBUG_TICKER("SWDA.MI")
* =FORZA_REFRESH()
*************************************************/
function _normTicker(t) {
return String(t || "").trim();
}
function _roundByHint(x, hint) {
if (x == null || isNaN(x)) return null;
const d = (hint != null && !isNaN(hint)) ? Math.max(0, Math.min(8, Number(hint))) : 4;
return Number(Number(x).toFixed(d));
}
function _fetchChart(ticker, range, interval) {
const t = _normTicker(ticker);
const cb = Date.now();
const url =
"https://query1.finance.yahoo.com/v8/finance/chart/" +
encodeURIComponent(t) +
"?range=" + encodeURIComponent(range) +
"&interval=" + encodeURIComponent(interval) +
"&includePrePost=false" +
"&_cb=" + cb;
const res = UrlFetchApp.fetch(url, {
muteHttpExceptions: true,
headers: { "User-Agent": "Mozilla/5.0", "Accept": "application/json" }
});
let json;
try { json = JSON.parse(res.getContentText()); } catch (e) { return null; }
return json?.chart?.result?.[0] || null;
}
function _lastNonNull(arr) {
if (!arr || !arr.length) return null;
for (let i = arr.length - 1; i >= 0; i--) {
const v = arr[i];
if (v != null && !isNaN(v)) return v;
}
return null;
}
function _lastTwoNonNull(arr) {
if (!arr || !arr.length) return { last: null, prev: null };
let last = null, prev = null;
for (let i = arr.length - 1; i >= 0; i--) {
const v = arr[i];
if (v == null || isNaN(v)) continue;
if (last === null) last = v;
else { prev = v; break; }
}
return { last, prev };
}
/***********************
* LIVE LAST: usa meta.regularMarketPrice da 1d/5m (fallback 1d/1m, poi 5d/1d)
***********************/
function _getLiveLast(ticker) {
const t = _normTicker(ticker);
const tries = [
{ range: "1d", interval: "5m", label: "1d/5m" },
{ range: "1d", interval: "1m", label: "1d/1m" },
{ range: "5d", interval: "1d", label: "5d/1d" }
];
for (const tr of tries) {
const r = _fetchChart(t, tr.range, tr.interval);
if (!r) continue;
const m = r.meta || {};
const hint = m.priceHint;
// Last preferito: meta.regularMarketPrice
let last = (m.regularMarketPrice != null && !isNaN(m.regularMarketPrice))
? Number(m.regularMarketPrice)
: null;
// Fallback: ultimo close della serie
if (last == null) {
const lastClose = _lastNonNull(r?.indicators?.quote?.[0]?.close);
if (lastClose != null) last = Number(lastClose);
}
if (last != null) {
return {
last,
priceHint: hint,
intervalUsed: tr.label,
meta: m
};
}
}
return { last: null, priceHint: null, intervalUsed: null, meta: null };
}
/***********************
* PREV CLOSE ROBUSTO
*
* CORREZIONE: la vecchia logica usava una tolleranza dello 0,3% per capire
* se dailyLast fosse il close di oggi o di ieri — troppo stretta in mercati
* mossi, causando il pick del ramo sbagliato e quindi una variazione errata.
*
* Nuova logica:
* 1. Usa meta.chartPreviousClose (o meta.previousClose) dalla chiamata 5d/1d:
* Yahoo lo popola con il close ufficiale di ieri, senza ambiguità.
* 2. Fallback: penultimo close dalle barre giornaliere.
***********************/
function _getPrevCloseFromDailyBars(ticker, liveLast) {
const t = _normTicker(ticker);
const r = _fetchChart(t, "5d", "1d");
if (!r) return { prevClose: null, dailyLast: null, dailyPrev: null };
const m = r.meta || {};
// --- PRIMA SCELTA: chartPreviousClose / previousClose dai meta ---
// Questi campi contengono direttamente il close ufficiale del giorno precedente.
const metaPrev =
(m.chartPreviousClose != null && !isNaN(m.chartPreviousClose)) ? Number(m.chartPreviousClose) :
(m.previousClose != null && !isNaN(m.previousClose)) ? Number(m.previousClose) :
null;
if (metaPrev != null) {
const closes = r?.indicators?.quote?.[0]?.close;
const two = _lastTwoNonNull(closes);
return {
prevClose: metaPrev,
dailyLast: two.last != null ? Number(two.last) : null,
dailyPrev: two.prev != null ? Number(two.prev) : null
};
}
// --- FALLBACK: penultimo close dalle barre giornaliere ---
const closes = r?.indicators?.quote?.[0]?.close;
const two = _lastTwoNonNull(closes);
return {
prevClose: two.prev != null ? Number(two.prev) : null,
dailyLast: two.last != null ? Number(two.last) : null,
dailyPrev: two.prev != null ? Number(two.prev) : null
};
}
/***********************
* ULTIMO PREZZO
***********************/
function ULTIMO(ticker) {
const t = _normTicker(ticker);
if (!t) return "Ticker?";
const live = _getLiveLast(t);
if (live.last == null) return "Dati non disponibili";
return _roundByHint(live.last, live.priceHint);
}
/***********************
* VARIAZIONE GIORNALIERA %
*
* CORREZIONE: usa direttamente meta.chartPreviousClose (o previousClose)
* già presente nella risposta di _getLiveLast, senza passare per
* _getPrevCloseFromDailyBars e la sua logica di tolleranza difettosa.
* _getPrevCloseFromDailyBars viene usata solo come fallback se i meta
* non contengono il prevClose.
***********************/
function VARGIORN(ticker) {
const t = _normTicker(ticker);
if (!t) return "Ticker?";
const live = _getLiveLast(t);
if (live.last == null) return "Dati incompleti";
const m = live.meta || {};
// Prova prima a leggere il prevClose direttamente dai meta della chiamata live
let prev =
(m.chartPreviousClose != null && !isNaN(m.chartPreviousClose)) ? Number(m.chartPreviousClose) :
(m.previousClose != null && !isNaN(m.previousClose)) ? Number(m.previousClose) :
null;
// Se i meta non lo hanno (raro), fallback a daily bars
if (prev == null) {
const prevObj = _getPrevCloseFromDailyBars(t, live.last);
prev = prevObj.prevClose;
}
if (prev == null || isNaN(prev) || Number(prev) === 0) return "Dati incompleti";
return Number((((Number(live.last) - prev) / prev) * 100).toFixed(2));
}
/***********************
* YTD % (storico close giornaliero)
***********************/
function YTD(ticker) {
const t = _normTicker(ticker);
if (!t) return "Ticker?";
const r = _fetchChart(t, "ytd", "1d");
if (!r) return "Errore Yahoo";
const p = r?.indicators?.quote?.[0]?.close;
if (!p) return "Dati incompleti";
let first = null, last = null;
for (let v of p) {
if (v != null && !isNaN(v)) {
if (first === null) first = v;
last = v;
}
}
if (first === null || last === null || Number(first) === 0) return "Dati incompleti";
return Number((((Number(last) - Number(first)) / Number(first)) * 100).toFixed(2));
}
/***********************
* VARPER % su N giorni (storico close)
***********************/
function VARPER(ticker, giorni) {
const t = _normTicker(ticker);
if (!t) return "Ticker?";
if (!giorni || giorni < 1) return "Periodo?";
const range = Math.max(giorni + 5, 30) + "d";
const r = _fetchChart(t, range, "1d");
if (!r) return "Errore Yahoo";
const p = r?.indicators?.quote?.[0]?.close;
if (!p) return "Dati incompleti";
const clean = p.filter(v => v != null && !isNaN(v));
if (clean.length <= giorni) return "Dati incompleti";
const last = clean[clean.length - 1];
const past = clean[clean.length - 1 - giorni];
if (Number(past) === 0) return "Dati incompleti";
return Number((((Number(last) - Number(past)) / Number(past)) * 100).toFixed(2));
}
/***********************
* MAX helpers (storico)
***********************/
function MAXVAL(ticker, range, dataInizio) { return _maxCore(ticker, dataInizio, "value", range); }
function MAXDATE(ticker, range, dataInizio) { return _maxCore(ticker, dataInizio, "date", range); }
function MAXINFO(ticker, range, dataInizio) { return _maxCore(ticker, dataInizio, "full", range); }
function _maxCore(ticker, dataInizio, mode, range = "1y") {
const t = _normTicker(ticker);
if (!t || !range) return "Ticker o range mancante";
const r = _fetchChart(t, range, "1d");
if (!r) return "Errore Yahoo";
const prices = r?.indicators?.quote?.[0]?.close;
const ts = r?.timestamp;
if (!prices || !ts) return "Dati incompleti";
let startTs = 0;
if (dataInizio) {
const d = new Date(dataInizio);
if (!isNaN(d.getTime())) startTs = Math.floor(d.getTime() / 1000);
}
let maxP = -Infinity, maxT = null;
for (let i = 0; i < prices.length; i++) {
const v = prices[i];
if (v == null || isNaN(v)) continue;
if (ts[i] < startTs) continue;
if (v > maxP) { maxP = v; maxT = ts[i]; }
}
if (maxT === null || maxP === -Infinity) return "No dati";
const ultimo = ULTIMO(t);
const drawdown =
(typeof ultimo === "number" && Number(maxP) !== 0)
? Number((((Number(ultimo) - Number(maxP)) / Number(maxP)) * 100).toFixed(2))
: "—";
const dataMax = Utilities.formatDate(new Date(maxT * 1000), Session.getScriptTimeZone(), "dd/MM/yyyy");
if (mode === "value") return _roundByHint(maxP, 4);
if (mode === "date") return dataMax;
return [
["Ticker", t],
["Prezzo attuale", ultimo],
["Var. oggi %", VARGIORN(t)],
["Massimo", _roundByHint(maxP, 4)],
["Data massimo", dataMax],
["Drawdown %", drawdown],
["YTD %", YTD(t)]
];
}
/***********************
* MIN helpers (storico)
***********************/
function MINVAL(ticker, range) { return _minCore(ticker, range, "value"); }
function MINDATE(ticker, range) { return _minCore(ticker, range, "date"); }
function MININFO(ticker, range) { return _minCore(ticker, range, "full"); }
function _minCore(ticker, range, mode) {
if (!ticker) return "Ticker?";
if (!range) range = "1y";
const r = _fetchChart(ticker, range, "1d");
if (!r) return "Errore Yahoo";
const prices = r?.indicators?.quote?.[0]?.close;
const ts = r?.timestamp;
if (!prices || !ts) return "Dati incompleti";
let minP = Infinity, minT = null;
for (let i = 0; i < prices.length; i++) {
const px = prices[i];
if (px == null || isNaN(px)) continue;
if (px < minP) { minP = px; minT = ts[i]; }
}
if (minT === null) return "No dati";
const ultimo = ULTIMO(ticker);
const rebound = (typeof ultimo === "number")
? Number((((ultimo - minP) / minP) * 100).toFixed(2))
: "—";
const dataMin = Utilities.formatDate(
new Date(minT * 1000),
Session.getScriptTimeZone(),
"dd/MM/yyyy"
);
if (mode === "value") return Number(minP.toFixed(4));
if (mode === "date") return dataMin;
return [
["Ticker", ticker],
["Prezzo attuale", ultimo],
["Var. oggi %", VARGIORN(ticker)],
["Minimo (" + range + ")", Number(minP.toFixed(4))],
["Data minimo", dataMin],
["Rimbalzo %", rebound],
["YTD %", YTD(ticker)]
];
}
/***********************
* CAMBIO VALUTA (via CHART - stabile)
*
* =CAMBIO("EURUSD")
* =CAMBIO("USDEUR")
* =CAMBIO("EURUSD"; 6)
***********************/
function CAMBIO(pair, decimals) {
if (!pair) return "Pair?";
const ticker = pair.toUpperCase() + "=X";
const r = _fetchChart(ticker, "1d", "5m");
if (!r) return "Errore Yahoo";
const price = r?.meta?.regularMarketPrice;
if (price == null) return "No dati";
const dec = (decimals != null && !isNaN(decimals)) ? Number(decimals) : 4;
return Number(Number(price).toFixed(dec));
}
/***********************
* VARIAZIONE % CAMBIO
*
* =CAMBIO_PCT("EURUSD")
***********************/
function CAMBIO_PCT(pair, decimals) {
if (!pair) return "Pair?";
const ticker = pair.toUpperCase() + "=X";
const r = _fetchChart(ticker, "1d", "5m");
if (!r) return "Errore Yahoo";
const last = r?.meta?.regularMarketPrice;
const prev = r?.meta?.previousClose ?? r?.meta?.chartPreviousClose ?? null;
if (last == null || prev == null || prev === 0) return "Dati incompleti";
const pct = ((last - prev) / prev) * 100;
const dec = (decimals != null && !isNaN(decimals)) ? Number(decimals) : 2;
return Number(pct.toFixed(dec));
}
/***********************
* DEBUG_TICKER
* Mostra tutti i valori chiave per diagnosticare discrepanze
***********************/
function DEBUG_TICKER(ticker) {
const t = _normTicker(ticker);
if (!t) return "Ticker?";
const live = _getLiveLast(t);
const prevObj = _getPrevCloseFromDailyBars(t, live.last);
const m = live.meta || {};
const metaPrev =
(m.chartPreviousClose != null && !isNaN(m.chartPreviousClose)) ? Number(m.chartPreviousClose) :
(m.previousClose != null && !isNaN(m.previousClose)) ? Number(m.previousClose) :
null;
// Il prevClose effettivamente usato da VARGIORN (meta > daily fallback)
const prevUsedByVargiorn = metaPrev ?? prevObj.prevClose;
return [
["Ticker", t],
["LIVE intervalUsed", live.intervalUsed],
["LIVE regularMarketPrice", m.regularMarketPrice],
["META chartPreviousClose", m.chartPreviousClose],
["META previousClose", m.previousClose],
["META prev usato (VARGIORN)", prevUsedByVargiorn],
["DAILY lastClose", prevObj.dailyLast],
["DAILY prevClose (fallback)", prevObj.dailyPrev],
["VARGIORN calcolato", VARGIORN(t)],
["PRICEHINT", live.priceHint]
];
}
/***********************
* TRIGGER CONTROLLATO
* (Ogni ora precisa, 09:00 - 18:00, LUN-VEN)
***********************/
function TRIGGER_PROGRAMMATO() {
const dataAttuale = new Date();
const oraAttuale = dataAttuale.getHours();
const giornoSettimana = dataAttuale.getDay(); // 0=Dom, 1=Lun, ..., 6=Sab
if (giornoSettimana >= 1 && giornoSettimana <= 5 && oraAttuale >= 9 && oraAttuale <= 18) {
FORZA_REFRESH();
console.log("Refresh eseguito alle ore: " + oraAttuale + ":00");
} else {
console.log("Fascia oraria o giorno non attivi. Ora attuale: " + oraAttuale);
}
}
function FORZA_REFRESH() {
const ss = SpreadsheetApp.getActive();
let sh = ss.getSheetByName("Refresh");
if (!sh) {
sh = ss.insertSheet("Refresh");
sh.hideSheet();
}
sh.getRange("A1").setValue(new Date());
}
Ora, sempre da Apps Script andate sull’icona ad orologio Attivatori.
- Scegliere la funzione da eseguire, selezionando: TRIGGER_PROGRAMMATO
- Selezionare l’origine dell’evento, selezionando: Evento vincolato a specifiche temporali
- Selezionare il tipo di attivatore basato sull’orario, selezionando: Timer in minuti
- Selezionare intervallo in minuti, selezionando: Ogni 30 minuti
Nel foglio, verrà creato in automatico un nuovo sheet che si chiamerà Refresh e nella cella A1 comparirà la data attuale che verrà refreshata ogni 30 minuti dalle ore 09:00 alle ore 18:00 dal lunedì al venerdì.