Files
Stonks/areweopen.html
2026-05-26 10:34:38 +02:00

544 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Börsen Öffnungszeiten · Live Status</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: linear-gradient(145deg, #f0f4fa 0%, #e6edf4 100%);
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
padding: 2rem 1rem;
color: #1a2c3e;
}
.container {
max-width: 1300px;
margin: 0 auto;
}
/* header */
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 1.9rem;
font-weight: 600;
background: linear-gradient(135deg, #1e3c4c, #2b5c74);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.3px;
}
.header .sub {
color: #2c5a74;
margin-top: 0.5rem;
font-weight: 500;
background: rgba(255,255,240,0.6);
display: inline-block;
padding: 0.2rem 1rem;
border-radius: 40px;
backdrop-filter: blur(2px);
}
.live-badge {
display: inline-flex;
align-items: center;
gap: 8px;
background: #1e2f3c;
color: white;
padding: 0.2rem 1rem;
border-radius: 60px;
font-size: 0.8rem;
margin-top: 0.8rem;
}
.pulse {
width: 10px;
height: 10px;
background-color: #2ef07c;
border-radius: 50%;
box-shadow: 0 0 5px #2ef07c;
animation: pulse-green 1.2s infinite;
}
@keyframes pulse-green {
0% { opacity: 0.4; transform: scale(0.8);}
100% { opacity: 1; transform: scale(1.2);}
}
/* cards grid */
.exchange-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.6rem;
margin-bottom: 2.5rem;
}
.card {
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(0px);
border-radius: 2rem;
box-shadow: 0 12px 28px rgba(0, 10, 20, 0.12), 0 2px 4px rgba(0,0,0,0.02);
padding: 1.3rem 1.5rem 1.8rem;
transition: transform 0.2s ease, box-shadow 0.2s;
border: 1px solid rgba(255,255,245,0.7);
}
.card:hover {
transform: translateY(-3px);
box-shadow: 0 22px 32px -12px rgba(0, 20, 40, 0.2);
}
.exchange-name {
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
border-bottom: 2px solid #dce5ef;
padding-bottom: 0.65rem;
margin-bottom: 1.2rem;
}
.exchange-name h2 {
font-size: 1.55rem;
font-weight: 600;
letter-spacing: -0.2px;
}
.status {
font-weight: 700;
font-size: 0.85rem;
padding: 0.25rem 0.75rem;
border-radius: 50px;
background: #eef2f7;
}
.status.open {
background: #c8f0e1;
color: #006b4c;
box-shadow: inset 0 0 0 1px #9be0c8;
}
.status.closed {
background: #ffe1de;
color: #b13e3e;
box-shadow: inset 0 0 0 1px #fbc5be;
}
.info-row {
display: flex;
justify-content: space-between;
margin: 0.8rem 0;
font-size: 0.95rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.label {
font-weight: 600;
color: #2c5a6e;
}
.value {
font-family: 'JetBrains Mono', monospace;
font-weight: 500;
background: #f3f7fb;
padding: 0.2rem 0.6rem;
border-radius: 20px;
}
.next-event {
margin-top: 1rem;
background: #eef3fc;
border-radius: 1.2rem;
padding: 0.7rem 1rem;
font-size: 0.85rem;
border-left: 4px solid #2c8eb3;
}
.footnote {
text-align: center;
margin-top: 2rem;
font-size: 0.75rem;
color: #4a627a;
background: #f8fafd;
max-width: 600px;
margin-left: auto;
margin-right: auto;
padding: 0.8rem;
border-radius: 2rem;
}
.refresh-note {
font-size: 0.7rem;
text-align: right;
margin-top: 0.2rem;
opacity: 0.7;
}
button {
background: #dce5f0;
border: none;
padding: 6px 14px;
border-radius: 30px;
font-weight: 500;
cursor: pointer;
margin-top: 1rem;
font-size: 0.75rem;
transition: 0.1s;
}
button:active {
background: #c0cee0;
}
@media (max-width: 680px) {
body { padding: 1rem; }
.exchange-name h2 { font-size: 1.3rem; }
.info-row { flex-direction: column; gap: 0.3rem; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 MarktStatus · Börsenöffnungszeiten</h1>
<div class="sub">Handelszeiten für Deutschland + USA (Echtzeitanzeige)</div>
<div class="live-badge">
<span class="pulse"></span>
<span>Live basierend auf deiner Systemzeit</span>
</div>
</div>
<div class="exchange-grid" id="exchangeGrid">
<!-- dynamic cards injected via JS -->
</div>
<div class="footnote">
⏰ Alle Zeiten basieren auf <strong>deiner lokalen Zeitzone</strong> (Browser/System).<br>
✅ Tradegate, Xetra, Frankfurt: MontagFreitag. US-Börsen (NYSE, NASDAQ): reguläre Session.<br>
️ Wochenend-Handel ist extrem selten (nur spezielle OTC). Anzeige zeigt "GESCHLOSSEN" an Samstagen/Sonntagen.
</div>
<div class="refresh-note">
<button id="refreshBtn">🔄 Status aktualisieren</button> — Seite zeigt live Öffnungsstatus
</div>
</div>
<script>
(function() {
// ----- definierte Börsen + Zeitzonen ----------
// Alle Zeiten werden in der LOKALEN Zeitzone des Users berechnet
// Für jeden Eintrag: openingHourLocal, closingHourLocal, sowie evtl. separate "early close" ist hier Standard.
// Achtung: US Börsen in Eastern Time (ET) -> müssen in lokale Zeit konvertiert werden.
// Ansatz: Wir definieren die Regeln in ET (für USA) und in CET/CEST für DE (Tradegate/Xetra/Frankfurt)
// Da der Browser die Sommerzeit korrekt behandelt, vergleichen wir mit aktuellen UTC-Local-Stunden.
// Hilfsfunktionen -------------------------------------------------
function getCurrentLocalDateTime() {
return new Date(); // lokale Browserzeit
}
// extrahiere Wochentag (0=Sonntag ... 6=Samstag) Achtung: getDay() So=0
function isWeekend(date) {
const day = date.getDay();
return day === 0 || day === 6; // Samstag oder Sonntag
}
// ---------- DEUTSCHE Börsen (Handel: Mo-Fr, KEIN Samstag/Sonntag)
// Typische Zeiten: Xetra 9:00 - 17:30, Tradegate 8:00 - 22:00, Frankfurt (Börse) 8:00 - 20:00
// Wir nutzen realistische Zeiten basierend auf lokaler Zeit (DE Börsen haben lokale Zeit = CET/CEST)
function isGermanExchangeOpen(exchangeConfig, now) {
if (isWeekend(now)) return false; // Sa/So grundsätzlich geschlossen (ausser evtl. Spezial, ignorieren wir)
const hours = now.getHours();
const minutes = now.getMinutes();
const currentMinutes = hours * 60 + minutes;
const openMinutes = exchangeConfig.openHour * 60 + (exchangeConfig.openMinute || 0);
const closeMinutes = exchangeConfig.closeHour * 60 + (exchangeConfig.closeMinute || 0);
return (currentMinutes >= openMinutes && currentMinutes < closeMinutes);
}
// ---------- US Börsen (NYSE, NASDAQ) : reguläre Handelszeiten 9:30 AM - 4:00 PM Eastern Time (ET)
// Wir müssen die aktuelle lokale Zeit in Eastern Time umrechnen, um den Status korrekt zu bestimmen
// Dazu verwenden wir toLocaleString mit 'America/New_York' timeZone
function isUSEasternExchangeOpen(now) {
if (isWeekend(now)) return false; // Wochenende -> geschlossen
// Eastern Time aktuell (EDT oder EST) ermitteln
const easternTimeStr = now.toLocaleString("en-US", { timeZone: "America/New_York", hour12: false });
const easternDate = new Date(easternTimeStr);
const hourET = easternDate.getHours();
const minuteET = easternDate.getMinutes();
const currentETMinutes = hourET * 60 + minuteET;
const openET = 9 * 60 + 30; // 9:30 AM
const closeET = 16 * 60 + 0; // 4:00 PM
return (currentETMinutes >= openET && currentETMinutes < closeET);
}
// --------------------------------------------------------------
// Hilfsfunktionen für nächste Öffnungszeiten (lesbare Anzeige)
// Für DE: nächster Werktag, für USA: nächster Werktag in ET
function getNextGermanOpening(exchangeConfig) {
const now = new Date();
let nextDate = new Date(now);
// Falls aktuell geöffnet -> vielleicht interessiert trotzdem "nächste schließung" aber besser nächste Öffnung falls geschlossen
// Wir nutzen für die Anzeige "nächste Öffnung" wenn aktuell geschlossen.
// Inkrementiere Tag für Tag bis Werktag und setze Uhrzeit auf openHour/openMinute
let daysAdded = 0;
let candidate = new Date(now);
if (isWeekend(candidate)) {
// springe zum Montag
let dow = candidate.getDay();
let addDays = (dow === 0) ? 1 : (6 - dow + 1); // So -> +1 ; Sa->+2
candidate.setDate(candidate.getDate() + addDays);
daysAdded = addDays;
}
// Jetzt check ob innerhalb Handelsfenster? Wenn aktuell NACH closing time (und wochentag), dann nächsten Tag
while (true) {
if (isWeekend(candidate)) {
candidate.setDate(candidate.getDate() + 1);
continue;
}
const hours = candidate.getHours();
const mins = candidate.getMinutes();
const currMin = hours*60 + mins;
const closeMin = exchangeConfig.closeHour*60 + (exchangeConfig.closeMinute||0);
const openMin = exchangeConfig.openHour*60 + (exchangeConfig.openMinute||0);
// wenn aktuelle Zeit bereits nach Schließung, dann nächsten Tag, selbe Öffnungszeit
if (currMin >= closeMin) {
candidate.setDate(candidate.getDate() + 1);
continue;
}
// wenn vor Öffnung (selber Tag), setze auf Öffnungszeit
if (currMin < openMin) {
candidate.setHours(exchangeConfig.openHour, exchangeConfig.openMinute || 0, 0, 0);
} else {
// wir sind innerhalb der Öffnungszeit? eigentlich dann wäre offen, aber dann ruft diese Funktion nur bei geschlossen auf
// aber für Sicherheit: setze auf Öffnung morgen?
if (currMin >= openMin && currMin < closeMin) {
// eigentlich offen, trotzdem schauen wir auf morgen? gewünscht: nächste Öffnung nach schließung
candidate.setDate(candidate.getDate() + 1);
continue;
}
candidate.setHours(exchangeConfig.openHour, exchangeConfig.openMinute || 0, 0, 0);
}
break;
}
return candidate;
}
function getNextUSOpening() {
const now = new Date();
const etNowStr = now.toLocaleString("en-US", { timeZone: "America/New_York" });
let etDate = new Date(etNowStr);
let nextOpenET = new Date(etDate);
// 9:30 AM ET
if (isWeekend(etDate)) {
// add days to Monday
let dow = etDate.getDay();
let addDays = (dow === 0) ? 1 : (6 - dow + 1);
nextOpenET.setDate(nextOpenET.getDate() + addDays);
nextOpenET.setHours(9, 30, 0, 0);
} else {
const currentHourET = etDate.getHours();
const currentMinET = etDate.getMinutes();
const currETMin = currentHourET*60+currentMinET;
const closeETMin = 16*60;
const openETMin = 9*60+30;
if (currETMin < openETMin) {
// gleicher Tag, vor Öffnung
nextOpenET.setHours(9, 30, 0, 0);
} else if (currETMin >= closeETMin) {
// nach Börsenschluss, nächster Werktag
nextOpenET.setDate(nextOpenET.getDate() + 1);
if (nextOpenET.getDay() === 6) nextOpenET.setDate(nextOpenET.getDate() + 2);
if (nextOpenET.getDay() === 0) nextOpenET.setDate(nextOpenET.getDate() + 1);
nextOpenET.setHours(9, 30, 0, 0);
} else {
// eigentlich geöffnet, falls diese Funktion aufgerufen wird (geschlossen), dann seltsam, aber für Klarheit: nächstes Öffnungsfenster nächster Tag
nextOpenET.setDate(nextOpenET.getDate() + 1);
if (nextOpenET.getDay() === 6) nextOpenET.setDate(nextOpenET.getDate() + 2);
if (nextOpenET.getDay() === 0) nextOpenET.setDate(nextOpenET.getDate() + 1);
nextOpenET.setHours(9, 30, 0, 0);
}
}
// Umwandeln zurück in lokale Zeit für Anzeige
const nextLocal = new Date(nextOpenET.toLocaleString("en-US", { timeZone: "Europe/Berlin" }));
// Vermeide falsche Anzeige: direkt Datum
return nextOpenET; // als ET-Date referenz, für Formatierung in lokale Zeit
}
// Formatierung für nächste Öffnung (lokales Datum + Uhrzeit)
function formatNextOpening(dateObj, isUS=false) {
if (!dateObj) return " — ";
// Für US: Umwandeln zu lokaler Zeitanzeige des Users (Browser)
const localDate = new Date(dateObj);
return localDate.toLocaleString(undefined, { weekday: 'short', day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
}
// ----- Exchanges definieren -----
const exchanges = [
{
name: "🇩🇪 Tradegate",
short: "Tradegate",
region: "DE",
config: { openHour: 8, openMinute: 0, closeHour: 22, closeMinute: 0 } // 8:00 - 22:00
},
{
name: "🇩🇪 Xetra",
short: "Xetra",
region: "DE",
config: { openHour: 9, openMinute: 0, closeHour: 17, closeMinute: 30 }
},
{
name: "🇩🇪 Börse Frankfurt",
short: "Frankfurt",
region: "DE",
config: { openHour: 8, openMinute: 0, closeHour: 20, closeMinute: 0 }
},
{
name: "🇺🇸 NYSE",
short: "NYSE",
region: "US",
config: null // special US handling
},
{
name: "🇺🇸 NASDAQ",
short: "NASDAQ",
region: "US",
config: null
}
];
function updateStatus() {
const now = new Date();
const grid = document.getElementById("exchangeGrid");
if (!grid) return;
grid.innerHTML = "";
for (let ex of exchanges) {
let isOpen = false;
let nextOpenDate = null;
let closingTimeStr = "";
let openingTimeStr = "";
if (ex.region === "DE" && ex.config) {
isOpen = isGermanExchangeOpen(ex.config, now);
// Schließzeit (lokal)
const closeH = ex.config.closeHour;
const closeM = ex.config.closeMinute;
const openH = ex.config.openHour;
const openM = ex.config.openMinute;
openingTimeStr = `${openH.toString().padStart(2,'0')}:${openM.toString().padStart(2,'0')} Uhr`;
closingTimeStr = `${closeH.toString().padStart(2,'0')}:${closeM.toString().padStart(2,'0')} Uhr`;
if (!isOpen) {
const nextOpen = getNextGermanOpening(ex.config);
nextOpenDate = nextOpen;
} else {
// Für geöffnete Börse: nächste Schließung am selben Tag
let closeToday = new Date(now);
closeToday.setHours(closeH, closeM, 0, 0);
if (closeToday < now) closeToday.setDate(closeToday.getDate() + 1);
nextOpenDate = closeToday; // für Anzeige "nächste Änderung: schließt"
}
}
else if (ex.region === "US") {
isOpen = isUSEasternExchangeOpen(now);
// US Öffnungszeiten in ET anzeigen, aber wir zeigen auch lokale Referenz
openingTimeStr = "15:30 Uhr (MEZ) / 09:30 ET";
closingTimeStr = "22:00 Uhr (MEZ) / 16:00 ET";
if (!isOpen) {
const nextOpenET = getNextUSOpening();
nextOpenDate = nextOpenET;
} else {
// bei geöffneter US Börse: nächstes Ereignis = Schließung heute (16:00 ET)
const nowET = new Date(now.toLocaleString("en-US", { timeZone: "America/New_York" }));
let closeTodayET = new Date(nowET);
closeTodayET.setHours(16, 0, 0, 0);
if (closeTodayET <= nowET) closeTodayET.setDate(closeTodayET.getDate() + 1);
nextOpenDate = closeTodayET;
}
}
// build extra event description
let eventText = "";
if (isOpen) {
eventText = `🔒 Schließt am ${formatNextOpening(nextOpenDate, ex.region === "US")}`;
} else {
if (nextOpenDate) eventText = `🟢 Nächste Öffnung: ${formatNextOpening(nextOpenDate, ex.region === "US")}`;
else eventText = `⛔ Keine Öffnungszeiten (Wochenende/Feiertag?)`;
}
const statusClass = isOpen ? "open" : "closed";
const statusLabel = isOpen ? "🔓 GEÖFFNET" : "🔒 GESCHLOSSEN";
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `
<div class="exchange-name">
<h2>${ex.name}</h2>
<span class="status ${statusClass}">${statusLabel}</span>
</div>
<div class="info-row">
<span class="label">📅 Handelszeiten (lokal):</span>
<span class="value">${openingTimeStr} ${closingTimeStr}</span>
</div>
<div class="info-row">
<span class="label">⏱️ Wochenende:</span>
<span class="value">Sa/So geschlossen (kein regulärer Handel)</span>
</div>
<div class="next-event">
${eventText}
</div>
<div class="info-row" style="margin-top: 0.5rem;">
<span class="label">🕒 Serverzeit (dein Gerät):</span>
<span class="value">${now.toLocaleTimeString()}</span>
</div>
`;
grid.appendChild(card);
}
// Zusatzkarte: Hinweis zu "Limit-Order" Wochenende
const noteCard = document.createElement("div");
noteCard.className = "card";
noteCard.style.background = "#eef2f1";
noteCard.innerHTML = `
<div class="exchange-name">
<h2>📌 Für deine offenen Orders</h2>
<span class="status closed">WochenendModus</span>
</div>
<div style="font-size:0.9rem; margin-top: 0.3rem;">
⚡ Heute ist <strong>${now.toLocaleDateString(undefined, { weekday: 'long' })}</strong>.<br>
Deine LimitOrder wird <strong>am nächsten Werktag (Montag früh)</strong> automatisch ausgeführt, sobald der Markt öffnet und dein Limit erreicht wird.<br>
✅ <strong>Tradegate & Xetra:</strong> Samstag/Sonntag keine Ausführung.
</div>
<div class="next-event" style="margin-top: 0.8rem;">
💡 Geduld zahlt sich aus am Montag um 8:00/9:00 Uhr geht's weiter.
</div>
`;
grid.appendChild(noteCard);
}
updateStatus();
const btn = document.getElementById("refreshBtn");
if (btn) {
btn.addEventListener("click", () => {
updateStatus();
});
}
// Optional: Auto refresh alle 60 Sekunden (optional)
setInterval(() => {
updateStatus();
}, 60000);
})();
</script>
</body>
</html>