CO2 = ATMEN
This commit is contained in:
98
CO2/breath.py
Normal file
98
CO2/breath.py
Normal 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
28
CO2/manifest.md
Normal 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
38
gold_market_report.txt
Normal 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)
|
||||
|
||||
================================================================================
|
||||
@@ -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
|
||||
|
||||
@@ -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
31
run_dashboard.sh
Executable 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
153
server.py
Normal 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
401
templates/index.html
Normal 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>© 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>
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user