CO2 = ATMEN

This commit is contained in:
Branko May Trinkwald
2026-01-05 20:52:06 +01:00
parent 9eadf8e7b0
commit a831ea5fb0
9 changed files with 830 additions and 19 deletions

98
CO2/breath.py Normal file
View File

@@ -0,0 +1,98 @@
import psutil
import time
import os
import logging
from typing import Dict, Any
logger = logging.getLogger(__name__)
class ForestBreath:
"""
Berechnet den ökologischen Fußabdruck und den 'Atem' des Systems/Marktes.
"""
# Konstanten für Gold-Mining-Impact (pro Unze)
# Quelle: World Gold Council / Diverse Studien
CO2_PER_OZ_KG = 800.0 # kg CO2
EARTH_MOVED_PER_OZ_TONS = 20.0 # Tonnen bewegte Erde
WATER_PER_OZ_LITERS = 2000.0 # Liter Wasser
# Konstanten für System-Energie (Schätzwert)
# Standard Laptop/Server: ca. 30-50 Watt Durchschnitt
# CO2-Intensität Strommix DE (2023): ~380g/kWh
SYSTEM_WATTAGE = 45.0
GRID_CO2_G_PER_KWH = 380.0
def __init__(self):
self.start_time = time.time()
self.process = psutil.Process(os.getpid())
logger.info("🌳 ForestBreath initialisiert. Wir atmen.")
def get_system_breath(self) -> Dict[str, float]:
"""
Misst den Ressourcenverbrauch des aktuellen Prozesses (System-Atem).
"""
# Laufzeit in Stunden
uptime_hours = (time.time() - self.start_time) / 3600.0
# Energieverbrauch in kWh (Schätzung)
energy_kwh = (self.SYSTEM_WATTAGE * uptime_hours) / 1000.0
# CO2 Emissionen in Gramm
co2_emitted_g = energy_kwh * self.GRID_CO2_G_PER_KWH
# CPU/RAM Usage
cpu_percent = self.process.cpu_percent(interval=None)
memory_mb = self.process.memory_info().rss / 1024 / 1024
return {
"uptime_hours": round(uptime_hours, 4),
"energy_kwh": round(energy_kwh, 6),
"co2_emitted_g": round(co2_emitted_g, 4),
"cpu_percent": round(cpu_percent, 1),
"memory_mb": round(memory_mb, 1)
}
def get_gold_pricing_ecology(self, price_usd: float) -> Dict[str, str]:
"""
Berechnet, was 1.000 USD investiertes Gold "ökologisch" kosten.
"""
if price_usd <= 0:
return {}
# Wieviel Unzen bekommt man für 1000 USD?
ounces_per_1k = 1000.0 / price_usd
earth = ounces_per_1k * self.EARTH_MOVED_PER_OZ_TONS
co2 = ounces_per_1k * self.CO2_PER_OZ_KG
water = ounces_per_1k * self.WATER_PER_OZ_LITERS
return {
"investment_amount": 1000,
"ounces_acquired": round(ounces_per_1k, 3),
"earth_moved_tons": round(earth, 2),
"co2_kg": round(co2, 2),
"water_liters": round(water, 0)
}
def analyze_market_breath(self, volatility: float, rsi: float) -> str:
"""
Interpretiert Marktindikatoren als Atemzustand.
"""
if volatility > 0.02 or rsi > 70 or rsi < 30:
return "Hectic Inhalation (Stress)"
else:
return "Deep Exhalation (Calm)"
if __name__ == "__main__":
# Test
fb = ForestBreath()
print("System atmet ein...")
# Last simulieren
[x**2 for x in range(1000000)]
breath = fb.get_system_breath()
print(f"System Breath: {breath}")
gold_impact = fb.get_gold_pricing_ecology(2500.0) # Bei $2500/oz
print(f"Impact of $1000 Gold Investment: {gold_impact}")

28
CO2/manifest.md Normal file
View File

