From 459c24fcd35f4b6922b502497d245933f46c1de5 Mon Sep 17 00:00:00 2001 From: Melchior Reimers Date: Tue, 27 Jan 2026 14:12:26 +0100 Subject: [PATCH] Fix: Analytics Worker berechnet jetzt alle Tabellen pro Tag --- dashboard/__pycache__/server.cpython-313.pyc | Bin 23990 -> 18287 bytes dashboard/server.py | 244 ++++++------------ .../__pycache__/worker.cpython-313.pyc | Bin 39097 -> 47287 bytes 3 files changed, 74 insertions(+), 170 deletions(-) diff --git a/dashboard/__pycache__/server.cpython-313.pyc b/dashboard/__pycache__/server.cpython-313.pyc index b0984a863c969197e11559a95af7eeb287cad131..36b2a128667267c875a4174fce68dc0e89a879c2 100644 GIT binary patch delta 4153 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 W<1O3)#=Q#GTw?=@m9@9S|M)N3L<*Gv delta 8154 zcmc&ZZERcDb?-fXh~FRLhdwAukNP5?M2eIsS+Zm~`tYZe+K!&-IFl0PS)wF56lFhB z{$RS&aj{lj+SKYrFlP-KHC+KE#ZYb9S|{sLCryy9KLTnpFhMX!!RaM0wmam zo%xT5ae#pQZXkv`4CD;;HwU^|LW5yBJ;w`t#I8uim(ODM%(+Xe%29cYz zD=cH05zpdvYbjgGS~@69ogjEyb`*V;)cfpXHjg+?a9Y^hF7@QlvyIu?spBN8;LCE0 zeogARa<+V|+(57uK^b4c9%d`YDp|*vgRL6Vjp@fK#|-V10*K;YyOMQQ5@+RSNxq7$ zE}RAI0^D{vTZ8dP8XZ|y`DzL48f911NWGs?h6+bJ#k#lQ z(>V;cr-KxUutx>j6w7SGb>(nTv&`3&5{+tg>ClwryKgn_JZ!ly(@# zJFv}wFK64g;n(Hx(Jy2o&wI9E)Jqtj$qY^h+a+jX?>4Lk39D0H)hcqh54iNg4|CoN zzx}|0fqiJ(%EsKv&OV9be!!&I0gNf>+CAoB2fI%b1Xo57EWs*<8J-zqMounXw~@xx z59Es^X zP$jmejDcwOFWWPOETGO9HD=v{V#`jLoHs)2#<~SLdeQt_#UUVZ7Jaky6h%?!H>KCe z0E$?=)geOq&M$0=5Yo~x)sZ&@BLQRAPy|OS?m{r@+!;ZieRK zAZ?bim)4+v)6++3$^PQ=vz|oDIX*HGrkm(Q%lUZ@a1)n>b%i?%D~fC#kDwp+c-n<2 z$&jVZgkeU(?*VT4X594SW&k()(3P?}@?rF6Wwhov44j8wd_Q`xtm~AL(3!3${^Zi) zr6u)0lr^oDHLcXIT>hE=j>_;I!Q4Z)Rgb=}HD)M9MN39Q_=C%*G6dPK&uEo4)3t*c zlfq_NIJl+D8~$%gw1_Kz-eXniRaXvfl@hv&8@6?A&6NXpw6#m^>sn7*?)faP1SYNy z_6|O@+FH>~k;hQfaYQi!gRi649DlCaR7qdwQ&oqwW|;Uo{6211pr2PgpLzv83=OLV z!=_qfF*U1a4Xm++ifIJM#L8e)dm7;nA*MV1vhdcQ{wv{aV9g;td~l^91GJWq4qB`5 z3EBkrWe;j&#*jk#L?4E|%0^^BN_tcx6jGokJFOI3hQ8J5kd=o_D9~j^Z**$Oa`fv? z=i!Qw8E97qWj;OdVGJ2rhp=;%u(MNus|C0ws1|WX^BaxNFdA$jsIYa$1gaY(q#695jYxtXr6e?}61ES3}<;^|jzounrhO z2Tg(wnzu`w71(ScPMPAApNLZy;t22>GqE7Iff!)RgR&FUSHQn(2^ywjrksBV(q4fL zpU}1hjWOf>#B68%0v7>+i`I|~7Pkq@f*}jQ?f7hp7I&~NG*r!4_T;w9o!jL1(Ko7H zsm|Ouz0+d0+eywoy6$Tk>IYdy&M-#s5x|3HI(7FokdB=OvRBx;J7+RQqUOzku8&iq zlBYn`J0amh?OENbzD0Iz%JFT-v%n1B3k@UNREoZrABOIxn!7MPF8CxymdD~p(ZA3a zrbhuIX~TXi=d~oC^|>nR*O{aSNQekbo&rPV;bax&VLZXl$0j`PN$#@SOD7_;;dp|Z zJ;x-S_zpjmtibN`()a}~Iv@554h{Qo{3M-tO7MESPVd9hmxU?xxS<}df4$nBta1xP z(0|pIr8FXpiEWbJP12n#rMcLo6a&za81304na?%iHaJT7obu914L!$ChI!miDq))m z7?b1oJtOGm6_R&hCCWIsDUJdp+&IbMBJShiDC)AC(L0P8{b{ZFFlGWjN!J%jg;Dr1 zOcS3|@<3k3qFb{}?@rQr>9DlNYw~?eWrv{2+I&!i5v2P>IuwwyK@tMB`pX=m=w>Xs zG=bM9uL^hL>bGkxB-xFAP*+8J=_$|$j@Qm`6Hn2JIeId}hbI#B`S~z^IXRLI+33{n zhwaJp^F?L@EH>s3Crb*m=Pk4pJiuspNNtEs`PsQhY*P_G7mXy~l|X&F<_ms@+a|32sfv2^C)g0CMNjD(AVnkXfFZyF8tyj%TT!CQYxb)%33xY4I32= zAC}1NngvZpM^w~pSSvqN$b_y&>~2`w0HIv7paTSJ^_rz&LA6m@wN_fcPy&nBD*OwE zjGVIR@9M1!N8!4)>o@d{w5|$3CxEtvp$tKlIqy0fZw$kj$Fbq4y^&%7tcOrkg+tL# zt%T0DQC0KOkr$5KQon0_)0jTN-tIfO-VplxzLPgwm&s+wUVSv}1SOHfq-(>)Ea9!1 z8#Wqz%n}~;>oYd35Vw>QR@ahkg<7)&7Y^L9G%dHUTLNia0Jse`PN&%0s?c4#Z`VWJ z^%SMkliBnjrJ?FUr}l-1mij%MTH4Zc^H{oPI6ZtmZRXcheDMi=P!N-S6rqU5Q{-nK zRT32g_%%d$j>3D26o%v7VC+{1oI~a0TQ$`~a@AYC{TArFZK@nn%inJ5mqF(ppJ~WY z`A+XZ4|IOjRXt=;{hhs^8bX!sF6t_Y?s^00C#@yuS@+MuSsd}SsD%5zG$=#gbvJZc zgeZs_m7*<3bF8c$q9EBQ#8Be1BXzW)H$1PBPpn3mlca*@01Mq}sGP+H5Fl zu)!tdNbyJTrd`DjEuVSj-4}LrqVLMZy|{M|mxq`V!Fv^i_I)r~7~f+gIlmu|3fDZ1 zp-yPf-osp{{l`YMtvLvLk79o}W)K2KEf+O%O zmoIBpTxnbTny!7};D$<{HV?0_AWY_KXNtpbrtO!3QBEM0&;o*c2>@kp(sZsVfx2C@3-juK4Uo zTOqCWCGAO=<)6UK72LR?NxmuES5Ph}MX$G=Kp(U`trRTF_M_cXLtBAoQm-A#P|#sox*nEkuY&CjC{swNC4^PB;HF0(0}Nw ztp&4Uyo)DV3>B#fE*_>Y&V*z1RN*>$KOLaqoMXjDiZ}9j3WybN#n8TDG7O~lt${#G zfc9^j$ZFk!DbjQ}3S0!}FdQz3^nRNog+~i{6S@AMvL(qVawK+(naPe7oqnE;6gF8A zWn3(bjh~xH6bIr0dqwLp)A!LOw%e%|k)kBUj>VGHsa>Ze`oEtu06sc)o-f{n7W9t+ z%y-dlq6lC<4^-_JkO#toak*#1Gd!e1xQ00{z#{orsyJxJoa7!s!9^^6X(PNphm!oi zlLGe9zWXT{q}i!~i{V&gI?O{kFoo-ZvHu$-?8aqYeo^iJ%Vv-hT(X1|m_^nU{$G@^ zU4B6ezl#!fQ*zFtbEZ(Bg%tiEa{UJ?LhxH|2rSM-qPwdLJnIgRBCd*s=ZlvJPB;aB zDe#4hgaT~AjXoujo0^YC>A86*tI#;5qzkXiI6a3imfYn<_ZHk<5&@~E5YPF?$*{g4 zvF5|b9Iz?-anLe=yn!Cg9|5=smwfyUbUtu6wX<+5m2CTO)!(wG4^O785a3N^lqy?W z#zX`=SKz-2vdh2W53cmTa$$KUL#R93AoPQo^MBE}Ql25|y49J64&k4S7%5bAvw7T( z=uUC9fByin+TYqp?iypgE5=yguGO!8zl|j8q);4cOgI%6gV}umedGDk;a+rle9VSphvB@8=(#_$YS!wY+ewb$T?7%D{2g^{~;AqbvjPv;aDjxquz~f3hk1HHJ zP91n`eLOxG9y7{grZ!dM<1ubFJU))z81!gv!Vnx95kHGEgO$qP5RGd@<7(Z3QqtK* zGB<)50$$7P7Jhv!BdGEW1yE)NjU4n=9wGr{aEriiJXw*UFqk<(q2~_{q(T(QY#2*3 z@TV8a+|`>h3JgGg4Pc1?sxYV)Kn($yS^= dateadd('d', -{days}, now()) - group by date, exchange - order by date desc, exchange asc - """ - data = query_questdb(query) + logger.warning(f"analytics_exchange_daily is empty. Analytics worker should calculate this data.") + return {'columns': [{'name': 'date'}, {'name': 'exchange'}, {'name': 'trade_count'}, {'name': 'volume'}], 'dataset': []} return format_questdb_response(data) @@ -136,25 +129,18 @@ async def get_summary(days: int = None): data = query_questdb(query) - # Fallback: Wenn analytics_daily_summary leer ist, berechne direkt aus trades + # Wenn analytics_daily_summary leer ist, gib leere Daten zurück + # Der Analytics Worker sollte die Daten berechnen if not data or not data.get('dataset') or not data['dataset']: - logger.info(f"analytics_daily_summary is empty, calculating from trades table") - if days: - query = f""" - select - count(*) as total_trades, - sum(price * quantity) as total_volume - from trades - where timestamp >= dateadd('d', -{days}, now()) - """ - else: - query = """ - select - count(*) as total_trades, - sum(price * quantity) as total_volume - from trades - """ - data = query_questdb(query) + logger.warning(f"analytics_daily_summary is empty. Analytics worker should calculate this data.") + return { + 'columns': [ + {'name': 'continent'}, + {'name': 'trade_count'}, + {'name': 'total_volume'} + ], + 'dataset': [['All', 0, 0.0]] + } if data and data.get('dataset') and data['dataset']: total_trades = data['dataset'][0][0] if data['dataset'][0][0] else 0 @@ -170,18 +156,15 @@ async def get_summary(days: int = None): 'dataset': [['All', total_trades, total_volume]] } - # Fallback: Original Query - query = """ - select - coalesce(m.continent, 'Unknown') as continent, - count(*) as trade_count, - sum(t.price * t.quantity) as total_volume - from trades t - left join metadata m on t.isin = m.isin - group by continent - """ - data = query_questdb(query) - return format_questdb_response(data) + # Wenn keine Daten vorhanden, gib leere Daten zurück + return { + 'columns': [ + {'name': 'continent'}, + {'name': 'trade_count'}, + {'name': 'total_volume'} + ], + 'dataset': [['All', 0, 0.0]] + } @app.get("/api/statistics/total-trades") async def get_total_trades(days: int = None): @@ -231,7 +214,11 @@ async def get_custom_analytics( # Für Custom Analytics: x_axis muss "date" sein (wird täglich vorberechnet) if x_axis != "date": - # Fallback auf direkte Query für nicht-date x_axis + # Für nicht-date x_axis: gib Fehler zurück, da dies nicht vorberechnet wird + raise HTTPException( + status_code=400, + detail="x_axis must be 'date' for pre-calculated analytics. Other x_axis values are not supported for performance reasons." + ) y_axis_map = { "volume": "sum(price * quantity)", "trade_count": "count(*)", @@ -278,22 +265,12 @@ async def get_custom_analytics( exchange_list = [e.strip() for e in exchanges.split(",")] if len(exchange_list) == 1: exchange_filter = exchange_list[0] - # Bei mehreren Exchanges: Fallback auf direkte Query else: - query = f""" - select - timestamp as x_value, - {group_by} as group_value, - {'sum(price * quantity)' if y_axis == 'volume' else 'count(*)' if y_axis == 'trade_count' else 'avg(price)'} as y_value - from trades - where timestamp >= '{date_from}' - and timestamp <= '{date_to}' - and exchange in ({','.join([f"'{e}'" for e in exchange_list])}) - group by timestamp, {group_by} - order by timestamp asc, {group_by} asc - """ - data = query_questdb(query, timeout=15) - return format_questdb_response(data) + # Bei mehreren Exchanges: gib Fehler zurück, da dies nicht vorberechnet wird + raise HTTPException( + status_code=400, + detail="Multiple exchanges are not supported for pre-calculated analytics. Please specify a single exchange or leave empty for all exchanges." + ) # Query für vorberechnete Daten query = f""" @@ -312,39 +289,16 @@ async def get_custom_analytics( data = query_questdb(query, timeout=5) if not data or not data.get('dataset'): - # Fallback: direkte Query wenn keine vorberechneten Daten vorhanden - logger.warning(f"No pre-calculated data found, falling back to direct query") - y_axis_map = { - "volume": "sum(price * quantity)", - "trade_count": "count(*)", - "avg_price": "avg(price)" + # Wenn keine vorberechneten Daten vorhanden, gib leere Daten zurück + logger.warning(f"No pre-calculated data found in analytics_custom. Analytics worker should calculate this data.") + return { + 'columns': [ + {'name': 'x_value'}, + {'name': 'group_value'}, + {'name': 'y_value'} + ], + 'dataset': [] } - group_by_map = { - "exchange": "exchange", - "isin": "isin", - "date": "date_trunc('day', timestamp)" - } - - y_metric = y_axis_map[y_axis] - group_by_field = group_by_map[group_by] - - query = f""" - select - date_trunc('day', timestamp) as x_value, - {group_by_field} as group_value, - {y_metric} as y_value - from trades - where timestamp >= '{date_from}' - and timestamp <= '{date_to}' - """ - - if exchanges: - exchange_list = ",".join([f"'{e.strip()}'" for e in exchanges.split(",")]) - query += f" and exchange in ({exchange_list})" - - query += f" group by date_trunc('day', timestamp), {group_by_field} order by x_value asc, group_value asc" - - data = query_questdb(query, timeout=15) return format_questdb_response(data) @@ -376,25 +330,21 @@ async def get_moving_average(days: int = 7, exchange: str = None): data = query_questdb(query, timeout=5) - # Fallback: Wenn analytics_exchange_daily leer ist, berechne direkt aus trades + # Wenn analytics_exchange_daily leer ist, gib leere Daten zurück + # Der Analytics Worker sollte die Daten berechnen if not data or not data.get('dataset') or len(data.get('dataset', [])) == 0: - logger.info(f"analytics_exchange_daily is empty, calculating moving average from trades table") - # Berechne Moving Average direkt aus trades (vereinfacht, ohne echte MA-Berechnung) - query = f""" - select - date_trunc('day', timestamp) as date, - exchange, - count(*) as trade_count, - sum(price * quantity) as volume, - count(*) as ma_count, - sum(price * quantity) as ma_volume - from trades - where timestamp >= dateadd('d', -{days}, now()) - """ - if exchange: - query += f" and exchange = '{exchange}'" - query += " group by date, exchange order by date asc, exchange asc" - data = query_questdb(query, timeout=10) + logger.warning(f"analytics_exchange_daily is empty. Analytics worker should calculate this data.") + return { + 'columns': [ + {'name': 'date'}, + {'name': 'exchange'}, + {'name': 'trade_count'}, + {'name': 'volume'}, + {'name': 'ma_count'}, + {'name': 'ma_volume'} + ], + 'dataset': [] + } return format_questdb_response(data) @@ -424,67 +374,21 @@ async def get_volume_changes(days: int = 7): data = query_questdb(query, timeout=5) - # Falls keine vorberechneten Daten vorhanden, berechne on-the-fly + # Wenn keine vorberechneten Daten vorhanden, gib leere Daten zurück + # Der Analytics Worker sollte die Daten berechnen if not data or not data.get('dataset'): - logger.info(f"No pre-calculated volume changes found for {days} days, calculating on-the-fly") - - # Berechne Volumen-Änderungen direkt aus trades - query = f""" - with - first_half as ( - select - exchange, - count(*) as trade_count, - sum(price * quantity) as volume - from trades - where timestamp >= dateadd('d', -{days}, now()) - and timestamp < dateadd('d', -{days/2}, now()) - group by exchange - ), - second_half as ( - select - exchange, - count(*) as trade_count, - sum(price * quantity) as volume - from trades - where timestamp >= dateadd('d', -{days/2}, now()) - group by exchange - ) - select - coalesce(f.exchange, s.exchange) as exchange, - coalesce(s.trade_count, 0) as trade_count, - coalesce(s.volume, 0) as volume, - case when f.trade_count > 0 then - ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count) - else 0 end as count_change_pct, - case when f.volume > 0 then - ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume) - else 0 end as volume_change_pct, - case - when f.trade_count > 0 and f.volume > 0 then - case - when ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count) > 5 - and ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume) > 5 - then 'mehr_trades_mehr_volumen' - when ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count) > 5 - and ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume) < -5 - then 'mehr_trades_weniger_volumen' - when ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count) < -5 - and ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume) > 5 - then 'weniger_trades_mehr_volumen' - when ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count) < -5 - and ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume) < -5 - then 'weniger_trades_weniger_volumen' - else 'stabil' - end - else 'neu' - end as trend - from first_half f - full outer join second_half s on f.exchange = s.exchange - order by s.volume desc - """ - - data = query_questdb(query, timeout=15) + logger.warning(f"No pre-calculated volume changes found for {days} days. Analytics worker should calculate this data.") + return { + 'columns': [ + {'name': 'exchange'}, + {'name': 'trade_count'}, + {'name': 'volume'}, + {'name': 'count_change_pct'}, + {'name': 'volume_change_pct'}, + {'name': 'trend'} + ], + 'dataset': [] + } return format_questdb_response(data) diff --git a/src/analytics/__pycache__/worker.cpython-313.pyc b/src/analytics/__pycache__/worker.cpython-313.pyc index 563a8bad7aed17b9bb20e4e963dd82472cd436bc..d2384294939bb992721e2a8b57ac365efd881f8a 100644 GIT binary patch delta 7284 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 M_E8)9%PHIc0x6nZ1^@s6 delta 1743 zcmZ`(TTB#J7(Qoauk8KCE_;DlS%}*xh=71a+fpy7T1*|sOQ6+dcR?&HW_D2_OtP~YlHA52yxY5LIEb7s3MiR~o&&42#O`M&+n z`RDdi=FS*nd}c7@A@X>}_P6i*V$67nVa73xF-}|r_n9Kh!5_>D*3awVIpc-ZtP?MQ zcUcd?9vO$&4+2#TU*JZ&6=FVbYgY2gW@nXxcgr3ZSs9vjn@XXa%`YU09@$;Md)-KM z^FD$ezEJeG>4L?ndy4B}orZ{E$?w@P3l&2FICS6<6Z#55dVi|_69ooOU!lz`U?Zwz zsxeiL5QjD%guzIW$3p$QRZtSNRVrV^1qA*Y2Yh%=HsKMM9 zTx4(Z{To1nI$KP(a>#UwtcTEfo8bDO&&=n`gie{zRjGuJP6wf`&!T8^2R)EJ_@kd& z#D&FZDA68k^Y;}7ZPJcM^FQk17(FeS<#x&A+Oo!H|t;k^cRUeqr2c(jk3D2~C zb*^stMAby86lHFM;N0&h)CLL1KORUtHlcpSsWk2>iw!>2%=)WkJo|eq?6Uhv{cgTC-pW%Oef@Jxk5h?EqO4e zbHL35+WMUPTk8IQiJ_5~<8bF>R?}i=IaT`~x@J#RGMRtI@kT*G?zBnJ5i|=bGDxZy zH#?K$<}FRdq)GB4GON9nT%{E-?{}#K|2YAB#=-@biUg%N{xb za|Av{X>wBqoLX?v$OLkivMkw~?3h?QX_D4%leX=YLaoxyHp#wgTGI~EFGotI^(~K0 zj$@q1I3X*bj=?t4`mYE$)>OPM9F&_T^erF!YeiV+G{8dyWE zwFH6+k$$Aag>ZaiJ9C*yJsG*H!8*8h;}+>b_~vItWi)>?%}G%3(&b@lxu|6Y)#y?S zb$b;WndH;0IsZp2QdBgv-`d5!XO^p##(p)9#rmDJocTs2G;FPlSTJ=+Q$w_>;! ze!EqqY9J(kGska#AE1`FoWg4q{8V-!ff-GGXIG@VBf3)D0Keb<+7Q-Z&r?KkcAXQ> HOnCnRWHY}u