From 22b09669c10e6c75cfe0f11020e094a5c06175db Mon Sep 17 00:00:00 2001 From: Melchior Reimers Date: Tue, 27 Jan 2026 10:48:11 +0100 Subject: [PATCH] updated dashboard --- __pycache__/daemon.cpython-313.pyc | Bin 0 -> 10267 bytes cleanup_duplicates.py | 154 ++++++++++++++++++ daemon.py | 65 +++++++- .../deutsche_boerse.cpython-313.pyc | Bin 15752 -> 16447 bytes .../__pycache__/gettex.cpython-313.pyc | Bin 17082 -> 17884 bytes .../__pycache__/stuttgart.cpython-313.pyc | Bin 16409 -> 17214 bytes src/exchanges/deutsche_boerse.py | 67 +++++--- src/exchanges/gettex.py | 23 ++- src/exchanges/stuttgart.py | 23 ++- 9 files changed, 298 insertions(+), 34 deletions(-) create mode 100644 __pycache__/daemon.cpython-313.pyc create mode 100644 cleanup_duplicates.py diff --git a/__pycache__/daemon.cpython-313.pyc b/__pycache__/daemon.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..083d85684838307cfe7a0d08398948eb771d8462 GIT binary patch literal 10267 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 literal 0 HcmV?d00001 diff --git a/cleanup_duplicates.py b/cleanup_duplicates.py new file mode 100644 index 0000000..96a763f --- /dev/null +++ b/cleanup_duplicates.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +Script zum Entfernen von duplizierten Trades aus QuestDB. +Erstellt eine neue Tabelle ohne Duplikate und ersetzt die alte. +""" + +import requests +import os +import sys + +DB_HOST = os.getenv("QUESTDB_HOST", "localhost") +DB_PORT = os.getenv("QUESTDB_PORT", "9000") +DB_USER = os.getenv("DB_USER", "admin") +DB_PASSWORD = os.getenv("DB_PASSWORD", "quest") + +DB_URL = f"http://{DB_HOST}:{DB_PORT}" +DB_AUTH = (DB_USER, DB_PASSWORD) if DB_USER and DB_PASSWORD else None + +def execute_query(query, timeout=300): + """Führt eine QuestDB Query aus.""" + try: + response = requests.get( + f"{DB_URL}/exec", + params={'query': query}, + auth=DB_AUTH, + timeout=timeout + ) + if response.status_code == 200: + return response.json() + else: + print(f"Query failed: {response.text}") + return None + except Exception as e: + print(f"Error executing query: {e}") + return None + +def get_table_count(table_name): + """Zählt Einträge in einer Tabelle.""" + result = execute_query(f"SELECT count(*) FROM {table_name}") + if result and result.get('dataset'): + return result['dataset'][0][0] + return 0 + +def main(): + print("=" * 60) + print("QuestDB Duplikat-Bereinigung") + print("=" * 60) + + # 1. Prüfe aktuelle Anzahl + original_count = get_table_count("trades") + print(f"\n1. Aktuelle Anzahl Trades: {original_count:,}") + + if original_count == 0: + print("Keine Trades in der Datenbank. Nichts zu tun.") + return + + # 2. Analysiere Duplikate pro Exchange + print("\n2. Analysiere Duplikate pro Exchange...") + + analysis_query = """ + SELECT + exchange, + count(*) as total, + count(distinct concat(isin, '-', cast(timestamp as string), '-', cast(price as string), '-', cast(quantity as string))) as unique_trades + FROM trades + GROUP BY exchange + ORDER BY exchange + """ + + result = execute_query(analysis_query) + if result and result.get('dataset'): + print(f"\n{'Exchange':<15} {'Total':>12} {'Unique':>12} {'Duplicates':>12}") + print("-" * 55) + total_all = 0 + unique_all = 0 + for row in result['dataset']: + exchange, total, unique = row + duplicates = total - unique + total_all += total + unique_all += unique + print(f"{exchange:<15} {total:>12,} {unique:>12,} {duplicates:>12,}") + print("-" * 55) + print(f"{'TOTAL':<15} {total_all:>12,} {unique_all:>12,} {total_all - unique_all:>12,}") + + # 3. Erstelle bereinigte Tabelle + print("\n3. Erstelle bereinigte Tabelle 'trades_clean'...") + + # Lösche alte clean-Tabelle falls vorhanden + execute_query("DROP TABLE IF EXISTS trades_clean") + + # Erstelle neue Tabelle mit DISTINCT auf allen relevanten Feldern + # QuestDB: Wir erstellen eine neue Tabelle mit DISTINCT + create_clean_query = """ + CREATE TABLE trades_clean AS ( + SELECT DISTINCT + exchange, + symbol, + isin, + price, + quantity, + timestamp + FROM trades + ) TIMESTAMP(timestamp) PARTITION BY DAY WAL + """ + + result = execute_query(create_clean_query, timeout=600) + if result is None: + print("Fehler beim Erstellen der bereinigten Tabelle!") + return + + clean_count = get_table_count("trades_clean") + print(f" Bereinigte Tabelle erstellt: {clean_count:,} Trades") + + removed = original_count - clean_count + print(f" Entfernte Duplikate: {removed:,} ({removed/original_count*100:.1f}%)") + + # 4. Ersetze alte Tabelle + print("\n4. Ersetze alte Tabelle...") + + # Rename alte Tabelle zu backup + execute_query("RENAME TABLE trades TO trades_backup") + + # Rename neue Tabelle zu trades + execute_query("RENAME TABLE trades_clean TO trades") + + # Verifiziere + final_count = get_table_count("trades") + print(f" Neue Trades-Tabelle: {final_count:,} Einträge") + + # 5. Lösche Backup (optional) + print("\n5. Lösche Backup-Tabelle...") + execute_query("DROP TABLE IF EXISTS trades_backup") + print(" Backup gelöscht.") + + # 6. Zusammenfassung + print("\n" + "=" * 60) + print("ZUSAMMENFASSUNG") + print("=" * 60) + print(f"Vorher: {original_count:>15,} Trades") + print(f"Nachher: {final_count:>15,} Trades") + print(f"Entfernt:{removed:>15,} Duplikate ({removed/original_count*100:.1f}%)") + print("=" * 60) + + # 7. Statistik-Tabellen neu berechnen + print("\n6. Lösche alte Analytics-Tabellen (werden neu berechnet)...") + for table in ['analytics_daily_summary', 'analytics_exchange_daily', + 'analytics_stock_trends', 'analytics_volume_changes', 'analytics_custom']: + result = execute_query(f"DROP TABLE IF EXISTS {table}") + print(f" {table} gelöscht") + + print("\nFertig! Der Analytics Worker wird die Statistiken beim nächsten Start neu berechnen.") + +if __name__ == "__main__": + main() diff --git a/daemon.py b/daemon.py index 1aeaadb..1402edf 100644 --- a/daemon.py +++ b/daemon.py @@ -1,6 +1,7 @@ import time import logging import datetime +import hashlib import os import requests from src.exchanges.eix import EIXExchange @@ -20,6 +21,39 @@ DB_USER = os.getenv("DB_USER", "admin") DB_PASSWORD = os.getenv("DB_PASSWORD", "quest") DB_AUTH = (DB_USER, DB_PASSWORD) if DB_USER and DB_PASSWORD else None +def get_trade_hash(trade): + """Erstellt einen eindeutigen Hash für einen Trade.""" + key = f"{trade.exchange}|{trade.isin}|{trade.timestamp.isoformat()}|{trade.price}|{trade.quantity}" + return hashlib.md5(key.encode()).hexdigest() + +def get_existing_trade_hashes(db_url, exchange_name, since_date): + """Holt alle Trade-Hashes für eine Exchange seit einem bestimmten Datum.""" + hashes = set() + + # Hole alle Trades seit dem Datum + date_str = since_date.strftime('%Y-%m-%dT%H:%M:%S.000000Z') + query = f"SELECT exchange, isin, timestamp, price, quantity FROM trades WHERE exchange = '{exchange_name}' AND timestamp >= '{date_str}'" + + try: + response = requests.get(f"{db_url}/exec", params={'query': query}, auth=DB_AUTH, timeout=60) + if response.status_code == 200: + data = response.json() + if data.get('dataset'): + for row in data['dataset']: + exchange, isin, ts, price, qty = row + # Konvertiere Timestamp + if isinstance(ts, str): + ts_iso = ts.replace('Z', '+00:00') + else: + ts_iso = datetime.datetime.fromtimestamp(ts / 1000000, tz=datetime.timezone.utc).isoformat() + + key = f"{exchange}|{isin}|{ts_iso}|{price}|{qty}" + hashes.add(hashlib.md5(key.encode()).hexdigest()) + except Exception as e: + logger.warning(f"Could not fetch existing trade hashes: {e}") + + return hashes + def get_last_trade_timestamp(db_url, exchange_name): # QuestDB query: get the latest timestamp for a specific exchange query = f"trades where exchange = '{exchange_name}' latest by timestamp" @@ -79,7 +113,7 @@ def run_task(historical=False): db_url = "http://questdb:9000" last_ts = get_last_trade_timestamp(db_url, exchange.name) - logger.info(f"Fetching data from {exchange.name} (Filtering trades older than {last_ts})...") + logger.info(f"Fetching data from {exchange.name} (Last trade: {last_ts})...") # Special handling for EIX to support smart filtering call_args = args.copy() @@ -91,11 +125,30 @@ def run_task(historical=False): trades = exchange.fetch_latest_trades(**call_args) - # Deduplizierung: Nur Trades nehmen, die neuer sind als der letzte in der DB - new_trades = [ - t for t in trades - if t.timestamp.replace(tzinfo=datetime.timezone.utc) > last_ts.replace(tzinfo=datetime.timezone.utc) - ] + if not trades: + logger.info(f"No trades fetched from {exchange.name}.") + continue + + # Hash-basierte Deduplizierung + # Hole existierende Hashes für Trades ab dem ältesten neuen Trade + oldest_trade_ts = min(t.timestamp for t in trades) + + # Nur prüfen wenn wir nicht einen komplett historischen Sync machen + if last_ts > datetime.datetime.min.replace(tzinfo=datetime.timezone.utc): + # Hole Hashes der letzten 7 Tage für diese Exchange + check_since = oldest_trade_ts - datetime.timedelta(days=1) + existing_hashes = get_existing_trade_hashes(db_url, exchange.name, check_since) + logger.info(f"Found {len(existing_hashes)} existing trade hashes in DB") + + # Filtere nur wirklich neue Trades + new_trades = [] + for t in trades: + trade_hash = get_trade_hash(t) + if trade_hash not in existing_hashes: + new_trades.append(t) + else: + # Historischer Sync - keine Deduplizierung nötig + new_trades = trades logger.info(f"Found {len(trades)} total trades, {len(new_trades)} are new.") diff --git a/src/exchanges/__pycache__/deutsche_boerse.cpython-313.pyc b/src/exchanges/__pycache__/deutsche_boerse.cpython-313.pyc index 8cb1cc7cd5fdac96416a2d5d20a46f566274da63..c69c10345f92f2fd7f0dd8d63c99a78b978fbd20 100644 GIT binary patch delta 3345 zcmb7GdrVZ>89!&6>97#8&!ybM{8tR(&;Qz97USQCpHG(QX`i5ux6- z%S$Bc1UtQLC;$(?G;9XhwZ?4hq&3DetxL$FBgO*1TkzCj#U|P^I-~}K1g~hLmyHF+ zVxWD?v>Sn?&SLsY(^~&9(AJDZNQ2P6s48Wpl4-s1v?`_2?ze%?7OkKZNggKE8KjT& zZhv;he<6pZrUQ#s#jffPn%3suLdXJ<_2yCMkjqLywq?@Z@`UNBeU0A$2RH&-}4 z1*o?)!)8PhIIlRUg)1&}*wI#9#vqS2qqt@*5;TgV#Zk2UrXu%_+0@%ocA)=w)fD}j ztJ?HeQ$N2eIvKTNLb>DJR%>Rw2UUkBIPlO#ATAg5x=UUE%eh_{ue6BaiSHC~>i}iDdXZw;_ zt&7$j)aY#3Qo5%G{kAF#@Fu4s-|BvdL-Y(3?-=u#oEGTCxNfkiQo;E$s zHonE+3_wa3BjKTxKCCD*A>XF;MJ0ysu*1D{v?$LIV=No!ew~~CsK|}qnY&h0rLWr$ zyPq2wWHZ(fz@()t7L6xVcgx6Yu#?hOREAP`fUr-VbTSa>n$kc^H#^i5Ng0ue8nq5K zGKuM6eB}((6g|(Vod79QNJ_{9@dP0U>9NvN_%CyREA`;AlzEs$#|GHp5S00oht;eW zeqX%_@O>m=MI&nH!#oe2(wj;IT;BurZijl;^fET}5j29f4wrFImcU#DXBmMLf=(5% zJ|m!iS|Df`XZ~}$!b(U^@F7{@BXVLg0q-^mwj%L_G{TpBe0@+Ramc$elHgy84voqR zl$NXn%ej3~1z4IYHt|AKQORxG=X!JyvS0$u=`_C1s4jq@CS{zE<(EU!F`_0>o4(?! zaF)`soWxmhJkJv!+wu;K$ccf76i*DWivs(QxMQgjcE2Z9J7Uj; zv^dT-zR`H5d1n7z%bNS{>EUv3 zUQeic`IAzNtMGbvaJ}sNS0B-sF|AI;mDA7oWDdc{v6>#2b*nZb%_HzHmj&)x)n>Kr zzu2^|oVbERF^NVuY8!-a*(@UJ-@N9K!in4yS0J!`FtT8vJsHNCP+*r^;f_hrl-WRy zNi+%0`g(-aaSHQtwNSJ!$X|icTb(7EpT3WMq4da9wR{im6YQ?0A?RittOy~Io|QqZ z#;x^&y#=!e#;fauZ=7`st}Pl#@KPiq^IdB5Z0CL*Av;TlSxDri`OfT!OBc*2ju5rB$L6 zgtaF^8L+w~KV!g}G+}=z$UX+u37G?P%KSquCUB5H3Wt@IyT))@Mjz$-8|{IssJ(a5vvwrc3Bm7PsX2IMcAw%zvy?)fSf zd=;~~cYRIMmWLMKT}$zUrw3)#Z2xS>eAOqG`Ke@A6V(%KOSM+L=gQHaPhFi_LO{;v+|2(d|1l#Uk)a*JKblcClfojU+or$=S4{=(p{+FyD(#`)t;rebV;N(Y zcM?5x_|i(Q!}5iz9Qlw1^eYB87|b*Hh{3NJe9S-{m+BS&hAo+7S_f;-Z)zW zH^I5%@Xzgj?Ge=jKOL>hHHW|hatkFjqbFc3ocW=t}oIE8HV@nC@rn zCD|SWi5Aqi<0OsLAH!qxv-;D^_D(nW&9RmC=$!`hayGtb2-GH4n#=iL=I$|b48~}4 zV|zBEf*!J&?H*>JFo@8LjeCM>CRF!Sp{M;$nHtBUq49{^Ode8KQ?sK*j{`G#j}d!6 LZsX{|rqX`{FuojK delta 2762 zcmZuzYfM|$9Y4p9dyRel0KbhP7tA9zGo(Di8$u&UUzn7{#dEs9Yqe@X*7Sw5(f!Ro*44_O4tL5vtv2}#L6fgAsPV~z z1woyX*fcV}A_tgJ%s^!J8Ifg5?>4V~TYeM5BdY^S9zrWV&6FEN#?R=0Pu(CB*&&%v zgRe59rb94d03fi3=Az55@NsyYErVLTz}AtR@32;=$9Gt-+~6z3pRr~3BA>ApidsLn zH|qkz#{uw}{2YEtRmL79vNxgXebM3?~Y$CC_;cEOWW3*eEVAj=VrS%U%KPT$^ z@?kfAL1o0j0?zs(vDW6|M`QpTeTnA4b*gekQkQX!s*01ucZqG*caMDy01e=YzTWuDz=~aiuwn*Pt%}Iw*`81ySTR0pD&Uwc@iFh)VLg9#zRf7@Y9`J_7 ziBO*>jO2DgZP}w>?MyN*25YC%X$LC#?Y%9WE@>fv6gRwfg%Xez^m6A3|#WYqnK28V`w5BB(aGX=e8Cxk>Q8W-J# z_^({ruHQ}&+D?#xLb8x_j>o69W3UDPMmu^!4Vz z7eBC9{@z~mvAt%cXTx5*$ZVFlE+1a6m|AyCUt%9Ipm+=}vYQs$YcrQ-maH3=#*2p@ znryEdFCV|^+%Q#L?8V>JeI0J%K7A$p6L#rcZMKqY+!bzVZhx z?Qjm1%GYd=$ZMrag6QC?*8t^J-K05aKY@%qDyC2~ReK1yHONe5a>W2ThDVIc@Wkp9 z<2_}qsxkv&=Iz za2Nijs|0=vzv(Kgr6Exx0kPGpo(kxUSJ1#h|bx?ZJ+g9MT~ z0{j;ivd}|>yDMto_0`iAjmiZd+4+Q}V5Mi11kxJs&V+l>1+tTo@2ihwU>UaI#|4^-=x1 z22EZ8OpcuhuW|Xj=F^I-pD}{Nr${*}e14x5e`+Ye<5n(9wIveOmf&ZtrB!~`C-Z8F z9ZMu#TN?lp8xykMU0}aHFLyy+Zi8P%`nOXJ4~|B*`I|=k9{FmJ@6zsQm4!5)R}BEE zz9_%?-Ij7AX`fYHvEVkPo79Knu@oPU#RPtYawp-w_!)jQj#A+%es4S?AU-KXQ+zTi zP6#173+`HVMnMInB5x-RIK^kL7(W?DJT-0OQ6)9u_*6VaI8|a< zEN_^VeWJwahGPr*3_Bf85~fAyhcv33Kt?Xc=Meo8qVH3n^D5msR7@rE$^mMk5ToDworZR$Wl6E*TQ1lFM0}LS#+JE?$1OEV$B#xH0rL28T6(OXnp~fX zeSGT7XLOTn;kzy8VILl7ohJbk9VYy+)d~CYzgpWR-Hr#_j0;C;{j`JU!KU7V?6jU6 z1O~6G(R);RpTf^6{DQ(SDg27UO$riuNX@=QRqC0IAraOGRHe{Ivvd&Q0{DYs@1|1w zNCuS$;AI!yY^#SKU}O6^IE-Iye{Q={M+eXNi4VGs4|LQ?oBQB1c)7z^Ar*(#Xyjel zKB9XRhVg@r0r(Mibv_SI;x{_Ku^qL!tCR`nqI%TqG5^rWaQ_&h-Ld19E^~`O>&$h- zme-B{n{b0Bq;L}dqieuQCASIH(cKt@1O*X~cJ~{kPDlkxPB-v7-8T3Tf7sonM+)e; R?A#*L3mscC1=xJR^B>(ki>Uwr diff --git a/src/exchanges/__pycache__/gettex.cpython-313.pyc b/src/exchanges/__pycache__/gettex.cpython-313.pyc index 947d40263dc4f7a138047ae7e2c292b44a407f9e..577fd8f49b30597c79f01d57956e04e2d3aa0cc5 100644 GIT binary patch delta 1841 zcmb7EZERCj7(VB=y_DX5-F}XZjfzqDTQ)7ZcEsv zW=2CYLL$=>12IJXVTgZ>Ba=t~4MBgPiE+y?EDbUEPySVr#9zku+^w`Q65~nkdCxiT z^L{+%y!X9+2H!u2?T>6W3xZ)eJCXjg^NRgT4*x^H@ziq01iQwoELJ2!UG8bbYeGrZqUdAEFnn44gTY*m#qYk$|12;Vl z5Avsa!H?yYw3<}%Qc_W+jFO+qgFKjkqf9QJNJ;hI-5gc4+_;ujQ+esN?ATF7RY0o- z8!g>g6(H?R4HC(!`f{h`p$SbRq?y69*2SiMFg;{MgIrg5QX(e zwCstIWFoI<7hs=b{3J=zs+iI{l2Y;`nM5u>lGhSRNCJ?Pg}?zNpI0XKPmCQ+s41oK zDb#ccCSh>b4{qK@MGJCnm@z&O9OrkR-F6#QIVtlqZh-ZrzZXhu%Y zj8HToyX$=XZ2a8eizkTrgp z8}6B9qiT}TfP?myLE5=35|hHl*hv zz20>?%2-B$4=+?ojhC1C`V~`j04LT#1sDhX^l)QsE!f0tt17fwIj;}lE+wD=BwKWnXkhaPUe7dfvY;i(EvrAK>X03!>(to=0Am<A+Nz7 ztED@eTluO}`x*K1X5aAtY(09bD0i6xj!k)nC2~AHJ`O!XVtod>R8*QWN})Xx{j_-p zr(LCYn}5b0`q_?pe2w1OAvd}74y0X&B(xha6)cHNMsGAqPU~O?wyc&;rPV}6Z%}kk zc*+E&h$MP5+{-s&=r12pG1A{&!JD4h=;R&AL=l5g}5I>TvvGhot>|n_^T|h zZ|Lv40-W~MynS~k#&9NvB{)(+qO7gl=NVKN3R%^P!Z7JH5 zAQhp73Ul!0(TfL_h=?tMh@uB+Jt?&yJ@!_S-hD4&X_O4?%Y4KMP}sx=1{ofqy$=0Y=*+a-Fp#YCE|t76}nC!CPN+Rc@*;S<#2G z$C;K^fmE)_(wn3zm>H5TS=+2AJ4N-|vQbupB?!l(^`?;Jgsr9<0vuQ2UR?0Yb*?fL zuUU?4mC=#?wIh>&;nQV0c7k07{OXE^{~wJm(HgB;vUsRiAK{usTICy|l08U6b%sz+ zD!PyM4A%CnhqI}<2s}%q6QQL>VW?DG&AR$w9-gVkgv>Nmp-Q)ccJ0}7OJVxdB4_R#|nQ%iO~ds$75 zIyCyj&tHMvRHj@?2RZMDbRR#Q=Dwu4;qLO<*=xM#4P$yu-I5BKBMOK;UT%*sM|%1( z-hivUO?ayOuy>uqAs8H37Psw_rBc!S#Hu#n=fD~K5rTvFW4~u@{g`ifF}9Cb{CuMt owa_lIC}@G1TQi|2L!T^@%Xb%7 diff --git a/src/exchanges/__pycache__/stuttgart.cpython-313.pyc b/src/exchanges/__pycache__/stuttgart.cpython-313.pyc index c3bda68b731f9e0be5ff4362ea6ee4e349200306..7982c384be5c2cbc1867c3eb9f0de7c1e8d569b0 100644 GIT binary patch delta 1462 zcmb7DU2NM_6ux$BC$;m_{5fs9PF}UHjYg`VwOx}mohGeld+4UtGgKrcIE|NP^PFq?EZxLIpcedl@&R@q0fjPX zp-kRk6DNM>1Wy9Nt4(^=KjstoZ( zazROwU?<7?(>cvd>uS+Vn^4VS6&Siy?UT&~)6A*R9KSU?ry4nJ-~hPNjGUwZC{ALg z+n~mS_RbRXNO0X9T^+qMwmw-Aj_-K`pNGD5?*vcpcu$uu)NG*LR}yM0aC+{hR#SIg zTYsbCkdC1CT;7rZk73r!Rd?Q))fci_dPysq8pPtz`@=od7pOeEYf~75Rlt-Bod|k+ znIWu&n88es#}@G)dK`eH`@a|fGJ}yGv=R09u?p)1GN&+3aE5DCNiDH{YaMmKEdk8p zM_XsR+*~Vei!B*IZ;$j`z%Vl4BQ~8KdiGHoR>j)?EStqKg)^+J2+#7#K!ZfFHpF82 z!Wp*lCIQ)Q*c7W5TznN@zYX{9A|pY6kF<3dyuvp(n&`&OEK}X}neopl6q`(k9k|@q zO2&CdlZ=BZEW@d^9iHMu3IGALoTk{L*lYl~;AWz5-eiUL^rX0p@dgL)GhBhNj{Xv?==0$MHCqChPGP+Ko>p zY8>c1S#s=&!Cf)3BSy+y6){l~9tmPa==;s?UOT^*{UrZkel58z4VGuhS2hP93Y%|k zdz4@7FW>7QjZ#zx$|Q2s7Wyf1$^q|_)=Z{Q(BaSMX<~%>9lbdA%}{Ngva`N&|JUJ- z@E-s_?jIXqgXOC=fRBfB^&iC>F|^$A)1W8_-maXwk`aR!V%qTzTxFY{Cp4Q49=u9FrGV zi7AD0z--Us4CO>N%~uv^B6E=R4UF;YZI07x;M)D3P%s`+xPbdd4 zSlD193Jkt63=G^13Je&gC<9gVhOz@mKI9<4PzUl3KhQf)HmW89RdQS&`8g#Di8(o` z3L!;_DXGN@W%;=ZVfjTRiRpS=w^&jVOHzwCfMHbR2_$YYMo)fVbC`$k7JEiwabihH zQPIB1>uqHxU$j-`JTr_TaPJCh^ryx9VQQCnOwd6iw@W(#{u zMn>_;MGonV`zGIVP~*78l9ZpHQ*>-HucHOy-N|l_J5@gxFsidkZ3y|m0HjW2fT;^f qX&2ISE+*xE2MGd&CYv~QF datetime.date: + """ + Findet den letzten Handelstag (überspringt Wochenenden). + Montag=0, Sonntag=6 + """ + date = from_date + # Wenn Samstag (5), gehe zurück zu Freitag + if date.weekday() == 5: + date = date - timedelta(days=1) + # Wenn Sonntag (6), gehe zurück zu Freitag + elif date.weekday() == 6: + date = date - timedelta(days=2) + return date + def fetch_latest_trades(self, include_yesterday: bool = True, since_date: datetime = None) -> List[Trade]: """ - Holt alle Trades vom Vortag (oder seit since_date). + Holt alle Trades vom letzten Handelstag (überspringt Wochenenden). """ all_trades = [] @@ -290,6 +298,13 @@ class DeutscheBoerseBase(BaseExchange): # Standard: Vortag target_date = (datetime.now(timezone.utc) - timedelta(days=1)).date() + # Überspringe Wochenenden + original_date = target_date + target_date = self._get_last_trading_day(target_date) + + if target_date != original_date: + print(f"[{self.name}] Skipping weekend: {original_date} -> {target_date}") + print(f"[{self.name}] Fetching trades for date: {target_date}") # Erst versuchen, Dateiliste von der Seite zu holen diff --git a/src/exchanges/gettex.py b/src/exchanges/gettex.py index 7a15e69..74da159 100644 --- a/src/exchanges/gettex.py +++ b/src/exchanges/gettex.py @@ -285,9 +285,23 @@ class GettexExchange(BaseExchange): # Nur bei den ersten paar Fehlern loggen return None + def _get_last_trading_day(self, from_date) -> datetime.date: + """ + Findet den letzten Handelstag (überspringt Wochenenden). + Montag=0, Sonntag=6 + """ + date = from_date + # Wenn Samstag (5), gehe zurück zu Freitag + if date.weekday() == 5: + date = date - timedelta(days=1) + # Wenn Sonntag (6), gehe zurück zu Freitag + elif date.weekday() == 6: + date = date - timedelta(days=2) + return date + def fetch_latest_trades(self, include_yesterday: bool = True, since_date: datetime = None) -> List[Trade]: """ - Holt alle Trades vom Vortag. + Holt alle Trades vom letzten Handelstag (überspringt Wochenenden). """ all_trades = [] @@ -297,6 +311,13 @@ class GettexExchange(BaseExchange): else: target_date = (datetime.now(timezone.utc) - timedelta(days=1)).date() + # Überspringe Wochenenden + original_date = target_date + target_date = self._get_last_trading_day(target_date) + + if target_date != original_date: + print(f"[{self.name}] Skipping weekend: {original_date} -> {target_date}") + print(f"[{self.name}] Fetching trades for date: {target_date}") # Versuche zuerst, Dateien von der Webseite zu laden diff --git a/src/exchanges/stuttgart.py b/src/exchanges/stuttgart.py index 7f11dd0..e9d6207 100644 --- a/src/exchanges/stuttgart.py +++ b/src/exchanges/stuttgart.py @@ -334,9 +334,23 @@ class StuttgartExchange(BaseExchange): print(f"[STU] Error parsing CSV row: {e}") return None + def _get_last_trading_day(self, from_date) -> datetime.date: + """ + Findet den letzten Handelstag (überspringt Wochenenden). + Montag=0, Sonntag=6 + """ + date = from_date + # Wenn Samstag (5), gehe zurück zu Freitag + if date.weekday() == 5: + date = date - timedelta(days=1) + # Wenn Sonntag (6), gehe zurück zu Freitag + elif date.weekday() == 6: + date = date - timedelta(days=2) + return date + def fetch_latest_trades(self, include_yesterday: bool = True, since_date: datetime = None) -> List[Trade]: """ - Holt alle Trades vom Vortag. + Holt alle Trades vom letzten Handelstag (überspringt Wochenenden). """ all_trades = [] @@ -346,6 +360,13 @@ class StuttgartExchange(BaseExchange): else: target_date = (datetime.now(timezone.utc) - timedelta(days=1)).date() + # Überspringe Wochenenden + original_date = target_date + target_date = self._get_last_trading_day(target_date) + + if target_date != original_date: + print(f"[{self.name}] Skipping weekend: {original_date} -> {target_date}") + print(f"[{self.name}] Fetching trades for date: {target_date}") # Download-Links holen