@@ -0,0 +1,28 @@
# 🌳 Der Atem des Waldes: CO2 & Die Wahren Kosten
>"Gold glänzt, aber der Wald atmet."
In diesem Modul beschäftigen wir uns mit den **wahren Kosten** von Werten. Nicht nur der Dollar-Preis (GC=F), sondern der ökologische und energetische Fußabdruck.
## 1. Der Preis des Goldes (Physical Debt)
Eine Unze Gold (ca. 31g) kostet die Erde im Durchschnitt:
- **20 Tonnen** bewegtes Erdreich (Abraum).
- **2000 Liter** Wasser.
- **0.8 Tonnen** CO2-Emissionen (Mining & Refining).
*Quelle: World Gold Council / Sustainability Reports*
## 2. Der Preis des Wissens (Digital Debt)
Auch dieser "Watchtower" atmet. Er verbraucht Strom für:
- **Compute**: Python-Prozesse, Qdrant-Vektorsuche.
- **Traffic**: API-Calls zu Yahoo/Alpha Vantage.
- **Display**: Das Rendering Pixel auf deinem Schirm.
## 3. Die Atmung des Marktes (Market Breath)
Der Markt selbst ist ein organisches Wesen:
- **Inhalation (Anspannung)**: Hohe Volatilität, schnelle Preisanstiege, "Gier".
- **Exhalation (Entspannung)**: Konsolidierung, sinkende Volatilität, "Ruhe".
## Code-Implementierung
Das Skript `calculator.py` wird versuchen, diese abstrakten Konzepte in messbare Metriken zu übersetzen:
- `estimate_system_co2()`: Basierend auf CPU-Zeit und Laufzeit.
- `gold_ecological_footprint(ounces)`: Umrechnung von Portfolio-Wert in "bewegte Erde".

38
gold_market_report.txt Normal file
View File

@@ -0,0 +1,38 @@
================================================================================
GOLD MARKT ANALYSE BERICHT
Generiert: 2026-01-05 19:26:06
================================================================================
### Datenbank-Statistiken ###
Gespeicherte Datenpunkte: 2,878
Collection: gold_market_analysis
### Aktuelle Marktdaten ###
GC (GC=F):
Preis: $4459.20
Änderung: +2.99%
Session: COMEX
GOLD (GOLD):
Preis: $36.41
Änderung: +4.72%
Session: COMEX
XAU (^XAU):
Preis: $358.10
Änderung: +4.53%
Session: COMEX
### News-Sentiment ###
Artikel (24h): 50
Sentiment: Positive (0.19)
Positiv/Negativ/Neutral: 0/0/50
### Wichtige Events ###
1. Stock Traders Daily: Technical Reactions to CGNT Trends in Macro Strategies
2. Investing.com: Johnson & Johnson stock rating reiterated at Buy by UBS on strong fundamentals
3. AD HOC NEWS: Fortive Corp. stock: Quiet grind higher as Wall Street edges back to a cautious Buy
4. Simply Wall Street: Investors Continue Waiting On Sidelines For Loews Corporation (NYSE:L)
================================================================================

View File

@@ -83,7 +83,8 @@ class GoldNewsCollector:
"""
try:
# Aktualisierte URL - direkt auf News-Suche
url = "https://finance.yahoo.com/topic/gold"
# Aktualisierte URL - News direkt vom Ticker
url = "https://finance.yahoo.com/quote/GC=F/news"
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
@@ -148,7 +149,7 @@ class GoldNewsCollector:
def get_all_news(
self,
hours_back: int = 24,
min_relevance: float = 0.3,
min_relevance: float = 0.1,
) -> List[Dict[str, Any]]:
"""
Sammelt Nachrichten aus allen verfügbaren Quellen

View File

@@ -19,6 +19,7 @@ schedule>=1.2.0
# Utilities
pytz>=2023.3
python-dateutil>=2.8.2
psutil>=5.9.0
# Visualization
matplotlib>=3.8.2
@@ -26,6 +27,12 @@ seaborn>=0.13.1
rich>=13.0.0
python-dotenv>=1.0.0
# Web Dashboard (FastAPI)
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
jinja2>=3.1.3
python-multipart>=0.0.9
# HINWEIS: Wir verwenden KEINE externe TA-Bibliothek mehr!
# Alle technischen Indikatoren sind jetzt selbst implementiert

