This commit is contained in:
18
daemon.py
18
daemon.py
@@ -5,6 +5,9 @@ import os
|
|||||||
import requests
|
import requests
|
||||||
from src.exchanges.eix import EIXExchange
|
from src.exchanges.eix import EIXExchange
|
||||||
from src.exchanges.ls import LSExchange
|
from src.exchanges.ls import LSExchange
|
||||||
|
from src.exchanges.deutsche_boerse import XetraExchange, FrankfurtExchange, QuotrixExchange
|
||||||
|
from src.exchanges.gettex import GettexExchange
|
||||||
|
from src.exchanges.stuttgart import StuttgartExchange
|
||||||
from src.database.questdb_client import DatabaseClient
|
from src.database.questdb_client import DatabaseClient
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -45,6 +48,13 @@ def run_task(historical=False):
|
|||||||
eix = EIXExchange()
|
eix = EIXExchange()
|
||||||
ls = LSExchange()
|
ls = LSExchange()
|
||||||
|
|
||||||
|
# Neue Deutsche Börse Exchanges
|
||||||
|
xetra = XetraExchange()
|
||||||
|
frankfurt = FrankfurtExchange()
|
||||||
|
quotrix = QuotrixExchange()
|
||||||
|
gettex = GettexExchange()
|
||||||
|
stuttgart = StuttgartExchange()
|
||||||
|
|
||||||
# Pass last_ts to fetcher to allow smart filtering
|
# Pass last_ts to fetcher to allow smart filtering
|
||||||
# daemon.py runs daily, so we want to fetch everything since DB state
|
# daemon.py runs daily, so we want to fetch everything since DB state
|
||||||
# BUT we need to be careful: eix.py's fetch_latest_trades needs 'since_date' argument
|
# BUT we need to be careful: eix.py's fetch_latest_trades needs 'since_date' argument
|
||||||
@@ -53,7 +63,13 @@ def run_task(historical=False):
|
|||||||
# We will modify the loop below to handle args dynamically
|
# We will modify the loop below to handle args dynamically
|
||||||
exchanges_to_process = [
|
exchanges_to_process = [
|
||||||
(eix, {'limit': None if historical else 5}), # Default limit 5 for safety if no historical
|
(eix, {'limit': None if historical else 5}), # Default limit 5 for safety if no historical
|
||||||
(ls, {'include_yesterday': historical})
|
(ls, {'include_yesterday': historical}),
|
||||||
|
# Neue Exchanges
|
||||||
|
(xetra, {'include_yesterday': historical}),
|
||||||
|
(frankfurt, {'include_yesterday': historical}),
|
||||||
|
(quotrix, {'include_yesterday': historical}),
|
||||||
|
(gettex, {'include_yesterday': historical}),
|
||||||
|
(stuttgart, {'include_yesterday': historical}),
|
||||||
]
|
]
|
||||||
|
|
||||||
db = DatabaseClient(host="questdb", user=DB_USER, password=DB_PASSWORD)
|
db = DatabaseClient(host="questdb", user=DB_USER, password=DB_PASSWORD)
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-400 mb-2">Exchanges (optional, komma-separiert)</label>
|
<label class="block text-sm font-bold text-slate-400 mb-2">Exchanges (optional, komma-separiert)</label>
|
||||||
<input type="text" id="customExchanges" class="input-glass" placeholder="z.B. EIX,LS" onchange="updateCustomGraph(); updateUrlParams()">
|
<input type="text" id="customExchanges" class="input-glass" placeholder="z.B. EIX,LS,XETRA,FRA,GETTEX,STU,QUOTRIX" onchange="updateCustomGraph(); updateUrlParams()">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -261,7 +261,8 @@
|
|||||||
const dates = [...new Set(data.map(r => r[dateIdx]))].sort();
|
const dates = [...new Set(data.map(r => r[dateIdx]))].sort();
|
||||||
|
|
||||||
const datasets = [];
|
const datasets = [];
|
||||||
const colors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6'];
|
// Erweiterte Farben für mehr Exchanges (EIX, LS, XETRA, FRA, GETTEX, STU, QUOTRIX)
|
||||||
|
const colors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899', '#14b8a6', '#84cc16', '#a855f7'];
|
||||||
|
|
||||||
exchanges.forEach((exchange, idx) => {
|
exchanges.forEach((exchange, idx) => {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
@@ -579,7 +580,8 @@
|
|||||||
const groups = [...new Set(data.map(r => r[groupIdx]))];
|
const groups = [...new Set(data.map(r => r[groupIdx]))];
|
||||||
const dates = [...new Set(data.map(r => r[xIdx]))].sort();
|
const dates = [...new Set(data.map(r => r[xIdx]))].sort();
|
||||||
|
|
||||||
const colors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899'];
|
// Erweiterte Farben für mehr Exchanges (EIX, LS, XETRA, FRA, GETTEX, STU, QUOTRIX)
|
||||||
|
const colors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899', '#14b8a6', '#84cc16', '#a855f7'];
|
||||||
const datasets = groups.map((group, idx) => ({
|
const datasets = groups.map((group, idx) => ({
|
||||||
label: group || 'Unknown',
|
label: group || 'Unknown',
|
||||||
data: dates.map(d => {
|
data: dates.map(d => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
requests
|
requests
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
|
lxml
|
||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
pandas
|
pandas
|
||||||
|
|||||||
@@ -650,30 +650,60 @@ class AnalyticsWorker:
|
|||||||
logger.error("Failed to connect to QuestDB. Exiting.")
|
logger.error("Failed to connect to QuestDB. Exiting.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Initiale Berechnung fehlender Tage
|
# Initiale Berechnung fehlender Tage (inkl. gestern und heute)
|
||||||
logger.info("Checking for missing dates...")
|
logger.info("Checking for missing dates...")
|
||||||
self.process_missing_dates()
|
self.process_missing_dates()
|
||||||
|
|
||||||
# Hauptschleife: Warte auf Mitternacht
|
# Stelle sicher, dass gestern und heute verarbeitet werden
|
||||||
logger.info("Waiting for midnight to process yesterday's data...")
|
today = datetime.date.today()
|
||||||
|
yesterday = today - datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
logger.info(f"Ensuring yesterday ({yesterday}) and today ({today}) are processed...")
|
||||||
|
existing_dates = self.get_existing_dates('analytics_custom')
|
||||||
|
|
||||||
|
if yesterday not in existing_dates:
|
||||||
|
logger.info(f"Processing yesterday's data: {yesterday}")
|
||||||
|
self.process_date(yesterday)
|
||||||
|
|
||||||
|
# Heute wird nur verarbeitet, wenn es bereits Trades gibt (normalerweise am Ende des Tages)
|
||||||
|
# Aber wir prüfen trotzdem, ob es Daten gibt
|
||||||
|
if today not in existing_dates:
|
||||||
|
# Prüfe ob es heute schon Trades gibt
|
||||||
|
query = f"select count(*) from trades where date_trunc('day', timestamp) = '{today}'"
|
||||||
|
data = self.query_questdb(query)
|
||||||
|
if data and data.get('dataset') and data['dataset'][0][0] and data['dataset'][0][0] > 0:
|
||||||
|
logger.info(f"Found trades for today ({today}), processing...")
|
||||||
|
self.process_date(today)
|
||||||
|
|
||||||
|
# Hauptschleife: Prüfe regelmäßig auf fehlende Tage
|
||||||
|
logger.info("Starting main loop - checking for missing dates every hour...")
|
||||||
|
last_check_hour = -1
|
||||||
while True:
|
while True:
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
current_hour = now.hour
|
||||||
|
|
||||||
# Prüfe ob es Mitternacht ist (00:00)
|
# Prüfe jede Stunde auf fehlende Tage
|
||||||
|
if current_hour != last_check_hour:
|
||||||
|
logger.info(f"Hourly check for missing dates (hour: {current_hour})...")
|
||||||
|
self.process_missing_dates()
|
||||||
|
last_check_hour = current_hour
|
||||||
|
|
||||||
|
# Stelle sicher, dass gestern verarbeitet wurde
|
||||||
|
yesterday = (now - datetime.timedelta(days=1)).date()
|
||||||
|
existing_dates = self.get_existing_dates('analytics_custom')
|
||||||
|
if yesterday not in existing_dates:
|
||||||
|
logger.info(f"Processing yesterday's data: {yesterday}")
|
||||||
|
self.process_date(yesterday)
|
||||||
|
|
||||||
|
# Prüfe ob es Mitternacht ist (00:00) - verarbeite dann gestern
|
||||||
if now.hour == 0 and now.minute == 0:
|
if now.hour == 0 and now.minute == 0:
|
||||||
yesterday = (now - datetime.timedelta(days=1)).date()
|
yesterday = (now - datetime.timedelta(days=1)).date()
|
||||||
logger.info(f"Processing yesterday's data: {yesterday}")
|
logger.info(f"Midnight reached - processing yesterday's data: {yesterday}")
|
||||||
self.process_date(yesterday)
|
self.process_date(yesterday)
|
||||||
# Warte 61s, um Mehrfachausführung zu verhindern
|
# Warte 61s, um Mehrfachausführung zu verhindern
|
||||||
time.sleep(61)
|
time.sleep(61)
|
||||||
|
|
||||||
# Prüfe auch auf fehlende Tage (alle 6 Stunden)
|
time.sleep(60) # Prüfe jede Minute
|
||||||
if now.hour % 6 == 0 and now.minute == 0:
|
|
||||||
logger.info("Checking for missing dates...")
|
|
||||||
self.process_missing_dates()
|
|
||||||
time.sleep(61)
|
|
||||||
|
|
||||||
time.sleep(30)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
worker = AnalyticsWorker()
|
worker = AnalyticsWorker()
|
||||||
|
|||||||
269
src/exchanges/deutsche_boerse.py
Normal file
269
src/exchanges/deutsche_boerse.py
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import requests
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import io
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import List, Optional
|
||||||
|
from .base import BaseExchange, Trade
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
# Browser User-Agent für Zugriff
|
||||||
|
HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeutscheBoerseBase(BaseExchange):
|
||||||
|
"""Basisklasse für Deutsche Börse Exchanges (Xetra, Frankfurt, Quotrix)"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self) -> str:
|
||||||
|
"""Override in subclasses"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _get_file_list(self) -> List[str]:
|
||||||
|
"""Parst die Verzeichnisseite und extrahiert alle Dateinamen"""
|
||||||
|
try:
|
||||||
|
response = requests.get(self.base_url, headers=HEADERS, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
files = []
|
||||||
|
|
||||||
|
# Deutsche Börse listet Dateien als Links auf
|
||||||
|
for link in soup.find_all('a'):
|
||||||
|
href = link.get('href', '')
|
||||||
|
# Nur posttrade JSON.gz Dateien
|
||||||
|
if 'posttrade' in href and href.endswith('.json.gz'):
|
||||||
|
files.append(href)
|
||||||
|
|
||||||
|
return files
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching file list from {self.base_url}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _filter_files_for_date(self, files: List[str], target_date: datetime.date) -> List[str]:
|
||||||
|
"""
|
||||||
|
Filtert Dateien für ein bestimmtes Datum.
|
||||||
|
Dateiformat: *posttrade-YYYY-MM-DDTHH:MM:SS*.json.gz
|
||||||
|
|
||||||
|
Da Handel bis 22:00 MEZ geht (21:00/20:00 UTC), müssen wir auch
|
||||||
|
Dateien nach Mitternacht UTC berücksichtigen.
|
||||||
|
"""
|
||||||
|
filtered = []
|
||||||
|
|
||||||
|
# Für den Vortag: Dateien vom target_date UND vom Folgetag (bis ~02:00 UTC)
|
||||||
|
target_str = target_date.strftime('%Y-%m-%d')
|
||||||
|
next_day = target_date + timedelta(days=1)
|
||||||
|
next_day_str = next_day.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
# Extrahiere Datum aus Dateiname
|
||||||
|
# Format: posttrade-2026-01-26T21:30:00.json.gz
|
||||||
|
if target_str in file:
|
||||||
|
filtered.append(file)
|
||||||
|
elif next_day_str in file:
|
||||||
|
# Prüfe ob es eine frühe Datei vom nächsten Tag ist (< 03:00 UTC)
|
||||||
|
try:
|
||||||
|
# Finde Timestamp im Dateinamen
|
||||||
|
parts = file.split('posttrade-')
|
||||||
|
if len(parts) > 1:
|
||||||
|
ts_part = parts[1].split('.json.gz')[0]
|
||||||
|
file_dt = datetime.fromisoformat(ts_part)
|
||||||
|
if file_dt.hour < 3: # Frühe Morgenstunden gehören noch zum Vortag
|
||||||
|
filtered.append(file)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
def _download_and_parse_file(self, file_url: str) -> List[Trade]:
|
||||||
|
"""Lädt eine JSON.gz Datei herunter und parst die Trades"""
|
||||||
|
trades = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Vollständige URL erstellen
|
||||||
|
if not file_url.startswith('http'):
|
||||||
|
full_url = f"{self.base_url.rstrip('/')}/{file_url.lstrip('/')}"
|
||||||
|
else:
|
||||||
|
full_url = file_url
|
||||||
|
|
||||||
|
response = requests.get(full_url, headers=HEADERS, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Gzip entpacken
|
||||||
|
with gzip.GzipFile(fileobj=io.BytesIO(response.content)) as f:
|
||||||
|
json_data = json.load(f)
|
||||||
|
|
||||||
|
# Trades parsen
|
||||||
|
# Deutsche Börse JSON Format (RTS1/RTS2):
|
||||||
|
# Typische Felder: TrdDt, TrdTm, ISIN, Pric, Qty, TrdCcy, etc.
|
||||||
|
for record in json_data:
|
||||||
|
try:
|
||||||
|
trade = self._parse_trade_record(record)
|
||||||
|
if trade:
|
||||||
|
trades.append(trade)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing trade record: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error downloading/parsing {file_url}: {e}")
|
||||||
|
|
||||||
|
return trades
|
||||||
|
|
||||||
|
def _parse_trade_record(self, record: dict) -> Optional[Trade]:
|
||||||
|
"""
|
||||||
|
Parst einen einzelnen Trade-Record aus dem JSON.
|
||||||
|
Deutsche Börse verwendet RTS1/RTS2 Format.
|
||||||
|
|
||||||
|
Wichtige Felder:
|
||||||
|
- TrdDt: Trading Date (YYYY-MM-DD)
|
||||||
|
- TrdTm: Trading Time (HH:MM:SS.ffffff)
|
||||||
|
- ISIN: Instrument Identifier
|
||||||
|
- FinInstrmId.Id: Alternative ISIN Feld
|
||||||
|
- Pric.Pric.MntryVal.Amt: Preis
|
||||||
|
- Qty.Unit: Menge
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# ISIN extrahieren
|
||||||
|
isin = record.get('ISIN') or record.get('FinInstrmId', {}).get('Id', '')
|
||||||
|
if not isin:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Preis extrahieren (verschiedene mögliche Pfade)
|
||||||
|
price = None
|
||||||
|
if 'Pric' in record:
|
||||||
|
pric = record['Pric']
|
||||||
|
if isinstance(pric, dict):
|
||||||
|
if 'Pric' in pric:
|
||||||
|
inner = pric['Pric']
|
||||||
|
if 'MntryVal' in inner:
|
||||||
|
price = float(inner['MntryVal'].get('Amt', 0))
|
||||||
|
elif 'Amt' in inner:
|
||||||
|
price = float(inner['Amt'])
|
||||||
|
elif 'MntryVal' in pric:
|
||||||
|
price = float(pric['MntryVal'].get('Amt', 0))
|
||||||
|
elif isinstance(pric, (int, float)):
|
||||||
|
price = float(pric)
|
||||||
|
|
||||||
|
if price is None or price <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Menge extrahieren
|
||||||
|
quantity = None
|
||||||
|
if 'Qty' in record:
|
||||||
|
qty = record['Qty']
|
||||||
|
if isinstance(qty, dict):
|
||||||
|
quantity = float(qty.get('Unit', qty.get('Qty', 0)))
|
||||||
|
elif isinstance(qty, (int, float)):
|
||||||
|
quantity = float(qty)
|
||||||
|
|
||||||
|
if quantity is None or quantity <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Timestamp extrahieren
|
||||||
|
trd_dt = record.get('TrdDt', '')
|
||||||
|
trd_tm = record.get('TrdTm', '00:00:00')
|
||||||
|
|
||||||
|
if not trd_dt:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Kombiniere Datum und Zeit
|
||||||
|
ts_str = f"{trd_dt}T{trd_tm}"
|
||||||
|
# Entferne Mikrosekunden wenn zu lang
|
||||||
|
if '.' in ts_str:
|
||||||
|
parts = ts_str.split('.')
|
||||||
|
if len(parts[1]) > 6:
|
||||||
|
ts_str = parts[0] + '.' + parts[1][:6]
|
||||||
|
|
||||||
|
# Parse als UTC (Deutsche Börse liefert UTC)
|
||||||
|
timestamp = datetime.fromisoformat(ts_str)
|
||||||
|
if timestamp.tzinfo is None:
|
||||||
|
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
return Trade(
|
||||||
|
exchange=self.name,
|
||||||
|
symbol=isin, # Symbol = ISIN
|
||||||
|
isin=isin,
|
||||||
|
price=price,
|
||||||
|
quantity=quantity,
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing record: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def fetch_latest_trades(self, include_yesterday: bool = True, since_date: datetime = None) -> List[Trade]:
|
||||||
|
"""
|
||||||
|
Holt alle Trades vom Vortag (oder seit since_date).
|
||||||
|
"""
|
||||||
|
all_trades = []
|
||||||
|
|
||||||
|
# Bestimme Zieldatum
|
||||||
|
if since_date:
|
||||||
|
target_date = since_date.date() if hasattr(since_date, 'date') else since_date
|
||||||
|
else:
|
||||||
|
# Standard: Vortag
|
||||||
|
target_date = (datetime.now(timezone.utc) - timedelta(days=1)).date()
|
||||||
|
|
||||||
|
print(f"[{self.name}] Fetching trades for date: {target_date}")
|
||||||
|
|
||||||
|
# Dateiliste holen
|
||||||
|
files = self._get_file_list()
|
||||||
|
print(f"[{self.name}] Found {len(files)} total files")
|
||||||
|
|
||||||
|
# Dateien für Zieldatum filtern
|
||||||
|
target_files = self._filter_files_for_date(files, target_date)
|
||||||
|
print(f"[{self.name}] {len(target_files)} files match target date")
|
||||||
|
|
||||||
|
# Alle passenden Dateien herunterladen und parsen
|
||||||
|
for file in target_files:
|
||||||
|
trades = self._download_and_parse_file(file)
|
||||||
|
all_trades.extend(trades)
|
||||||
|
print(f"[{self.name}] Parsed {len(trades)} trades from {file}")
|
||||||
|
|
||||||
|
print(f"[{self.name}] Total trades fetched: {len(all_trades)}")
|
||||||
|
return all_trades
|
||||||
|
|
||||||
|
|
||||||
|
class XetraExchange(DeutscheBoerseBase):
|
||||||
|
"""Xetra (Deutsche Börse) - DETR"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self) -> str:
|
||||||
|
return "https://mfs.deutsche-boerse.com/DETR-posttrade"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "XETRA"
|
||||||
|
|
||||||
|
|
||||||
|
class FrankfurtExchange(DeutscheBoerseBase):
|
||||||
|
"""Börse Frankfurt - DFRA"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self) -> str:
|
||||||
|
return "https://mfs.deutsche-boerse.com/DFRA-posttrade"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "FRA"
|
||||||
|
|
||||||
|
|
||||||
|
class QuotrixExchange(DeutscheBoerseBase):
|
||||||
|
"""Quotrix (Düsseldorf/Tradegate) - DGAT"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self) -> str:
|
||||||
|
return "https://mfs.deutsche-boerse.com/DGAT-posttrade"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "QUOTRIX"
|
||||||
229
src/exchanges/gettex.py
Normal file
229
src/exchanges/gettex.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import requests
|
||||||
|
import gzip
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import List, Optional
|
||||||
|
from .base import BaseExchange, Trade
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
# Browser User-Agent für Zugriff (gettex prüft User-Agent!)
|
||||||
|
HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8',
|
||||||
|
'Referer': 'https://www.gettex.de/'
|
||||||
|
}
|
||||||
|
|
||||||
|
# gettex Download-Basis-URLs
|
||||||
|
GETTEX_PAGE_URL = "https://www.gettex.de/handel/delayed-data/posttrade-data/"
|
||||||
|
GETTEX_DOWNLOAD_BASE = "https://erdk.bayerische-boerse.de:8000/delayed-data/MUNC-MUND/posttrade/"
|
||||||
|
|
||||||
|
|
||||||
|
class GettexExchange(BaseExchange):
|
||||||
|
"""
|
||||||
|
gettex Exchange (Bayerische Börse)
|
||||||
|
Kombiniert MUNC und MUND Daten.
|
||||||
|
|
||||||
|
Dateiformat: posttrade.YYYYMMDD.HH.mm.{munc|mund}.csv.gz
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "GETTEX"
|
||||||
|
|
||||||
|
def _get_file_list_from_page(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Parst die gettex Seite und extrahiert Download-Links.
|
||||||
|
"""
|
||||||
|
files = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(GETTEX_PAGE_URL, headers=HEADERS, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Suche nach Links zu CSV.gz Dateien
|
||||||
|
for link in soup.find_all('a'):
|
||||||
|
href = link.get('href', '')
|
||||||
|
if href and 'posttrade' in href.lower() and href.endswith('.csv.gz'):
|
||||||
|
files.append(href)
|
||||||
|
|
||||||
|
# Falls keine Links gefunden, versuche alternative Struktur
|
||||||
|
if not files:
|
||||||
|
# Manchmal sind Links in data-Attributen versteckt
|
||||||
|
for elem in soup.find_all(attrs={'data-href': True}):
|
||||||
|
href = elem.get('data-href', '')
|
||||||
|
if 'posttrade' in href.lower() and href.endswith('.csv.gz'):
|
||||||
|
files.append(href)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[GETTEX] Error fetching page: {e}")
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
def _generate_expected_files(self, target_date: datetime.date) -> List[str]:
|
||||||
|
"""
|
||||||
|
Generiert erwartete Dateinamen basierend auf dem Datum.
|
||||||
|
gettex veröffentlicht Dateien alle 15 Minuten während des Handels.
|
||||||
|
|
||||||
|
Dateiformat: posttrade.YYYYMMDD.HH.mm.{munc|mund}.csv.gz
|
||||||
|
"""
|
||||||
|
files = []
|
||||||
|
date_str = target_date.strftime('%Y%m%d')
|
||||||
|
|
||||||
|
# Handelszeiten: ca. 08:00 - 22:00 MEZ
|
||||||
|
# In UTC: 07:00 - 21:00 (Winter) / 06:00 - 20:00 (Sommer)
|
||||||
|
# Generiere für alle 15-Minuten-Intervalle
|
||||||
|
|
||||||
|
for hour in range(6, 23): # 06:00 - 22:45 UTC (abdeckend)
|
||||||
|
for minute in [0, 15, 30, 45]:
|
||||||
|
time_str = f"{hour:02d}.{minute:02d}"
|
||||||
|
files.append(f"posttrade.{date_str}.{time_str}.munc.csv.gz")
|
||||||
|
files.append(f"posttrade.{date_str}.{time_str}.mund.csv.gz")
|
||||||
|
|
||||||
|
# Auch frühe Dateien vom Folgetag (nach Mitternacht UTC)
|
||||||
|
next_date = target_date + timedelta(days=1)
|
||||||
|
next_date_str = next_date.strftime('%Y%m%d')
|
||||||
|
for hour in range(0, 3): # 00:00 - 02:45 UTC
|
||||||
|
for minute in [0, 15, 30, 45]:
|
||||||
|
time_str = f"{hour:02d}.{minute:02d}"
|
||||||
|
files.append(f"posttrade.{next_date_str}.{time_str}.munc.csv.gz")
|
||||||
|
files.append(f"posttrade.{next_date_str}.{time_str}.mund.csv.gz")
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
def _download_and_parse_file(self, filename: str) -> List[Trade]:
|
||||||
|
"""Lädt eine CSV.gz Datei und parst die Trades"""
|
||||||
|
trades = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Vollständige URL
|
||||||
|
url = f"{GETTEX_DOWNLOAD_BASE}{filename}"
|
||||||
|
|
||||||
|
response = requests.get(url, headers=HEADERS, timeout=60)
|
||||||
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
# Datei existiert nicht - normal für Zeiten ohne Handel
|
||||||
|
return []
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Gzip entpacken
|
||||||
|
with gzip.GzipFile(fileobj=io.BytesIO(response.content)) as f:
|
||||||
|
csv_text = f.read().decode('utf-8')
|
||||||
|
|
||||||
|
# CSV parsen
|
||||||
|
reader = csv.DictReader(io.StringIO(csv_text), delimiter=';')
|
||||||
|
|
||||||
|
for row in reader:
|
||||||
|
try:
|
||||||
|
trade = self._parse_csv_row(row)
|
||||||
|
if trade:
|
||||||
|
trades.append(trade)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[GETTEX] Error parsing row: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if e.response.status_code != 404:
|
||||||
|
print(f"[GETTEX] HTTP error downloading {filename}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[GETTEX] Error downloading {filename}: {e}")
|
||||||
|
|
||||||
|
return trades
|
||||||
|
|
||||||
|
def _parse_csv_row(self, row: dict) -> Optional[Trade]:
|
||||||
|
"""
|
||||||
|
Parst eine CSV-Zeile zu einem Trade.
|
||||||
|
|
||||||
|
Erwartete Spalten (RTS Format):
|
||||||
|
- TrdDtTm: Trading Date/Time
|
||||||
|
- ISIN: Instrument Identifier
|
||||||
|
- Pric: Preis
|
||||||
|
- Qty: Menge
|
||||||
|
- Ccy: Währung
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# ISIN
|
||||||
|
isin = row.get('ISIN', row.get('FinInstrmId', ''))
|
||||||
|
if not isin:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Preis
|
||||||
|
price_str = row.get('Pric', row.get('Price', '0'))
|
||||||
|
price_str = price_str.replace(',', '.')
|
||||||
|
price = float(price_str)
|
||||||
|
|
||||||
|
if price <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Menge
|
||||||
|
qty_str = row.get('Qty', row.get('Quantity', '0'))
|
||||||
|
qty_str = qty_str.replace(',', '.')
|
||||||
|
quantity = float(qty_str)
|
||||||
|
|
||||||
|
if quantity <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Timestamp
|
||||||
|
ts_str = row.get('TrdDtTm', row.get('TradingDateTime', ''))
|
||||||
|
if not ts_str:
|
||||||
|
# Fallback: Separate Felder
|
||||||
|
trd_dt = row.get('TrdDt', '')
|
||||||
|
trd_tm = row.get('TrdTm', '00:00:00')
|
||||||
|
ts_str = f"{trd_dt}T{trd_tm}"
|
||||||
|
|
||||||
|
# Parse Timestamp (UTC)
|
||||||
|
ts_str = ts_str.replace('Z', '+00:00')
|
||||||
|
if 'T' not in ts_str:
|
||||||
|
ts_str = ts_str.replace(' ', 'T')
|
||||||
|
|
||||||
|
timestamp = datetime.fromisoformat(ts_str)
|
||||||
|
if timestamp.tzinfo is None:
|
||||||
|
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
return Trade(
|
||||||
|
exchange=self.name,
|
||||||
|
symbol=isin,
|
||||||
|
isin=isin,
|
||||||
|
price=price,
|
||||||
|
quantity=quantity,
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[GETTEX] Error parsing CSV row: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def fetch_latest_trades(self, include_yesterday: bool = True, since_date: datetime = None) -> List[Trade]:
|
||||||
|
"""
|
||||||
|
Holt alle Trades vom Vortag.
|
||||||
|
"""
|
||||||
|
all_trades = []
|
||||||
|
|
||||||
|
# Zieldatum bestimmen
|
||||||
|
if since_date:
|
||||||
|
target_date = since_date.date() if hasattr(since_date, 'date') else since_date
|
||||||
|
else:
|
||||||
|
target_date = (datetime.now(timezone.utc) - timedelta(days=1)).date()
|
||||||
|
|
||||||
|
print(f"[{self.name}] Fetching trades for date: {target_date}")
|
||||||
|
|
||||||
|
# Generiere erwartete Dateinamen
|
||||||
|
expected_files = self._generate_expected_files(target_date)
|
||||||
|
print(f"[{self.name}] Trying {len(expected_files)} potential files")
|
||||||
|
|
||||||
|
# Versuche Dateien herunterzuladen
|
||||||
|
successful_files = 0
|
||||||
|
for filename in expected_files:
|
||||||
|
trades = self._download_and_parse_file(filename)
|
||||||
|
if trades:
|
||||||
|
all_trades.extend(trades)
|
||||||
|
successful_files += 1
|
||||||
|
|
||||||
|
print(f"[{self.name}] Successfully downloaded {successful_files} files")
|
||||||
|
print(f"[{self.name}] Total trades fetched: {len(all_trades)}")
|
||||||
|
|
||||||
|
return all_trades
|
||||||
366
src/exchanges/stuttgart.py
Normal file
366
src/exchanges/stuttgart.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import requests
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import List, Optional
|
||||||
|
from .base import BaseExchange, Trade
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
# Browser User-Agent
|
||||||
|
HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8',
|
||||||
|
'Referer': 'https://www.boerse-stuttgart.de/'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Börse Stuttgart URLs
|
||||||
|
STUTTGART_PAGE_URL = "https://www.boerse-stuttgart.de/de-de/fuer-geschaeftspartner/reports/mifir-ii-delayed-data/xstf-post-trade/"
|
||||||
|
|
||||||
|
|
||||||
|
class StuttgartExchange(BaseExchange):
|
||||||
|
"""
|
||||||
|
Börse Stuttgart (XSTF)
|
||||||
|
MiFIR II Delayed Data Post-Trade
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "STU"
|
||||||
|
|
||||||
|
def _get_download_links(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Parst die Börse Stuttgart Seite und extrahiert Download-Links.
|
||||||
|
"""
|
||||||
|
files = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(STUTTGART_PAGE_URL, headers=HEADERS, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Suche nach Download-Links
|
||||||
|
# Börse Stuttgart verwendet oft bestimmte CSS-Klassen oder data-Attribute
|
||||||
|
for link in soup.find_all('a'):
|
||||||
|
href = link.get('href', '')
|
||||||
|
|
||||||
|
# Prüfe auf typische Dateiendungen
|
||||||
|
if href and ('posttrade' in href.lower() or 'post-trade' in href.lower()):
|
||||||
|
if href.endswith('.gz') or href.endswith('.json') or href.endswith('.csv'):
|
||||||
|
# Vollständige URL erstellen
|
||||||
|
if not href.startswith('http'):
|
||||||
|
if href.startswith('/'):
|
||||||
|
href = f"https://www.boerse-stuttgart.de{href}"
|
||||||
|
else:
|
||||||
|
href = f"https://www.boerse-stuttgart.de/{href}"
|
||||||
|
files.append(href)
|
||||||
|
|
||||||
|
# Alternative: Suche nach JavaScript-generierten Links
|
||||||
|
if not files:
|
||||||
|
# Manchmal sind Links in Script-Tags versteckt
|
||||||
|
for script in soup.find_all('script'):
|
||||||
|
script_text = script.string or ''
|
||||||
|
if 'posttrade' in script_text.lower():
|
||||||
|
# Versuche URLs zu extrahieren
|
||||||
|
import re
|
||||||
|
urls = re.findall(r'https?://[^\s\'"<>]+posttrade[^\s\'"<>]+\.(?:gz|json|csv)', script_text, re.IGNORECASE)
|
||||||
|
files.extend(urls)
|
||||||
|
|
||||||
|
# Fallback: Versuche bekannte URL-Muster
|
||||||
|
if not files:
|
||||||
|
files = self._generate_expected_urls()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[STU] Error fetching page: {e}")
|
||||||
|
files = self._generate_expected_urls()
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
def _generate_expected_urls(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Generiert erwartete Download-URLs basierend auf bekannten Mustern.
|
||||||
|
Börse Stuttgart verwendet typischerweise ähnliche Formate wie andere Deutsche Börsen.
|
||||||
|
"""
|
||||||
|
files = []
|
||||||
|
|
||||||
|
# Versuche verschiedene URL-Muster
|
||||||
|
base_patterns = [
|
||||||
|
"https://www.boerse-stuttgart.de/api/v1/delayed-data/xstf-post-trade/",
|
||||||
|
"https://www.boerse-stuttgart.de/downloads/delayed-data/",
|
||||||
|
"https://mfs.boerse-stuttgart.de/XSTF-posttrade/",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Für die letzten 3 Tage
|
||||||
|
for days_ago in range(1, 4):
|
||||||
|
target_date = datetime.now(timezone.utc) - timedelta(days=days_ago)
|
||||||
|
date_str = target_date.strftime('%Y-%m-%d')
|
||||||
|
date_str_compact = target_date.strftime('%Y%m%d')
|
||||||
|
|
||||||
|
for base in base_patterns:
|
||||||
|
files.append(f"{base}posttrade-{date_str}.json.gz")
|
||||||
|
files.append(f"{base}posttrade.{date_str_compact}.json.gz")
|
||||||
|
files.append(f"{base}xstf-posttrade-{date_str}.json.gz")
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
def _filter_files_for_date(self, files: List[str], target_date: datetime.date) -> List[str]:
|
||||||
|
"""Filtert Dateien für ein bestimmtes Datum"""
|
||||||
|
filtered = []
|
||||||
|
target_str = target_date.strftime('%Y-%m-%d')
|
||||||
|
target_str_compact = target_date.strftime('%Y%m%d')
|
||||||
|
|
||||||
|
# Auch Dateien vom Folgetag (frühe Morgenstunden)
|
||||||
|
next_day = target_date + timedelta(days=1)
|
||||||
|
next_day_str = next_day.strftime('%Y-%m-%d')
|
||||||
|
next_day_compact = next_day.strftime('%Y%m%d')
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
file_lower = file.lower()
|
||||||
|
if target_str in file_lower or target_str_compact in file_lower:
|
||||||
|
filtered.append(file)
|
||||||
|
elif next_day_str in file_lower or next_day_compact in file_lower:
|
||||||
|
# Prüfe ob frühe Morgenstunde
|
||||||
|
if 'T00' in file or 'T01' in file or 'T02' in file:
|
||||||
|
filtered.append(file)
|
||||||
|
# Für kompakte Formate
|
||||||
|
elif '.00.' in file or '.01.' in file or '.02.' in file:
|
||||||
|
filtered.append(file)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
def _download_and_parse_file(self, url: str) -> List[Trade]:
|
||||||
|
"""Lädt eine Datei herunter und parst die Trades"""
|
||||||
|
trades = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=HEADERS, timeout=60)
|
||||||
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
return []
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
content = response.content
|
||||||
|
|
||||||
|
# Prüfe ob Gzip
|
||||||
|
if url.endswith('.gz'):
|
||||||
|
try:
|
||||||
|
with gzip.GzipFile(fileobj=io.BytesIO(content)) as f:
|
||||||
|
content = f.read()
|
||||||
|
except Exception:
|
||||||
|
pass # Vielleicht nicht wirklich gzip
|
||||||
|
|
||||||
|
# Versuche als JSON zu parsen
|
||||||
|
if url.endswith('.json') or url.endswith('.json.gz'):
|
||||||
|
try:
|
||||||
|
data = json.loads(content)
|
||||||
|
if isinstance(data, list):
|
||||||
|
for record in data:
|
||||||
|
trade = self._parse_json_record(record)
|
||||||
|
if trade:
|
||||||
|
trades.append(trade)
|
||||||
|
return trades
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Versuche als CSV zu parsen
|
||||||
|
try:
|
||||||
|
text = content.decode('utf-8') if isinstance(content, bytes) else content
|
||||||
|
reader = csv.DictReader(io.StringIO(text), delimiter=';')
|
||||||
|
for row in reader:
|
||||||
|
trade = self._parse_csv_row(row)
|
||||||
|
if trade:
|
||||||
|
trades.append(trade)
|
||||||
|
except Exception:
|
||||||
|
# Versuche mit Komma als Delimiter
|
||||||
|
try:
|
||||||
|
text = content.decode('utf-8') if isinstance(content, bytes) else content
|
||||||
|
reader = csv.DictReader(io.StringIO(text), delimiter=',')
|
||||||
|
for row in reader:
|
||||||
|
trade = self._parse_csv_row(row)
|
||||||
|
if trade:
|
||||||
|
trades.append(trade)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[STU] Could not parse {url}: {e}")
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if e.response.status_code != 404:
|
||||||
|
print(f"[STU] HTTP error downloading {url}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[STU] Error downloading {url}: {e}")
|
||||||
|
|
||||||
|
return trades
|
||||||
|
|
||||||
|
def _parse_json_record(self, record: dict) -> Optional[Trade]:
|
||||||
|
"""Parst einen JSON-Record zu einem Trade"""
|
||||||
|
try:
|
||||||
|
# ISIN
|
||||||
|
isin = record.get('ISIN') or record.get('FinInstrmId', {}).get('Id', '')
|
||||||
|
if not isin:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Preis (verschiedene mögliche Strukturen)
|
||||||
|
price = None
|
||||||
|
if 'Pric' in record:
|
||||||
|
pric = record['Pric']
|
||||||
|
if isinstance(pric, dict):
|
||||||
|
if 'Pric' in pric:
|
||||||
|
inner = pric['Pric']
|
||||||
|
if isinstance(inner, dict):
|
||||||
|
price = float(inner.get('MntryVal', {}).get('Amt', 0) or inner.get('Amt', 0))
|
||||||
|
else:
|
||||||
|
price = float(inner)
|
||||||
|
elif 'MntryVal' in pric:
|
||||||
|
price = float(pric['MntryVal'].get('Amt', 0))
|
||||||
|
elif 'Amt' in pric:
|
||||||
|
price = float(pric['Amt'])
|
||||||
|
else:
|
||||||
|
price = float(pric)
|
||||||
|
elif 'Price' in record:
|
||||||
|
price = float(str(record['Price']).replace(',', '.'))
|
||||||
|
|
||||||
|
if not price or price <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Menge
|
||||||
|
quantity = None
|
||||||
|
if 'Qty' in record:
|
||||||
|
qty = record['Qty']
|
||||||
|
if isinstance(qty, dict):
|
||||||
|
quantity = float(qty.get('Unit', qty.get('Qty', 0)))
|
||||||
|
else:
|
||||||
|
quantity = float(qty)
|
||||||
|
elif 'Quantity' in record:
|
||||||
|
quantity = float(str(record['Quantity']).replace(',', '.'))
|
||||||
|
|
||||||
|
if not quantity or quantity <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Timestamp
|
||||||
|
ts_str = record.get('TrdDtTm', '')
|
||||||
|
if not ts_str:
|
||||||
|
trd_dt = record.get('TrdDt', '')
|
||||||
|
trd_tm = record.get('TrdTm', '00:00:00')
|
||||||
|
if trd_dt:
|
||||||
|
ts_str = f"{trd_dt}T{trd_tm}"
|
||||||
|
|
||||||
|
if not ts_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ts_str = ts_str.replace('Z', '+00:00')
|
||||||
|
timestamp = datetime.fromisoformat(ts_str)
|
||||||
|
if timestamp.tzinfo is None:
|
||||||
|
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
return Trade(
|
||||||
|
exchange=self.name,
|
||||||
|
symbol=isin,
|
||||||
|
isin=isin,
|
||||||
|
price=price,
|
||||||
|
quantity=quantity,
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[STU] Error parsing JSON record: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_csv_row(self, row: dict) -> Optional[Trade]:
|
||||||
|
"""Parst eine CSV-Zeile zu einem Trade"""
|
||||||
|
try:
|
||||||
|
# ISIN
|
||||||
|
isin = row.get('ISIN', row.get('FinInstrmId', ''))
|
||||||
|
if not isin:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Preis
|
||||||
|
price_str = row.get('Pric', row.get('Price', '0'))
|
||||||
|
price_str = str(price_str).replace(',', '.')
|
||||||
|
price = float(price_str)
|
||||||
|
|
||||||
|
if price <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Menge
|
||||||
|
qty_str = row.get('Qty', row.get('Quantity', '0'))
|
||||||
|
qty_str = str(qty_str).replace(',', '.')
|
||||||
|
quantity = float(qty_str)
|
||||||
|
|
||||||
|
if quantity <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Timestamp
|
||||||
|
ts_str = row.get('TrdDtTm', row.get('TradingDateTime', ''))
|
||||||
|
if not ts_str:
|
||||||
|
trd_dt = row.get('TrdDt', '')
|
||||||
|
trd_tm = row.get('TrdTm', '00:00:00')
|
||||||
|
if trd_dt:
|
||||||
|
ts_str = f"{trd_dt}T{trd_tm}"
|
||||||
|
|
||||||
|
if not ts_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ts_str = ts_str.replace('Z', '+00:00')
|
||||||
|
if 'T' not in ts_str:
|
||||||
|
ts_str = ts_str.replace(' ', 'T')
|
||||||
|
|
||||||
|
timestamp = datetime.fromisoformat(ts_str)
|
||||||
|
if timestamp.tzinfo is None:
|
||||||
|
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
return Trade(
|
||||||
|
exchange=self.name,
|
||||||
|
symbol=isin,
|
||||||
|
isin=isin,
|
||||||
|
price=price,
|
||||||
|
quantity=quantity,
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[STU] Error parsing CSV row: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def fetch_latest_trades(self, include_yesterday: bool = True, since_date: datetime = None) -> List[Trade]:
|
||||||
|
"""
|
||||||
|
Holt alle Trades vom Vortag.
|
||||||
|
"""
|
||||||
|
all_trades = []
|
||||||
|
|
||||||
|
# Zieldatum bestimmen
|
||||||
|
if since_date:
|
||||||
|
target_date = since_date.date() if hasattr(since_date, 'date') else since_date
|
||||||
|
else:
|
||||||
|
target_date = (datetime.now(timezone.utc) - timedelta(days=1)).date()
|
||||||
|
|
||||||
|
print(f"[{self.name}] Fetching trades for date: {target_date}")
|
||||||
|
|
||||||
|
# Download-Links holen
|
||||||
|
all_links = self._get_download_links()
|
||||||
|
print(f"[{self.name}] Found {len(all_links)} potential download links")
|
||||||
|
|
||||||
|
# Nach Datum filtern
|
||||||
|
target_links = self._filter_files_for_date(all_links, target_date)
|
||||||
|
|
||||||
|
if not target_links:
|
||||||
|
# Fallback: Versuche alle Links
|
||||||
|
target_links = all_links
|
||||||
|
|
||||||
|
print(f"[{self.name}] Trying {len(target_links)} files for target date")
|
||||||
|
|
||||||
|
# Dateien herunterladen und parsen
|
||||||
|
successful = 0
|
||||||
|
for url in target_links:
|
||||||
|
trades = self._download_and_parse_file(url)
|
||||||
|
if trades:
|
||||||
|
all_trades.extend(trades)
|
||||||
|
successful += 1
|
||||||
|
print(f"[{self.name}] Parsed {len(trades)} trades from {url}")
|
||||||
|
|
||||||
|
print(f"[{self.name}] Successfully processed {successful} files")
|
||||||
|
print(f"[{self.name}] Total trades fetched: {len(all_trades)}")
|
||||||
|
|
||||||
|
return all_trades
|
||||||
Reference in New Issue
Block a user