From e71d1a061e45249a6591254f9dda2793db8c80f0 Mon Sep 17 00:00:00 2001 From: Melchior Reimers Date: Tue, 27 Jan 2026 10:14:27 +0100 Subject: [PATCH] fixed deutsche boerse --- dashboard/public/index.html | 149 +++++++++++------- .../deutsche_boerse.cpython-313.pyc | Bin 0 -> 13187 bytes .../__pycache__/gettex.cpython-313.pyc | Bin 0 -> 13978 bytes .../__pycache__/stuttgart.cpython-313.pyc | Bin 0 -> 16409 bytes src/exchanges/deutsche_boerse.py | 65 ++++++-- src/exchanges/gettex.py | 139 +++++++++++++--- src/exchanges/stuttgart.py | 20 ++- 7 files changed, 277 insertions(+), 96 deletions(-) create mode 100644 src/exchanges/__pycache__/deutsche_boerse.cpython-313.pyc create mode 100644 src/exchanges/__pycache__/gettex.cpython-313.pyc create mode 100644 src/exchanges/__pycache__/stuttgart.cpython-313.pyc diff --git a/dashboard/public/index.html b/dashboard/public/index.html index 2fb66e7..0acc709 100644 --- a/dashboard/public/index.html +++ b/dashboard/public/index.html @@ -71,7 +71,7 @@
-

Moving Average: Tradezahlen & Volumen je Exchange

+

Moving Average: Tradezahlen & Volumen (alle Exchanges)