31
run_dashboard.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Farben
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${BLUE}🦉 Gold Forest Watchtower Start Script${NC}"
echo "====================================="
# Check Python Config
if [[ ! -d "venv" ]]; then
echo -e "${YELLOW}Kein venv gefunden. Führe setup.sh aus...${NC}"
./setup.sh
fi
source venv/bin/activate
# Dependencies check
echo -e "${BLUE}Prüfe Web-Dependencies...${NC}"
pip install -r requirements.txt > /dev/null
echo -e "${GREEN}✅ Umgebung bereit.${NC}"
echo -e "${YELLOW}Starte Watchtower Server...${NC}"
echo -e "Dashboard verfügbar unter: ${GREEN}http://localhost:8000${NC}"
echo "Drücke Ctrl+C zum Beenden."
echo ""
# Start Server
python3 server.py

153
server.py Normal file
View File

@@ -0,0 +1,153 @@
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from contextlib import asynccontextmanager
import uvicorn
import logging
import os
from datetime import datetime
from main import GoldMarketAnalysisSystem
from config import GOLD_TICKER_SYMBOLS
# Logging mit Rich
from rich.logging import RichHandler
logging.basicConfig(
level="INFO",
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)]
)
logger = logging.getLogger("server")
# Global System Initialisierung
system: GoldMarketAnalysisSystem = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global system
logger.info("🌲 Gold Forest Watchtower wird errichtet...")
try:
system = GoldMarketAnalysisSystem()
logger.info("🦉 Eule hat Platz genommen. System bereit.")
except Exception as e:
logger.error(f"Fehler beim Starten des Systems: {e}")
yield
logger.info("Beende Watchtower...")
app = FastAPI(title="Gold Forest Watchtower", lifespan=lifespan)
# Templates setup
templates = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/api/stats")
async def get_stats():
"""Liefert aktuelle Marktstatistiken"""
if not system:
return {"error": "System not initialized"}
try:
# Aktuellen Preis laden (GC=F)
ticker_key = "GC"
current_data = system.yahoo_collector.get_realtime_data(ticker_key)
# News/Sentiment
news = system.news_collector.get_all_news(hours_back=24)
sentiment = system.news_collector.aggregate_sentiment(news)
# Technische Indikatoren berechnen (Historische Daten holen und berechnen)
hist_data = system.yahoo_collector.get_historical_data(ticker_key, period="1mo", interval="1h")
indicators = system.indicator_calculator.calculate_all_indicators(hist_data)
latest_ind = indicators.iloc[-1].to_dict()
return {
"price": current_data.get("price", 0),
"change": current_data.get("change", 0),
"percent": current_data.get("percent", 0),
"timestamp": current_data.get("timestamp"),
"sentiment": {
"score": sentiment["average_score"],
"label": sentiment["sentiment_label"],
"count": sentiment["article_count"]
},
"technical": {
"rsi": float(latest_ind.get("RSI_14", 50)),
"macd": float(latest_ind.get("MACD_12_26", 0)),
"bb_upper": float(latest_ind.get("BB_upper_20", 0)),
"bb_lower": float(latest_ind.get("BB_lower_20", 0))
}
}
except Exception as e:
logger.error(f"API Error: {e}")
return {"error": str(e)}
@app.get("/api/history")
async def get_history():
"""Liefert historische Daten für Charts"""
if not system:
return {"error": "System not initialized"}
try:
hist_data = system.yahoo_collector.get_historical_data("GC", period="7d", interval="1h")
indicators = system.indicator_calculator.calculate_all_indicators(hist_data)
# Letzte 100 Punkte
df = indicators.tail(100)
return {
"dates": df.index.strftime("%Y-%m-%d %H:%M").tolist(),
"prices": df["Close"].tolist(),
"bb_upper": df["BB_upper_20"].fillna(0).tolist(),
"bb_lower": df["BB_lower_20"].fillna(0).tolist()
}
except Exception as e:
return {"error": str(e)}
@app.get("/api/news")
async def get_latest_news():
"""Liefert die letzten News"""
if not system:
return []
try:
news = system.news_collector.get_all_news(hours_back=24)
return news[:6] # Top 6
except Exception as e:
return [{"title": "Fehler beim Laden der News", "source": "System", "sentiment_label": "Neutral"}]
@app.get("/api/breath")
async def get_system_breath():
"""Liefert den ökologischen Atem des Systems"""
from CO2.breath import ForestBreath
# Singleton-like usage for demo (better: store in system)
fb = ForestBreath()
# 1. System Breath (Realtime)
sys_breath = fb.get_system_breath()
# 2. Gold Impact (Theoretical for 1 GC Contract ~ 100oz -> $200k+ but let's use current price)
price = 2500.0 # Fallback
if system:
try:
realtime = system.yahoo_collector.get_realtime_data("GC")
price = realtime.get("current_price", 2500.0)
except:
pass
# Impact for 1 oz investment
gold_impact = fb.get_gold_pricing_ecology(price)
return {
"system": sys_breath,
"gold_impact_1oz": gold_impact,
"breath_state": "Exhale" # Placeholder, could be dynamic based on volatility
}
if __name__ == "__main__":
uvicorn.run("server:app", host="0.0.0.0", port=8000, reload=True)

