114 lines
4.4 KiB
C#
114 lines
4.4 KiB
C#
using System;
|
||
|
||
namespace DesktopBot.Services
|
||
{
|
||
/// <summary>
|
||
/// Helper per determinare se un mercato è aperto in base all'asset class e all'orario locale.
|
||
///
|
||
/// Regole approssimative (senza chiamata API aggiuntiva):
|
||
/// - US Equity : Lun–Ven 09:30–16:00 ET (UTC-5 standard / UTC-4 daylight)
|
||
/// - Crypto : sempre aperto (24/7)
|
||
/// - Altro : assume sempre aperto
|
||
///
|
||
/// Per una verifica precisa (pre-market, post-market, festivi) usare
|
||
/// IAlpacaTradingClient.GetClockAsync() — disponibile ma costa una chiamata API.
|
||
/// </summary>
|
||
public static class MarketHoursService
|
||
{
|
||
// ── Orari NYSE/NASDAQ in Eastern Time ────────────────────────────────
|
||
private static readonly TimeSpan MarketOpen = new TimeSpan(9, 30, 0);
|
||
private static readonly TimeSpan MarketClose = new TimeSpan(16, 0, 0);
|
||
|
||
// Fuso Eastern (tiene conto automaticamente del DST di .NET)
|
||
private static readonly TimeZoneInfo EasternTz =
|
||
TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
|
||
|
||
/// <summary>
|
||
/// Ritorna true se il mercato relativo all'asset class indicata è
|
||
/// presumibilmente aperto adesso.
|
||
/// </summary>
|
||
public static bool IsMarketOpen(string assetClass)
|
||
{
|
||
if (string.IsNullOrEmpty(assetClass))
|
||
return true;
|
||
|
||
var cls = assetClass.ToLowerInvariant();
|
||
|
||
// Crypto: 24/7
|
||
if (cls.Contains("crypto"))
|
||
return true;
|
||
|
||
// US Equity: Lun–Ven, orario NYSE
|
||
var nowEt = TimeZoneInfo.ConvertTime(DateTime.UtcNow, EasternTz);
|
||
if (nowEt.DayOfWeek == DayOfWeek.Saturday || nowEt.DayOfWeek == DayOfWeek.Sunday)
|
||
return false;
|
||
|
||
var tod = nowEt.TimeOfDay;
|
||
return tod >= MarketOpen && tod < MarketClose;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Testo descrittivo dello stato mercato per la UI.
|
||
/// </summary>
|
||
public static string GetMarketStatusLabel(string assetClass)
|
||
{
|
||
if (string.IsNullOrEmpty(assetClass))
|
||
return "";
|
||
|
||
var cls = assetClass.ToLowerInvariant();
|
||
if (cls.Contains("crypto"))
|
||
return "Mercato 24/7 — sempre aperto";
|
||
|
||
var nowEt = TimeZoneInfo.ConvertTime(DateTime.UtcNow, EasternTz);
|
||
if (nowEt.DayOfWeek == DayOfWeek.Saturday || nowEt.DayOfWeek == DayOfWeek.Sunday)
|
||
return $"Mercato chiuso (weekend) — riapertura lunedì {MarketOpen:hh\\:mm} ET";
|
||
|
||
var tod = nowEt.TimeOfDay;
|
||
if (tod < MarketOpen)
|
||
return $"Mercato chiuso — apertura alle {MarketOpen:hh\\:mm} ET";
|
||
if (tod >= MarketClose)
|
||
return $"Mercato chiuso — riapertura domani {MarketOpen:hh\\:mm} ET";
|
||
|
||
var remaining = MarketClose - tod;
|
||
return $"Mercato aperto — chiude tra {(int)remaining.TotalHours}h {remaining.Minutes}m ET";
|
||
}
|
||
|
||
/// <summary>
|
||
/// Colore suggerito per il badge stato mercato.
|
||
/// </summary>
|
||
public static string GetMarketStatusColor(string assetClass)
|
||
=> IsMarketOpen(assetClass) ? "#00E676" : "#FFC107";
|
||
|
||
/// <summary>
|
||
/// Secondi da attendere prima della prossima apertura (utile per sleep nel loop).
|
||
/// Ritorna 0 se il mercato è già aperto.
|
||
/// </summary>
|
||
public static double SecondsUntilOpen(string assetClass)
|
||
{
|
||
if (IsMarketOpen(assetClass)) return 0;
|
||
|
||
var cls = (assetClass ?? "").ToLowerInvariant();
|
||
if (cls.Contains("crypto")) return 0;
|
||
|
||
var nowEt = TimeZoneInfo.ConvertTime(DateTime.UtcNow, EasternTz);
|
||
|
||
DateTime nextOpen;
|
||
if (nowEt.TimeOfDay < MarketOpen)
|
||
{
|
||
nextOpen = nowEt.Date + MarketOpen;
|
||
}
|
||
else
|
||
{
|
||
// Dopo la chiusura → prossimo giorno lavorativo
|
||
var candidate = nowEt.Date.AddDays(1) + MarketOpen;
|
||
while (candidate.DayOfWeek == DayOfWeek.Saturday || candidate.DayOfWeek == DayOfWeek.Sunday)
|
||
candidate = candidate.AddDays(1);
|
||
nextOpen = candidate;
|
||
}
|
||
|
||
var diff = nextOpen - nowEt;
|
||
return diff.TotalSeconds > 0 ? diff.TotalSeconds : 0;
|
||
}
|
||
}
|
||
}
|