@@ -251,61 +251,81 @@ if (charts.movingAverage) charts.movingAverage.destroy(); const dateIdx = columns.findIndex(c => c.name === 'date' || c.name === 'timestamp'); - const exchangeIdx = columns.findIndex(c => c.name === 'exchange'); const countIdx = columns.findIndex(c => c.name === 'trade_count'); const volumeIdx = columns.findIndex(c => c.name === 'volume'); - const maCountIdx = columns.findIndex(c => c.name === 'ma_count'); - const maVolumeIdx = columns.findIndex(c => c.name === 'ma_volume'); - const exchanges = [...new Set(data.map(r => r[exchangeIdx]))]; + // Alle Daten nach Datum aggregieren (über alle Exchanges summieren) const dates = [...new Set(data.map(r => r[dateIdx]))].sort(); - const datasets = []; - // 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) => { - datasets.push({ - label: `${exchange} - Trade Count`, - data: dates.map(d => { - const row = data.find(r => r[dateIdx] === d && r[exchangeIdx] === exchange); - return row ? (row[countIdx] || 0) : 0; - }), - borderColor: colors[idx % colors.length], - backgroundColor: colors[idx % colors.length] + '33', - borderWidth: 2, - yAxisID: 'y', - tension: 0.3 - }); - - datasets.push({ - label: `${exchange} - MA Count`, - data: dates.map(d => { - const row = data.find(r => r[dateIdx] === d && r[exchangeIdx] === exchange); - return row ? (row[maCountIdx] || 0) : 0; - }), - borderColor: colors[idx % colors.length], - backgroundColor: 'transparent', - borderWidth: 2, - borderDash: [5, 5], - yAxisID: 'y', - tension: 0.3 - }); - - datasets.push({ - label: `${exchange} - Volume`, - data: dates.map(d => { - const row = data.find(r => r[dateIdx] === d && r[exchangeIdx] === exchange); - return row ? (row[volumeIdx] || 0) : 0; - }), - borderColor: colors[(idx + 2) % colors.length], - backgroundColor: colors[(idx + 2) % colors.length] + '33', - borderWidth: 2, - yAxisID: 'y1', - tension: 0.3 - }); + // Summiere Trade Count und Volume pro Tag (alle Exchanges zusammen) + const dailyTotals = {}; + dates.forEach(date => { + const dayRows = data.filter(r => r[dateIdx] === date); + dailyTotals[date] = { + tradeCount: dayRows.reduce((sum, r) => sum + (r[countIdx] || 0), 0), + volume: dayRows.reduce((sum, r) => sum + (r[volumeIdx] || 0), 0) + }; }); + // Moving Average berechnen (7-Tage gleitender Durchschnitt) + const maWindow = 7; + const tradeCountData = dates.map(d => dailyTotals[d].tradeCount); + const volumeData = dates.map(d => dailyTotals[d].volume); + + const calculateMA = (data, window) => { + return data.map((val, idx) => { + if (idx < window - 1) return null; + const slice = data.slice(idx - window + 1, idx + 1); + return slice.reduce((a, b) => a + b, 0) / window; + }); + }; + + const maTradeCount = calculateMA(tradeCountData, maWindow); + const maVolume = calculateMA(volumeData, maWindow); + + const datasets = [ + { + label: 'Trades (täglich)', + data: tradeCountData, + borderColor: '#38bdf8', + backgroundColor: '#38bdf833', + borderWidth: 1, + yAxisID: 'y', + tension: 0.3, + pointRadius: 2 + }, + { + label: `Trades (${maWindow}-Tage MA)`, + data: maTradeCount, + borderColor: '#38bdf8', + backgroundColor: 'transparent', + borderWidth: 3, + yAxisID: 'y', + tension: 0.4, + pointRadius: 0 + }, + { + label: 'Volumen (täglich)', + data: volumeData, + borderColor: '#10b981', + backgroundColor: '#10b98133', + borderWidth: 1, + yAxisID: 'y1', + tension: 0.3, + pointRadius: 2 + }, + { + label: `Volumen (${maWindow}-Tage MA)`, + data: maVolume, + borderColor: '#10b981', + backgroundColor: 'transparent', + borderWidth: 3, + yAxisID: 'y1', + tension: 0.4, + pointRadius: 0 + } + ]; + charts.movingAverage = new Chart(ctx, { type: 'line', data: { @@ -321,7 +341,7 @@ type: 'linear', display: true, position: 'left', - title: { display: true, text: 'Trade Count', color: '#94a3b8' }, + title: { display: true, text: 'Anzahl Trades', color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#64748b' } }, @@ -329,13 +349,20 @@ type: 'linear', display: true, position: 'right', - title: { display: true, text: 'Volume (€)', color: '#94a3b8' }, + title: { display: true, text: 'Volumen (€)', color: '#94a3b8' }, grid: { drawOnChartArea: false }, - ticks: { color: '#64748b' } + ticks: { + color: '#64748b', + callback: function(value) { + if (value >= 1e6) return (value / 1e6).toFixed(1) + 'M'; + if (value >= 1e3) return (value / 1e3).toFixed(0) + 'k'; + return value; + } + } }, x: { grid: { display: false }, - ticks: { color: '#64748b' } + ticks: { color: '#64748b', maxRotation: 45 } } }, plugins: { @@ -343,6 +370,22 @@ display: true, position: 'bottom', labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 15 } + }, + tooltip: { + callbacks: { + label: function(context) { + let label = context.dataset.label || ''; + if (label) label += ': '; + if (context.parsed.y !== null) { + if (context.dataset.yAxisID === 'y1') { + label += '€' + context.parsed.y.toLocaleString(); + } else { + label += context.parsed.y.toLocaleString(); + } + } + return label; + } + } } } } diff --git a/src/exchanges/__pycache__/deutsche_boerse.cpython-313.pyc b/src/exchanges/__pycache__/deutsche_boerse.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76f5aa242cba23620f8c32aa27be97dceb9c573f GIT binary patch literal 13187 zcmcgyeNbE1m4Etv65>-x2oUCBgM+XUHpY&PO&kbdz{c1-WK)By3h4oIgoN)CiLGtA z*-p1hx6>WmHe+ZyW4ZBe>p&zZZpXr zyXV}eCka9lcc-%#``-KRJ@?#?cXZD0ockW+=bIQvxBu)?;AguT=6Cp@Bwf1l;0{!t zV|a#F9cPGKs)&j{tBIPH8lr(xeO%k8CE7k6(e>$xoniSfZ?F?_KVEDYzy!{$^6{Lu^nxQp+v?$YBOj`?} zwJ@i(gtitzYjIAiWkTgFxsU8lIyDmO_eO+BU`mjTnEQob#4E8_jfO*lQ!Qyv1jLBM z4opV^;gC0YUj+l9+1%k3g|17!NpEOEkaR=D>ld6VDZfMT&O`#^GeJH)Gaa4j4@U#R zptotiYY$i7@AU;jk+3*g3v|r-Q;-VeEJy(zJiy zqpp1ixccKgL;WXqbHTuq0(VUCJsEa#PfU{Vl+d)dd5??!alCijO9JU3(W4RJQlx1z zG8NqI1%?5i7Z=}j30rnu+T1!7JbbZjkL%FxhNcEuJLptLO>R*jjqMXcC?e_GeLi72 z;*(#2oU`eN2mc0@=a^9jB>|*B1rnep+EE>^8P&I_d2I!ALAyUqObnTJ-G+7+sEwm~ z6g3#5$39}>4N4o&j^+*8(@b)ToKbq-NJr)Krcpg@FQDACd>(Bt4^}T zAt4_MnUb)M(*K|Uvgeo)rJWH}PF2)4@RUGEz%Out5GT%z`RE$Nn<~k2G92lh0&$vx zcPRL~2niEt)`~)KTtudv#EHb$B&3B{Z1Sf!=ozCsbWLBD*fH3qXNCl&N@Yjc?YorD zQs5?{26>iQ*E5CN6Ku@`T;{{?<0L5odO{h|wGkUUOWKeZewk%kOpP7oKq8_bKFfSi zPy>?^Y}qg=_R_=q2y#$fDo{>LNVSa_Q9}<+gIcAQEw?jnW>BAfR%#=P*n>jjwTCnz zwOc)yr}Sn^Mc_H6i3w@ks=@qhpHky>ThFmSy|R$jt*uWJd&?NctsN{>#%4=izdtQ3 zA)Q-S0P1s}I;0;^^K3{D^P+%mO2cD=c-EUzW>&+9Rp}amS>@z5D6=Rf>`J4o&JQnp z1dpC3IFT**^Tx~!teYLIRH(BhZ&G+$n4Y&8CS-IQ2NmO!B?EK7n=`yjKvR{|nXdBr ze8C}AD9>$D*62OUfN^k}2CF|!t-P(n(B*vs=!zbKt_kRh-6lE)#1=->e3~;~tn7&| z!L1Ca+@_G(tr^_$X-k59$zwQXq)u5kZvpAusXS#%kigOrr}FI9C_JFux=*J(aWH2sQ^SljD3k`q`v|=A z{G1tVTmL?QQ<*!zfZ3&43h~UrhUAt{!dz-aW+ne#RxD03Kr<2&Dn;=Q9n>%-ki>dXx&5eQP%}A z92#{^M5C<=xBtV~JT&mGY1pbjB$ZdvP7+~UVuk>i=tQtE(~>c>!YE7UgefIe)BXQ} zB&kL|X3uloVLXtil^YKP1(ACy;N^%gAzW$_5z%a;c7z)jBEHE$Xo3;}G=Z}n2S~zw ztZwC;Hc3qci6z3t89|JQl4b%d9_kXw(9_l4*)_;ZMZ_Bb!!jNw9x>vL%!ra+6uiVY zDd{E%%p)1b10g@W3Q3DtM$+OGl12nZsBR?|yFJ*?0&@?g(FMa#!@hMQARYiMfP93I zU(!tz09^zXj-&|+A;}1)7|;v=B4+_XT}J@%0!Jc<)8UW^V2*BFGEc+d1ul7}ys%YC zFF#3!X>TMVkdS1c?Scpf71s+33_VHkXH8ltJ7hwwmr4-Pa&sG!Y~BhyxOk-W(EZhH zVdyrJpso{t1RDQY=67uIn!4U-TP>@;`ql4#HP-oJ|MmVi9(}tbSvD}+vtlk?HrFN1 zb$88=+$*)scCDEh$F^&S77iuq+HOUD_RLS8NmcgGp7?EHV+#8Vr8#1-fT?S4$dB1Ew%s9GJp1YN2+w^ zY}bG5nbPvrvf9}2i|4POU+hnnb9+_`TRJP1@0t>q%mKTq_61^FH>8mO3@RI3m|G!*#=fXb)Q;(m!*V?vJ`uDqH zEiblSZ;kt2IJ(&Us~rd4ZkcCS>{Zvy3+CAHdv@3R%`fkUlLO%4Kz2QSEQ`NL?XYxzt+UNXN16KlbBQf@6OWgfZMckZd z=}naM-8COyvDjx%e6(6eJt*QclFIK+cB28&6Qw&0P{QM!;rtlnvNaPyj9{j)}ndKrNVlP4s!B143hF9 zUV@T=zKIWjOC}*C!RH5G0};r;84Z98EtuV0%JdLEqksV(s7Es96g{v*sz#s!n4tfH zkp{O4O!7sbWrv5U!LxwT&<+~XljIa`Hmj8T;WsyY3F)a(+RiZB(9{@n$kSC`593hJ zLPE2z7(Opp=8+7%0#m;gjQTV*F}Y1V+X^7W`*7={}QCe{{F_!&Y_+jPk+CnoVog`K!kGxf(QFWz--kxO~8oj@rJ-x<;DUc z*WBE?XAjrkb&i`5CL>&Z^ImvvYTkpb?x81~ySb@1-vC<|;+_r=&O76qlve@`A+K+e z>km+k23447=m}UjdGih5lOjwO34jCWGRZbD%GQ0Uv2Lod&QGly`VxNcWigtwi~3Jl zIp+|zH9zC@q|5vGp6{cmI@z@5g^i7)IVFh12?`9^3z?)r#odgLk3r_lleP8Z*l-*( zS#N{-gu@a1k}3VG0d^7_0;>VPpUaY2sez#(k`|S%q?2{Jb}~FeM3k=_-7ITn8){`> zOO>-o?G*kPCFfQedP~wiU@P8+>{;eP5o0U?)iBF~Hnr6(Tb)U(Gv1K0?w>WSShn3L zUsNS)nv<61S$3tYdbzAVSymsfNR_qBntxYXKHCYSiYiitH7iB-tA$qzW7_wM>Q;)Y z-Yu?M)3U|o_du`rF7zgLo=FUkrX1(zx_(>cSgqb3+yCO>>xbhPUwAB4-8`=a^&6{s z{@{wE_S(S0K)mxk$KDS$jHC8L9b>NoJ!q|1+s+i(XA3`CH!yZ^;8exsQmK7S1Leme z{F0cTSG1ctTEAEV!LI`TQ$6(+$e%+eNDaSz)gWlC z&52hj8bkTj`*(nLQEjP{)Cw)QAIw&c8-PQ#TLa)w6VeW--KvmwB;&>nA|~6Ez|qr= z;A&|~J9Xel4VsmGz~gpi#G$kfBI@0g;KV+6Ha_o= zN)1+Jgi{6%g67|xfJ@eKHSlKqQ!D_-oHc;G0S7C@g48gB8K)!5C2MCk*bCJ6>OpWM zHYc}HISp{GfO9`67<6ddYSi_+nT+g#gpR9G*K_c9vP*U#z)?{M7_bK5>V`}?Zz|ty zf;XL!O&raeL#B39zaJbiTFW{kn|Q%lZ{StnR>AuMM|Gp@C>(LUn>jj*W)8r9PrP}< zAK}3A2wWdOa1wyMY?HW2fy|)&ppFhY2eKbTLzVzvCnJ$*f=71$2xJ8PFT!;%Z zc$om0WGBMo3xn%Lw*lCLA&9_88xk0NVK@SaWojp);0CeBAjf@4>OfdBbl}o^2P7?e zVF2Y}W0J)~{U{Gzv`1dFY@2e8(I_(4QyP&R!v_{(zd=-p#w880SgO1rkQK%YT8h5Ww*@A6sZUa@6XzoxA`YpA#-3WXxRMsvy~-WejxHRH??YhDu58hP&V>*>Vl;Y7)o?wZd*RI$W%wd%W7 zH^__8A4M1Imzz71%^j&np7>R9=i91P@H2XU&>QnUe`3z~dks_4rFvjyDr@KTYifOQ z6}TbGj+UgOW${ePafF)375lbp`3w0m;XV6qu!3L!4M4wUXNqjI`MwOU%9a2&iTVyRy+?J>a!0Fy-a7_UccbP`adCIO=1#p1>SP*b z*-j;2gRM1OVCGIkW3>YsLTsxi!48wG~cTId?s8N}qd>p1TQm6Eft?Z1|jW2k@OU zn_{-N4aAj| za|htTDrfJfXX<9D={ADt)`UzLMW(U<@gRr}qnoiQ@kNsd=Z*Ig^t(pC0Yhs07zt<)~!G(fUZo89iq_Yt>W}PfVa?taj{KlBm*(G zyo$3xyxlxfsSEMW6t#Rw!X1?^0p1Ro?hzY<7fGI?PYm`yEmBx8D0xKQH zc&XliG7GDmwwZnK5=^HkzCjceLM#*&f>;~Em~7)9HNM~hi=1DWl2MXLF{jE!9bBh9 z4RJ#u!VM1bdz&C@hL9-w>@Jh(Y}zO0x`iMhjaE}5u=aO?JC8FU#z7yStIvA#PMKtA zDoZj1t}a*K)6?4D-^%l@ahi~>m+w8<3RlB`b7t_;j_dV99vFwPH1u`{LbPkD*YD~D z{M3&AX$Wq1pAzUux+b7I1yKSQ&HBN8zI?_TbhS^x3QrLsAVUA?$Yqy16o8g~0q(9% zk{0Kc@^bh~>Rvyoh1YOW(jq5`Rai+H;6=j$k_H$_TI7V+-*5=D7@CT*doc6>|0LCr zq;k<ks%~MdN_wB4iLIC!qRdFi6m_NckAA4v1kn9z(v4 zZ3ZGt2faRl;IB;5%tU+yeM1^NrZEVLEtRk_8d&n@n0*t;wGe3bNxDENBoOi!*rK@z z%Lj1i_xK?mjb&sCN|8n)bO0|SBALbUJ&=7?keED)tad75nsfWQ1wdUuMtHUPEVEY8 zsWRH{S<0`rUTK{lPg&}gEkkkJD-|~@7W0+v(H&xrW(6v&vJtjQ=Wtl{Gb9but(fO{A zR?2t8BB}CSt6W{oA8$?WJg~eI^444XmugZwJ5yZOyzZk_YjsR}Lwva_7F@ROOIr7> zI%;CI&mW)bx>s2Gt!HAoc<~D+dF^%w#J&ql=k?$I(-oUz-hE|iPJ>JL-Dp|1)F&8J`&&JOz@~@wL_3SP8QhREDcdG7Ks=DV&56-2z;d^<1Y;@V$oU}I60nbP0 z5bGujt78WtqS7W$W^-IUcja8n5HCqtch7dOSS!=d6)9`WZ1;ay%4NFp>bXnv7h|>< zo3J`(yYCg2%#DBh8N{F%w&qiI=bUcUVw;rR)c0dshnW%Z1gjyky}cD~`HlM`O~__}cL6 zqpyx89E}M_$DHv4i+!cM>RR~%E^5VEzG8#Of6Yq8jt}z*_BJSPKeh| zYap-=v|9zRl-U8@W&=Ii&kP~yHiwXa@^ivRA>Bi9)#nC|6a)gu#jr2;ewG=`-2P|x zJ3OSEGz54aW(RD}T$z9m%w^m#mmb1BClGtW{xaVYWM|EOeGp>d0wIGjawu}H9Uf|3 zrkr(U#=^P)u>yL_tQ3MpC(}3I53^fy4p&X~WcTIVRO7w3Ge5fD)DsS-@0uw%fs0nS zK0OmA5$^<7k6{cBZ>PEJ@5AW=Q;k7rYEVf(>|PGhI3Pr$T*uP5f(Vfs!XarZM;d@G zYUU#0h&L!-=|9D*nT?tt^@BixCcr zTN>3Z=>8LVwu8O_qslTi`>Y9WD$TalrUQ%7r31@NeaWW2`G#vv3r%+`b|p@o`_^&L z&k$ul@`EEUv#*$Mnp2hg-!Lv+Odjk_R`xDe3?wTCQWd9XkAJ}C&#C5WuR52UlQ(^xMTqJlTv&RF_FPQEr^uP@a zL@eQUT?mDt+u(u7m=7xKGzm|`#oJ~0DgeX|m6eRs5NQ%BT_}aJJW>&(P!8pc{3T`> z5|F`_b}SqYk{CQoRuBCiA>i|%FbBqFKwdl^@*|{0%|xY%pqE86aLO3*pCS7u^RL=d z_jLJdYPD|r{0_J#)@^^FVe|#G0mKGtD&6)uA4G~WT~J%o@6y>~+8BS`xXwUz?VPGc zXODL-T5k5PF;HFVd8_~D{j{1mb1pIR<>VR9I(Dpes~U8+Mavok#jVm?!cQyLvAQ;_ zI;{d0wQCHPiwAFYy?S(=);}CqZ_`yQ4y`dzygf;a#MpRZBA6VT`aL$%CV8j0Ngw>r zBs@+`4-l^Y5kC3U6XS%z$x8Y3gGl%)gMLdJ$cJCr40?sMnX6ZjZ`wg~a*g!vlz@rf z_^lrz0$k$*)HKgA@R_grWYmS>ZLzhfX=+?_`O_abHl{yt^o6IIHr~U^ZyLVmV&qRG z<@b%kCD}GCGxP0Z5ls64{RN1z79OQXx8V!KtyU%JhT-XFjFP2vIp~V+OaJ{`@vWfzs0r zNB_r?9^jR||K9qAQt7G4tUy^~MuHr$Rfx z$L?I=uz!(o`~wOHW_Yd{;n?#~!ci^@M|nm#WKclaIZNexJpuU#Fq^;(b-IimQTssv zZe5ljpsh>5hn=flr r74wdD2FvwJ2BWGhQMz-TfpXn$R9O@D27IvMLwUkh|6lMRAHe?tf^T84 literal 0 HcmV?d00001 diff --git a/src/exchanges/__pycache__/gettex.cpython-313.pyc b/src/exchanges/__pycache__/gettex.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d81427af22f78e0828b6293ac61011092f05fcf9 GIT binary patch literal 13978 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 literal 0 HcmV?d00001 diff --git a/src/exchanges/__pycache__/stuttgart.cpython-313.pyc b/src/exchanges/__pycache__/stuttgart.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3bda68b731f9e0be5ff4362ea6ee4e349200306 GIT binary patch literal 16409 zcmd6OYj7J^c4p)KAV`n|hzFldilhjNBq%;aiLxwHd{d$xY{HU7Sy%{YQm{#Y?gsTh z*`7%@HO0wf6KWloAUhdBWo-<3TxB?MC9K-Xt~{QMG@hv%V4xAOZ6;RLc&Ez$=t%3C zlK<>Ex7h$lL9()2o7yJp_U+r}p1#j>zH{!u{o-Oh1=p*8biu!O4@LbJAEe8;k+~m( z%$pQLF|vacC*Eb8j6BOZIk_u11-UCZCEVo)RTC;sHKFFz6B`Z7lyIet zg)6gB4HRRoq!`;NL$5+?DOWxxbJ)Mchr0XfYcO23Ut& zP#*I0Awhd&G2{;hJo8`5pdu6-4tRKW_=5MWCosnfs%g&SV;wS~_yFr!3i)T3=9%Eq zVmLS%4EyKjJ4g?CRM|U)a^-pa&Kg=h@@znS=h2 zv$wmiqk9+Ka&T;V@=zN+?|+%4M_KR7K?gl}mJ2Sh&YfMr0RL#lGwb2}8&$&pA;ew? zInRa`=G#2L(C_tNvz-^PWXFZAr3>?WU)k5$v8TFSgM>c6J_yw~ao-g=fx&LW_LA z(|P{<`HnL|mgCuWKC~1H&3U*`hmUmvUr1+{Sgw7J+!887OHe8-7ISOMs8{`(kC0I7XhP-Fnhgd$8|2!G=vH8!8 zA3Bgnk1Y+`U5j(LZrjHLJnLQJ*!C&*l_lWBhl>Y2-m`4`U@#Ekg7e{*{~u1?1wJ&} zz8K^~?ICQ8(<|;%G1YE7-2WOfZ&Ify6e%E1WFSi9obr^4QJhlu$QflN^^&r8gP7Am zsrHmw1<&wKh3`1srZP1t(=!^0j?tbfnzC&$$h+m$QZqVItC-QBQj_u$@&zSRM9Phf z;Z!MdBvQmCnP%zW38x)8)CC|>!@<>Aql|1KlvKtbRcjor;;zdCj zfUP1}vTc%@-GSxXp*oKv_g(6q(R?2=)TSn=p@1~uYe?UuoK!$oOU)n|xu)b&E_ZLB zX0W~Fn!)t4wFEI_Ay z3OeLDMo~a%dXyuhlUkf@L&sxj^^efPPI(O9qDN@49e+mXD+;v3@CbeVV`+=G(t2R7 znG%<(gzAZ zTQ663uA$eSM|xcfy{4wNZLO7Cbpe@6)=1S;GmR3BhVnRI6!bH=m&wJLU3#1aX`EnO zzlCQV#gqs11A62w=4NHH)ILQWH7j#eEDZo#{ZS9chiIRlr5}-U3=1laUJCeVKs=o1 zte@pV^ic48U_R*awIA{aUgkUWU*c%pkPDi#ET|Ta7c_|Mf=i)r5h9`vfES<$1(`=s zp5@qCff976iHIT!>vKx!p`v3h{7}{L5+4kNl^tIG98>^2BgmZLx^K~qVftGLoPwJ7 za{k2-2djWO9Nt6d_XAX#Iem&hwY}k)XJ6crZB|~{sg9QY{d3`0u~n}^n;hY)8IaU3 z(!(4Vr|xl)qO~X4^_nWF|KB!NiS;A00~7>|Q-| zd+1jaKcD!;q2D}n0xP_B>9wdUZQYTy?udmxcM4kKY#5-&vf-O`9S}a`yJ61$WWYJ9qCW0xhV&=(0~FjoX*y5>w}0vzFu?7W zNm@FWlffDgzL5x&Ffd!ibG*kGEEte#4^FWM(QSOnogUwE0Nb!~m@W^Dxr4WT$;tQ%y z4lNx6ZW>T81;CF>Q$m0Xq}uK*5JJWJSZbY1{coVwqf+0@dvZclW0UVl>a3A+6fONV zbWT5tB7~>|mOBr!hy^{7RSY2Gc>0WohxZ_GXwT9teTIG669|C(p(mI45X%KR9>vt> zfECcQAv$z%(GS)sK7)LuKe>K30A}`CmL36<3ci+<<1onQ~=S-4w`q;TK2agk&fznM`OoCBgAzA(!dw8PPQAoiGOj&Zo=zQl!j z1Mt7kbCG|jZF-@-X`#Kz_fXyRLeoN%PgIqJzWt$=Xi3nQd8Hj;W3~q(nL{h6z*wJS zIovaXA`m>!p-mttmO@?*KPPBGYs{j)<`Dimv_#-@&*H0Mm_gRP=n3I;f%L~_xjl11 zp$L=@k=!s7qHhPZ7bldWmj`ohZ*XDJ;|&Q)u6-Pcg|=C6tp zlqdd_gQC@?BhO$*GU|;yS}7Va|CC?wKn?9i?GngCPY737u#A2qk6&DRs`f5# z2uTGIl7QT)5&t}F9RSR*FZ>|+W&EEp{SRwJV{u{nl(>9?Ad;th`eBcJ=Z^#%NC& zw@ zFSSx?gQo}DpauyG{tAGE1+9Enw&m!{kZKYy7x*xj&y4CrRsB!VgE!`5rld_z07UNB}^e+nKz?U4uc zF8#)M6v23)F|p-xWxZnm_v2AdwNcU__&^iR%4w>OqTz2;cDPCOpMb9htz&Y5uLk5W z(!l#($-@E^xs}l@(dO<@3wAlyI{a~7oqVnz;rsuuXUFjGJ3Do-vWcH8f0SUo2MjaP z)~A?amkf-V5|;tIWm&m_;|#t+!+=5LA?9=bu)G<_PSMn^Y&M&j+MUg2Gcc1t88EmE zQ+Qsm<$8<^2JPm7{gl^kazXUKwaVR0!7Qrjn7Pg4~w87xgypIk9L*PoovzD;DpAOfE@5ZL5kJ2o0 zxJf1#`rN{{d{dfs26A#*YX}4qTfYc2-cLBM?1u9v?60{k%T_j00ZqiOu5~b~zYK(0Vsg zxjEJwMmZ1|@P~eA#poB-ADVcBNE;{dQImaCs;u|f*c^6s>pgU=9+?#1v@B{xHa0uW# zaN?=gm3_KB8&16F;78V*4N2?p^4LA2HErCMG;WLXDPwEKYG2Xa%k%2pv(%<7%}Go1 z@+jcHQqy0&`u$fU!)aqf(%7&rSC-k=0N&NSRr5agLHND!>h_fLz_0CtH($v#Y5!sJ}}%c#80MMo=dhow=UNKud2GM{crV0=TcQ2 zD?@A6`e@_(%CxmTX>I?kxivD9X>3V1?oBrCT^&g^4n~GQuWO7fW$5O2i?0>O*t>Mk zR~l;j4sbkHH>^yq>&uGwuXR1Mdi(I*i%dcO0Jvw*C+L4%gVMV!SufAIQ zR_*)p4~pI^iVvk)_S|HW_VE?HCzd%r2O1>k7Dk^PrLfM<4NFk{ACY`@%w* zYwlC};{EGNZQ1^{UC-UL{<0~(>u7S<(MaFbXRkaP+kU62EAjk`Fc>xT)zfdCe*eG+ z2j4rms!X-*yKPF=Osl#R5x6myfPVUx?A1( zuNsPOUaur={lfzjWvj=g>|Zn66V+Yu?s!w2TdiL;B`Wu&&Ce#y&#spe(r?yHK=p9l z1a1800m5o==F`Tuq_GX1lm}!PW9eUnzaLH;o07(+m?>p!TPv+d*m~n$K<^E!$5sau zmgnx24txXifBqk$%Nbsp_l)>s{+zbH8QCDsa`*^Q}wC2Y@$u|X_FF&K6Q}tHWildBqhDb;?v!=hdLF% zuk}8@pMdUg{tOA7;^}ifTANcbs(=DK12VMdJxRc?#cg7UU@rs zT*TP|$dIF%$y%l`gZg}4Q$<@>7|a+1s!1c36f24Ajp87~ToWFnDNE!c+ zMhec`H;oj;;^ao^F}t)hZx`lUq2Q2uOpUU<8hDmoXv84IP#O`i`*hh>!H#n20NIP@ z;EGLl9+^)#(=W6xDi}+apLWI|t(+-Xty?d!JhL`(pvYBJ$Xnq>`?{#mm!9m!V61y& zfub3!^i8m1vsR2$2An~fxfCw7tBA4fQ4?DyYej;c0W|>YLkq-zO51jtZ?D zvby1(vv3@h;pU?R!Xv;B&MfeMc_D0c8KyM>JV}Rgr z;tc>lBq-2Ts)4x3c@OjowVY5q3$dYoK8V4Ao)8y>GIFNE9mO0v$~c@YVk{7(E}?N$ zoZNLx{tG065>D8?g32F&6A$ihutf0+430X)6Q3)DC)(7mo4k?CyrO!mwDm--{KDN|G0G##^iP{m#F8@BWvn%k*-NhjS~b-~eAkE4CP&ibfYPY> zjn@J9MW4TBh^W@8nxgFc)9I?NWL4K%qa)Vzf#ZfFK5w&wf(EzHx;Scp_FZSMfE%I_6|pny*(jz+*S?eM@Dl*{e$mhEY%U$ zrG*s*_S<{CCv9p;np)QC8l%j+$FCiaJs)TO;rNHgS6w#;QoSRorqNX0*rl;A@crB2 zOJkX`s>r!iS<_k-eccp&J~okT>Pyw{PS-z|tbgvN>83YTKar|BxT5`?sVZ_Iw)5@R z#1>epFQ2@0GOCMpr7Aj>NB+fR&)DnGe;pl-O{MIeE9%_y$&`KfiW*7i=E#NUD=|w< zo2c2jGV(hpvQ~ks`ts>Zr=x>0TdJaKd1TE-r)?d-wspkL!GZtsc*Y25x-NFV}mb^Avr{^8VzrxMleiRuF@x-U#Nbf4R=Aiqq7J!7fL*y}Tu+rBE+m`m0vO>xN| z9+Ypy3)l#p?u~omOrm_x9Yg<`$@!H%GHcU;w9Z7Dgi|bxm*efuoOB>!#MoBS~*~ut%Xqtj6SQbAJ@8~$F$B+pmm#| z?o+o;l?R7%5fA_^o}zUC#0rjSNiLovI|>l+C&2>W8gn$ta zeMsCJkO;C)0zA0C#1hbN6!#`3mobSzA{Y_dfci(z<}}7UT)|gwVe&R6e}&0kWAZ~t z1R2d;#fNK;Q|-OsU7Ly!6KzK_WbsLEobMaj$6> z#Rr-jnz&}wlxo?RvOT*zzGkb6jK5t18YKxDg$KuvZ8wyf?GZ=H+_;S4VG-F6!lG8Q zR$iXFG#6nvbmvK6)3h&Td3Jep&0>#ie`7vvX-!&MW8L8Evh+ZywK{VAjn|2HFtYOp zl5VtLK7Q$V$W#lMUG+!?Qrn-b-{cMQAo^e0F`>W>{GPU`)E5=cMZOKzX& z2O8k%r|k!1aQm54KU${v*`AV7gW{G!1^L55nLps2U-Gf;ix8H;a(Mo)2apUa4-e}% z#i^h*j%)gUKT)jks{x2pk;JeE_G2yx&jr69k;Gb-q7<#0LNWW&(G8p($^*(H&*Uub zLT4v%diX>!DJiQN^j(t+LjuKcy(fJgyhGX0J4Oit5Zb%-0)+tKe4EdefHEsKbM#W5 z0;;Fts-CHk>gFQo;cJ@itQw^;-rFishTk@=Dh^NVK%OqOOim9p`t(421^+FFdjJzG@4LmSU z)9#|Yc4tqFq1^^l-QC%qz-jj>@wEG?xE7Oe%aZ)QKUFq+<uEkjq5E``!5aNQ%Q(BoWtxuNLN1N}II^Zedxnx|?!?8<57HPcdxZ;RZMaSRw#+!fia_q&`_S=e^ zO9{*5-O|GlGijzFvQt-*-_hRqOYu`TEs2t`JKFJ0T}xE=e8j9){+&C~Gc1n8J)=FL zwTtnVC@mfG>BA1Ipmn?PmvL^lpm)0$g1#jPUULfuw;Q}h^BZrBZirQbGj3QYY#<1) zHEuZP^g>1pD}527`7Uw<VL0R`FhT~?JXHWWAA z7+$Wr+>0Q*et9tyX+^@BwDmUzd%^fP;CR z!h5`Bwf95k1Cn2#mhV#8qK)em++w@q!#DOn!0cCxvhAwM`1Cpjx7%mQEpcWxF*lz) zv+#Q?Bt>G+vC|Hdc$UA21F(e2YnX7D5P(fY21*d{mOKh@L@v9nR fk^H-w^+w7vC{Ix357ft1vdW0ze^GcRL-4-=68`Vq literal 0 HcmV?d00001 diff --git a/src/exchanges/deutsche_boerse.py b/src/exchanges/deutsche_boerse.py index 51e1983..189a621 100644 --- a/src/exchanges/deutsche_boerse.py +++ b/src/exchanges/deutsche_boerse.py @@ -28,20 +28,44 @@ class DeutscheBoerseBase(BaseExchange): def _get_file_list(self) -> List[str]: """Parst die Verzeichnisseite und extrahiert alle Dateinamen""" + import re 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) + # Primär: Regex-basierte Extraktion (zuverlässiger) + # Pattern: PREFIX-posttrade-YYYY-MM-DDTHH_MM.json.gz + # Das Prefix wird aus der base_url extrahiert (z.B. DETR, DFRA, DGAT) + prefix_match = re.search(r'/([A-Z]{4})-posttrade', self.base_url) + if prefix_match: + prefix = prefix_match.group(1) + # Suche nach Dateinamen mit diesem Prefix + pattern = f'{prefix}-posttrade-\\d{{4}}-\\d{{2}}-\\d{{2}}T\\d{{2}}_\\d{{2}}\\.json\\.gz' + else: + # Generisches Pattern + pattern = r'[A-Z]{4}-posttrade-\d{4}-\d{2}-\d{2}T\d{2}_\d{2}\.json\.gz' + matches = re.findall(pattern, response.text) + files = list(set(matches)) + + # Sekundär: BeautifulSoup für Links (falls Regex nichts findet) + if not files: + soup = BeautifulSoup(response.text, 'html.parser') + for link in soup.find_all('a'): + href = link.get('href', '') + text = link.get_text(strip=True) + + # Prüfe href und Text für posttrade Dateien + if href and 'posttrade' in href.lower() and '.json.gz' in href.lower(): + # Extrahiere nur den Dateinamen + filename = href.split('/')[-1] if '/' in href else href + files.append(filename) + elif text and 'posttrade' in text.lower() and '.json.gz' in text.lower(): + files.append(text) + + print(f"[{self.name}] Found {len(files)} files via regex/soup") return files except Exception as e: print(f"Error fetching file list from {self.base_url}: {e}") @@ -50,11 +74,12 @@ class DeutscheBoerseBase(BaseExchange): 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 + Dateiformat: DETR-posttrade-YYYY-MM-DDTHH_MM.json.gz (mit Unterstrich!) Da Handel bis 22:00 MEZ geht (21:00/20:00 UTC), müssen wir auch Dateien nach Mitternacht UTC berücksichtigen. """ + import re filtered = [] # Für den Vortag: Dateien vom target_date UND vom Folgetag (bis ~02:00 UTC) @@ -64,18 +89,17 @@ class DeutscheBoerseBase(BaseExchange): for file in files: # Extrahiere Datum aus Dateiname - # Format: posttrade-2026-01-26T21:30:00.json.gz + # Format: DETR-posttrade-2026-01-26T21_30.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 + # Finde Timestamp im Dateinamen mit Unterstrich für Minuten + match = re.search(r'posttrade-(\d{4}-\d{2}-\d{2})T(\d{2})_(\d{2})', file) + if match: + hour = int(match.group(2)) + if hour < 3: # Frühe Morgenstunden gehören noch zum Vortag filtered.append(file) except Exception: pass @@ -88,13 +112,22 @@ class DeutscheBoerseBase(BaseExchange): try: # Vollständige URL erstellen + # Format: https://mfs.deutsche-boerse.com/DETR-posttrade/DETR-posttrade-2026-01-27T08_53.json.gz if not file_url.startswith('http'): - full_url = f"{self.base_url.rstrip('/')}/{file_url.lstrip('/')}" + # Entferne führenden Slash falls vorhanden + filename = file_url.lstrip('/') + full_url = f"{self.base_url}/{filename}" else: full_url = file_url response = requests.get(full_url, headers=HEADERS, timeout=60) + + if response.status_code == 404: + print(f"[{self.name}] File not found: {full_url}") + return [] + response.raise_for_status() + print(f"[{self.name}] Downloaded: {full_url} ({len(response.content)} bytes)") # Gzip entpacken with gzip.GzipFile(fileobj=io.BytesIO(response.content)) as f: diff --git a/src/exchanges/gettex.py b/src/exchanges/gettex.py index afc5cf3..84ac844 100644 --- a/src/exchanges/gettex.py +++ b/src/exchanges/gettex.py @@ -17,7 +17,9 @@ HEADERS = { # 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/" +# Die Download-URL ist auf der gettex-Webseite als Direkt-Link verfügbar +# Basis-URL für fileadmin Downloads (gefunden durch Seitenanalyse) +GETTEX_DOWNLOAD_BASE = "https://www.gettex.de/fileadmin/posttrade-data/" class GettexExchange(BaseExchange): @@ -32,9 +34,10 @@ class GettexExchange(BaseExchange): def name(self) -> str: return "GETTEX" - def _get_file_list_from_page(self) -> List[str]: + def _get_file_list_from_page(self) -> List[dict]: """ Parst die gettex Seite und extrahiert Download-Links. + Gibt Liste von dicts mit 'filename' und 'url' zurück. """ files = [] @@ -47,16 +50,32 @@ class GettexExchange(BaseExchange): # 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) + text = link.get_text(strip=True) + + # Prüfe den Link-Text oder href auf posttrade CSV.gz Dateien + if href and 'posttrade' in href.lower() and '.csv.gz' in href.lower(): + # Vollständige URL erstellen + if not href.startswith('http'): + url = f"https://www.gettex.de{href}" if href.startswith('/') else f"https://www.gettex.de/{href}" + else: + url = href + filename = href.split('/')[-1] + files.append({'filename': filename, 'url': url}) + + elif text and 'posttrade' in text.lower() and '.csv.gz' in text.lower(): + # Link-Text ist der Dateiname, href könnte die URL sein + filename = text + if href: + if not href.startswith('http'): + url = f"https://www.gettex.de{href}" if href.startswith('/') else f"https://www.gettex.de/{href}" + else: + url = href + else: + # Fallback: Versuche verschiedene URL-Patterns + url = f"https://www.gettex.de/fileadmin/posttrade-data/{filename}" + files.append({'filename': filename, 'url': url}) - # 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) + print(f"[GETTEX] Found {len(files)} files on page") except Exception as e: print(f"[GETTEX] Error fetching page: {e}") @@ -211,19 +230,95 @@ class GettexExchange(BaseExchange): 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 zuerst, Dateien von der Webseite zu laden + page_files = self._get_file_list_from_page() - # 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 + if page_files: + # Filtere Dateien für das Zieldatum + target_str = target_date.strftime('%Y%m%d') + next_day = target_date + timedelta(days=1) + next_day_str = next_day.strftime('%Y%m%d') + + target_files = [] + for f in page_files: + filename = f['filename'] + # Dateien vom Zieldatum oder frühe Morgenstunden des nächsten Tages + if target_str in filename: + target_files.append(f) + elif next_day_str in filename: + # Frühe Morgenstunden (00:00 - 02:45) gehören zum Vortag + try: + # Format: posttrade.YYYYMMDD.HH.MM.{munc|mund}.csv.gz + parts = filename.split('.') + if len(parts) >= 4: + hour = int(parts[2]) + if hour < 3: + target_files.append(f) + except: + pass + + print(f"[{self.name}] Found {len(target_files)} files for target date from page") + + # Lade Dateien von der Webseite + for f in target_files: + trades = self._download_file_by_url(f['url'], f['filename']) + if trades: + all_trades.extend(trades) + + # Fallback: Versuche erwartete Dateinamen + if not all_trades: + print(f"[{self.name}] No files from page, trying generated filenames...") + expected_files = self._generate_expected_files(target_date) + print(f"[{self.name}] Trying {len(expected_files)} potential files") + + 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}] Successfully downloaded {successful_files} files") print(f"[{self.name}] Total trades fetched: {len(all_trades)}") return all_trades + + def _download_file_by_url(self, url: str, filename: str) -> List[Trade]: + """Lädt eine Datei direkt von einer URL""" + trades = [] + + try: + print(f"[{self.name}] Downloading: {url}") + response = requests.get(url, headers=HEADERS, timeout=60) + + if response.status_code == 404: + 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"[{self.name}] Error parsing row: {e}") + continue + + print(f"[{self.name}] Parsed {len(trades)} trades from {filename}") + + except requests.exceptions.HTTPError as e: + if e.response.status_code != 404: + print(f"[{self.name}] HTTP error downloading {url}: {e}") + except Exception as e: + print(f"[{self.name}] Error downloading {url}: {e}") + + return trades diff --git a/src/exchanges/stuttgart.py b/src/exchanges/stuttgart.py index d10fc8b..7f11dd0 100644 --- a/src/exchanges/stuttgart.py +++ b/src/exchanges/stuttgart.py @@ -8,12 +8,22 @@ from typing import List, Optional from .base import BaseExchange, Trade from bs4 import BeautifulSoup -# Browser User-Agent +# Browser User-Agent (Vollständiger Browser-Fingerprint für Stuttgart) 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/' + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) 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,image/avif,image/webp,image/apng,*/*;q=0.8', + 'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br', + 'Referer': 'https://www.boerse-stuttgart.de/de-de/fuer-geschaeftspartner/reports/mifir-ii-delayed-data/', + 'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'Cache-Control': 'max-age=0' } # Börse Stuttgart URLs