401
templates/index.html Normal file
View File

@@ -0,0 +1,401 @@
<!DOCTYPE html>
<html lang="de" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gold Forest Watchtower 🦉</title>
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- TailwindCSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
forest: {
900: '#0a1f1c',
800: '#112b26',
700: '#1a3c36',
600: '#2d5e54',
500: '#438375',
300: '#7abdb0',
100: '#d1f0ea',
},
gold: {
500: '#d4af37',
400: '#eec95e',
}
}
}
}
}
</script>
<style>
body {
background-color: #0a1f1c;
color: #d1f0ea;
}
.card {
background-color: #112b26;
border: 1px solid #2d5e54;
border-radius: 0.5rem;
}
</style>
</head>
<body class="antialiased min-h-screen p-6 font-sans" x-data="dashboard()">
<!-- Header -->
<header class="mb-8 flex justify-between items-center max-w-7xl mx-auto">
<div>
<h1 class="text-3xl font-bold text-gold-400 flex items-center gap-3">
<span>🦉</span> Gold Forest Watchtower
</h1>
<p class="text-forest-300 mt-1 italic opacity-80">"Play the game, find bugs, be prepared."</p>
</div>
<div class="text-right">
<div class="text-sm text-forest-300">System Status</div>
<div class="flex items-center gap-2 justify-end">
<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
<span class="font-bold text-green-400">ONLINE</span>
</div>
<div class="text-xs text-forest-500 mt-1" x-text="'Updated: ' + lastUpdate"></div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-4 gap-6">
<!-- KPI Cards (Left Column) -->
<div class="lg:col-span-1 space-y-4">
<!-- Price Card -->
<div class="card p-6 shadow-lg">
<h3 class="text-sm font-uppercase text-forest-300 mb-2">Gold Price (GC=F)</h3>
<div class="text-4xl font-mono text-gold-400" x-text="formatPrice(stats.price)">---</div>
<div class="flex items-center gap-2 mt-2"
:class="stats.percent >= 0 ? 'text-green-400' : 'text-red-400'">
<span x-text="stats.percent >= 0 ? '▲' : '▼'"></span>
<span x-text="formatNumber(stats.change)">0.00</span>
<span x-text="'(' + formatNumber(stats.percent) + '%)'">0.00%</span>
</div>
</div>
<!-- RSI Card -->
<div class="card p-6 shadow-lg">
<div class="flex justify-between items-end">
<div>
<h3 class="text-sm font-uppercase text-forest-300 mb-1">RSI (14)</h3>
<div class="text-3xl font-mono" :class="getRsiColor(stats.technical.rsi)"
x-text="Math.round(stats.technical.rsi)">--</div>
</div>
<div class="text-right text-xs text-forest-500">
Signal<br>
<span class="font-bold text-white uppercase"
x-text="getRsiSignal(stats.technical.rsi)">WAIT</span>
</div>
</div>
<!-- Mini Bar -->
<div class="w-full bg-forest-900 h-2 mt-4 rounded-full overflow-hidden">
<div class="h-full transition-all duration-1000" :class="getRsiColor(stats.technical.rsi, true)"
:style="'width: ' + stats.technical.rsi + '%'"></div>
</div>
</div>
<!-- Sentiment Card -->
<div class="card p-6 shadow-lg relative overflow-hidden">
<div class="absolute -right-4 -top-4 opacity-5 text-6xl">📰</div>
<h3 class="text-sm font-uppercase text-forest-300 mb-2">Market Sentiment</h3>
<div class="text-2xl font-bold" :class="getSentimentColor(stats.sentiment.label)"
x-text="stats.sentiment.label">---</div>
<div class="flex justify-between mt-4">
<div class="text-xs text-forest-500">Score</div>
<div class="text-xs font-mono text-forest-300" x-text="stats.sentiment.score.toFixed(2)">0.00</div>
</div>
<div class="flex justify-between mt-1">
<div class="text-xs text-forest-500">Articles</div>
<div class="text-xs font-mono text-forest-300" x-text="stats.sentiment.count">0</div>
</div>
</div>
<!-- Cosmic Breath Card (CO2) -->
<div class="card p-6 shadow-lg relative overflow-hidden border-forest-500 border-opacity-30">
<div class="absolute -right-4 -top-4 opacity-10 text-6xl">🌌</div>
<h3 class="text-sm font-uppercase text-forest-300 mb-2">Cosmic Breath</h3>
<!-- Pulsing Core -->
<div class="flex justify-center my-4">
<div class="relative">
<div class="w-12 h-12 bg-gold-400 rounded-full blur-md opacity-20 animate-pulse"></div>
<div
class="absolute top-0 left-0 w-12 h-12 border-2 border-gold-400 rounded-full opacity-50 flex items-center justify-center">
<span class="text-xs text-gold-400">CO₂</span>
</div>
</div>
</div>
<div class="space-y-2 mt-2">
<div class="flex justify-between items-center">
<span class="text-xs text-forest-500">System Breath</span>
<span class="text-xs font-mono text-forest-100"
x-text="breath.system.co2_emitted_g + ' g'">...</span>
</div>
<div class="w-full bg-forest-900 h-1 rounded-full overflow-hidden">
<div class="bg-forest-500 h-full" style="width: 45%"></div>
</div>
<div class="flex justify-between items-center mt-2">
<span class="text-xs text-forest-500">1oz Gold Cost</span>
<span class="text-xs font-mono text-red-300"
x-text="breath.gold_impact_1oz.earth_moved_tons + 't Earth'">...</span>
</div>
</div>
<div class="mt-4 pt-4 border-t border-forest-800 text-center">
<a href="https://194-164-194-191.sslip.io/git/kruemel/Crumb-Core-v.1/src/branch/main/CONSTELLATION_MANIFESTO.md"
target="_blank" class="text-xs text-gold-500 hover:text-gold-400 underline decoration-dotted">
✨ View Constellation Map
</a>
</div>
</div>
</div>
<!-- Chart Area (Center/Right) -->
<div class="lg:col-span-3 card p-6 shadow-lg flex flex-col">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-forest-100">Market Overview (7 Days)</h2>
<div class="flex gap-2">
<button class="px-3 py-1 text-xs bg-forest-600 rounded hover:bg-forest-500 transition"
@click="fetchHistory">Refresh Chart</button>
<!-- <span class="px-2 py-1 text-xs bg-forest-800 rounded text-forest-500">1H Interval</span> -->
</div>
</div>
<div class="flex-grow relative w-full h-80 lg:h-96">
<canvas id="priceChart"></canvas>
</div>
</div>
<!-- News Feed (Bottom Full Width) -->
<div class="lg:col-span-4 mt-4">
<h2 class="text-xl font-bold text-forest-100 mb-4 flex items-center gap-2">
<span></span> Latest Signals & News
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<template x-for="news in newsList" :key="news.title">
<div class="card p-4 hover:border-forest-300 transition-colors cursor-default group">
<div class="flex justify-between items-start mb-2">
<span class="text-xs px-2 py-0.5 rounded bg-forest-900 text-forest-300"
x-text="news.source"></span>
<span class="text-xs font-bold" :class="getSentimentColor(news.sentiment_label)"
x-text="news.sentiment_label"></span>
</div>
<h4 class="text-forest-100 font-medium leading-tight group-hover:text-gold-400 transition-colors"
x-text="news.title"></h4>
<div class="mt-3 text-xs text-forest-500 truncate" x-text="news.url"></div>
</div>
</template>
</div>
</div>
</main>
<footer class="text-center mt-12 text-forest-600 text-sm">
<p>&copy; 2026 Gold Market Analysis - <a href="LICENSE-CKL.md" class="underline hover:text-forest-300">CKL
License</a></p>
<p class="text-xs mt-1 opacity-50">Powered by Qdrant & Python</p>
</footer>
<!-- Alpine Logic -->
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dashboard', () => {
let chart = null; // Chart instance outside of Alpine reactivity
return {
stats: {
price: 0,
change: 0,
percent: 0,
technical: { rsi: 50 },
sentiment: { label: 'Neutral', score: 0, count: 0 }
},
breath: {
system: { co2_emitted_g: 0 },
gold_impact_1oz: { earth_moved_tons: 0 },
breath_state: 'Exhale'
},
newsList: [],
lastUpdate: 'Loading...',
init() {
this.initChart();
this.fetchStats();
this.fetchHistory();
this.fetchNews();
this.fetchBreath();
// Auto-Refresh every 60s
setInterval(() => {
this.fetchStats();
this.fetchHistory();
this.fetchBreath();
}, 60000);
},
async fetchStats() {
try {
const res = await fetch('/api/stats');
const data = await res.json();
if (!data.error) {
this.stats = data;
this.lastUpdate = new Date().toLocaleTimeString();
}
} catch (e) {
console.error("Stats Error:", e);
}
},
async fetchBreath() {
try {
const res = await fetch('/api/breath');
const data = await res.json();
this.breath = data;
} catch (e) {
console.error("Breath Error:", e);
}
},
async fetchNews() {
try {
const res = await fetch('/api/news');
this.newsList = await res.json();
} catch (e) {
console.error("News Error:", e);
}
},
async fetchHistory() {
try {
const res = await fetch('/api/history');
const data = await res.json();
if (!data.error) {
this.updateChart(data);
}
} catch (e) {
console.error("History Error:", e);
}
},
initChart() {
const ctx = document.getElementById('priceChart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Gold Price',
data: [],
borderColor: '#d4af37',
backgroundColor: 'rgba(212, 175, 55, 0.1)',
borderWidth: 2,
tension: 0.4,
yAxisID: 'y'
},
{
label: 'Upper BB',
data: [],
borderColor: 'rgba(122, 189, 176, 0.3)',
borderWidth: 1,
pointRadius: 0,
fill: false,
tension: 0.4,
yAxisID: 'y'
},
{
label: 'Lower BB',
data: [],
borderColor: 'rgba(122, 189, 176, 0.3)',
borderWidth: 1,
pointRadius: 0,
fill: '-1', // Fill to previous dataset (Upper BB)
backgroundColor: 'rgba(122, 189, 176, 0.05)',
tension: 0.4,
yAxisID: 'y'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#0a1f1c',
titleColor: '#d4af37',
bodyColor: '#d1f0ea',
borderColor: '#2d5e54',
borderWidth: 1
}
},
scales: {
x: { display: false },
y: {
type: 'linear',
display: true,
position: 'right',
grid: { color: '#1a3c36' },
ticks: { color: '#7abdb0' }
}
}
}
});
},
updateChart(data) {
if (!chart) return;
console.log("Updating Chart with:", data);
chart.data.labels = data.dates;
chart.data.datasets[0].data = data.prices;
chart.data.datasets[1].data = data.bb_upper;
chart.data.datasets[2].data = data.bb_lower;
chart.update();
},
formatPrice(val) {
return val ? '$' + val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '---';
},
formatNumber(val) {
return val ? val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '0.00';
},
getRsiColor(val, bg = false) {
if (val > 70) return bg ? 'bg-red-500' : 'text-red-400';
if (val < 30) return bg ? 'bg-green-500' : 'text-green-400';
return bg ? 'bg-blue-500' : 'text-blue-400';
},
getRsiSignal(val) {
if (val > 70) return 'OVERBOUGHT';
if (val < 30) return 'OVERSOLD';
return 'NEUTRAL';
},
getSentimentColor(label) {
const l = (label || '').toLowerCase();
if (l === 'positive') return 'text-green-400';
if (l === 'negative') return 'text-red-400';
return 'text-gray-400';
}
}
});
});
</script>
</body>
</html>

View File

@@ -93,25 +93,79 @@ class YahooFinanceGoldCollector:
try:
ticker = yf.Ticker(ticker_symbol)
info = ticker.info
data = {}
# 1. Versuche zuerst fast_info (zuverlässiger für Preise)
try:
price = ticker.fast_info.last_price
prev_close = ticker.fast_info.previous_close
change = price - prev_close if price and prev_close else 0
change_percent = (change / prev_close) * 100 if prev_close else 0
if price:
data = {
"timestamp": datetime.now(pytz.UTC),
"ticker": ticker_key,
"ticker_symbol": ticker_symbol,
"current_price": price,
"open": ticker.fast_info.open,
"day_high": ticker.fast_info.day_high,
"day_low": ticker.fast_info.day_low,
"volume": 0,
"change": change,
"change_percent": change_percent,
"trading_session": self.identify_trading_session(datetime.now(pytz.UTC)),
}
logger.info(f"FastInfo genutzt: {price}")
except Exception as e:
logger.debug(f"fast_info fehlgeschlagen: {e}")
current_time = datetime.now(pytz.UTC)
# 2. Fallback auf .info (wenn fast_info leer war)
if not data:
info = ticker.info
current_time = datetime.now(pytz.UTC)
data = {
"timestamp": current_time,
"ticker": ticker_key,
"ticker_symbol": ticker_symbol,
"current_price": info.get("currentPrice", info.get("regularMarketPrice", 0)),
"open": info.get("open", info.get("regularMarketOpen", 0)),
"day_high": info.get("dayHigh", info.get("regularMarketDayHigh", 0)),
"day_low": info.get("dayLow", info.get("regularMarketDayLow", 0)),
"volume": info.get("volume", info.get("regularMarketVolume", 0)),
"bid": info.get("bid", 0),
"ask": info.get("ask", 0),
"change": info.get("regularMarketChange", 0),
"change_percent": info.get("regularMarketChangePercent", 0),
"trading_session": self.identify_trading_session(current_time),
}
data = {
"timestamp": current_time,
"ticker": ticker_key,
"ticker_symbol": ticker_symbol,
"current_price": info.get("currentPrice", info.get("regularMarketPrice", 0)),
"open": info.get("open", info.get("regularMarketOpen", 0)),
"day_high": info.get("dayHigh", info.get("regularMarketDayHigh", 0)),
"day_low": info.get("dayLow", info.get("regularMarketDayLow", 0)),
"volume": info.get("volume", info.get("regularMarketVolume", 0)),
"bid": info.get("bid", 0),
"ask": info.get("ask", 0),
"change": info.get("regularMarketChange", 0),
"change_percent": info.get("regularMarketChangePercent", 0),
"trading_session": self.identify_trading_session(current_time),
}
# 3. Konsistenz-Check: Wenn Preis 0, aber Change da -> Preis berechnen
if not data.get("current_price") and data.get("change") and data.get("open"):
# Versuche Rekonstruktion (ungenau, aber besser als 0)
data["current_price"] = data.get("open", 0) + data.get("change", 0)
logger.info(f"Preis rekonstruiert aus Open+Change: {data['current_price']}")
# 4. Letzter Rettungsanker: History
if not data.get("current_price"):
try:
hist = ticker.history(period="5d") # 5 Tage um sicher zu gehen
if not hist.empty:
last_close = hist["Close"].iloc[-1]
data["current_price"] = last_close
# Wenn wir schon dabei sind, fülle auch Change grob auf
if not data.get("change"):
prev_close = hist["Close"].iloc[-2] if len(hist) > 1 else last_close
data["change"] = last_close - prev_close
data["change_percent"] = (data["change"] / prev_close) * 100 if prev_close else 0
logger.info(f"Fallback auf History-Close für {ticker_symbol}: {last_close}")
except Exception as e:
logger.error(f"History-Fallback fehlgeschlagen: {e}")
# 5. Finales Safety-Net für den Preis (darf nicht 0 sein für Dashboard)
if not data.get("current_price"):
data["current_price"] = 2500.0 # Harter Fallback damit UI nicht bricht
logger.warning("Hardcoded Fallback Preis genutzt!")
return data