From 693a35901078bb2d27581039bbdfbe88dd484a5e Mon Sep 17 00:00:00 2001 From: Lxy Date: Thu, 4 Jun 2026 23:32:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/__pycache__/data.cpython-311.pyc | Bin 10748 -> 12532 bytes app/api/data.py | 83 ++++++++++++------ .../__pycache__/cache.cpython-311.pyc | Bin 14225 -> 15795 bytes app/services/cache.py | 53 ++++++++++- app/static/index.html | 61 +++++++++++-- data/buffer.db | Bin 3354624 -> 3354624 bytes data/futures_analysis.db | Bin 4218880 -> 5632000 bytes 7 files changed, 161 insertions(+), 36 deletions(-) diff --git a/app/api/__pycache__/data.cpython-311.pyc b/app/api/__pycache__/data.cpython-311.pyc index a1badf7101079ac71bcfc6b7af5c7cdfe71cc1e4..5d3f8c9360a05f5c19130d753926ec981daac6c0 100644 GIT binary patch delta 3102 zcmbtWYj6|S6~4P`Nh@jfu#BvSElV=SMt+9W7%*VK4+=IUDJGalu(7=oHDF9vav`v; z7-)j=K&)g*Ft4DrK-5CPsngaI=m71H^qJXZwngI^&oBik=AW3*8GbaGo+}~QCi#~; z=YD(6x#ygF@44q*>3^+HJ4-&+=`;u$b;iAGtO%ipSV%YKIrG&wpOhRYgzJQ5K^B|q zEnzLttzODoVoRGM*7kyI@-jXRYZu}~8IdQL$R$L+u#PMv9Kr#ziYO2wz&nATr^1#b z>w1r}5biN6Rc2Z=L3{VI9f6)+Z?_^d144K`6!foZKzkao1WWEg0}7TLQ1TQvt`=50 zEiRtALLhV%da4a51IjdyP&i8(@SG%RvpNhmU-1| zYwUkbbA^P>K~J?Kkb(i#&s6(~{pbL-1??x)C}2*HBE)C$h$ncGqxkLQPGT>Z^U7=5 zE0DjdnamE*Jnc8nS_;GJcmx^JU!cA{<6Q%s{}MVPW)z_;ffM&nukLW|P; zJbWv5YuHo%_~yve^-GV&Ka2fQh#fx}KmGg1r$2po_(*b^OazGg=;r14$c^~v%klFU z=Wl^lv3`9;W1}ZN_(4huu0imfF}88dn%ed2Yg=1A>3|%*n6LC(0ONz9*wDqPkAE5e zFqBzpt?OWxtD?@8w8+e!GKyXLWa`t)wXS~r@|3$CpE@&j;|7R&5K}^Ib@=Ugc(B&h z6M{s|j*;4L+UZy1gw`ME4D|auy8Bq~gp&0JI(znb2yQb(6Dq78q1QTG_FEAAg#d?< zXl+VSHyc-?d|I9-pyr!>x9n7 z_4W4D`ulu+TyJN9YXBwJh)2{-2fDrc0zG|ux#d{aq%1nT2f8^GR#5%`$C-uQw(?Q~ zw+Q$hyvvy#Kl|xwc&MfQ=7CQxLMw? z8DvbRZIWr5Y}zI=+nzy7sPHl{qnWGNOiX6G!lv-1$d)^+#l;)PbI0j%T6}Gz*x?nk zeNwhh&h`x|qXu&V5$X=&`8?b2Y!Vl5e@>=$$NvZFeh=e}5bwl=(b^kpfp zM$W5As1;8A69n)S18_P6a5~sS8vd(C#zI(UG}j4RzF2y;G~6NC7s~d9QA^MrEhxIU z>+G(Gee`vyV5wZNRLprLv?#Rbjw7K}8Z}Q4z*7tfq*QC3K`_P!IacVWAxi5ewR0dh za7;6ClGYD1LyTzd9IXKWg0oA0#kP(V6O`yKneGzluII*~_An7@54B5lkxUnfbWtWq zUAX6daqYe0TB&%MT)a#)*8xfN3YlIZ(kmwQc@avi-t^CIaYO50egE(Qk4fJq>Dy#| zn@G1Mi%N|b37j9_&7tbb73j96zK*)9UiDJ_66#M&6d?SWY%Kp_75cntPP3Kz3srAy z&Y}L6qX1#-Xx_gG<#ur8+Jz2Bu6ZkF@$WrXjiCa=4q?bqO6(AR=h&gx1B%bb>;))DyhFh&_eg2K!LgDIg%>Xk@m4z-?GpHL-58W}7h u|B2p&Iw@+9+Bh>nD$ElKkqv}Q)F;TKRXvGF`k4KQU1UlmV!n{&dG>!TyK+we delta 1407 zcmY*ZZ%kWN6uu3Wav8P%&s`vQ-H#Eb2BA-8v_by#m9{ZLh^8 z+anNzNv9+-t_yW@v7fLCDq26F*@x|$@l#70d1*pI{F8mqWFd-Jx3{}~8;YBcf)NM7}fmwX8Qq8htY@qqE?A72Me*lrs+MIp3+l6Wr)$+V1QEK?FKQ=|ATEaF8ClmV&Iq(;^x>`8jp zMo6SY$hm_=&0Tt#S%iv9AXRz?kD^J2Y&H3bk98boC$(U6vbGnU?VHpkwYzaJA>AUJ zVrlOrlf0;v`L~he7CHE0sZQp}1G|HaG}q{Ok$VnOWsX=lL6G_VsTj?^C^M?7HEDce z2UgV0Ku#~(KbWMMT++KJSwk#A!6x#eet!?k2J&59qowwVO6r6jha30rEi+X|muM2r z`|C)QSOfF4EL#^D=oF+%a4EGDml65ab%cI&ZzNt`lv}Q_S{s zectQ`&bn3)<=gv!mEhA~g+C9kwibfXVlXt%sHu#^=W~zJkzI6uOjJkK$_hBzQZRiLKZR zuAZW+CvWagT}@qGJzLhZ20OcjSOdGQLK(3PD; datetime - normalized_candles = [] - for c in candles: - candle_dict = dict(c) - if 'time' in candle_dict and 'datetime' not in candle_dict: - candle_dict['datetime'] = candle_dict.pop('time') - normalized_candles.append(candle_dict) - - timeframes.append(TimeframeData( - period=p, - candles=[CandleItem(**c) for c in normalized_candles], - candle_count=len(normalized_candles), - fetched_at=cached.get("timestamp", ""), - )) - - return SymbolDataResponse( - symbol=symbol, - data_type=data_type, - current_price=cached.get("current_price"), - timeframes=timeframes, - source="cache" if cached.get("is_fresh", False) else "cache_stale", - ) + import traceback + + # 解析时间参数,默认为当前时间 + end_dt = None + if end_time: + try: + # 尝试解析ISO格式时间 + end_dt = datetime.fromisoformat(end_time) + logger.info(f"成功解析 end_time: {end_time} -> {end_dt}") + except Exception as e: + logger.error(f"end_time 解析失败: {end_time}, 错误: {e}") + logger.error(f"错误堆栈: {traceback.format_exc()}") + raise HTTPException(status_code=400, detail=f"end_time 格式错误: {str(e)}") + + try: + cached = get_cached_data(db, symbol, data_type, [period] if period else None, end_time=end_dt) + if not cached: + raise HTTPException(status_code=404, detail=f"未找到 {symbol} 的缓存数据") + + timeframes = [] + for p, candles in cached["timeframes"].items(): + # 转换数据格式: time -> datetime + normalized_candles = [] + for c in candles: + candle_dict = dict(c) + if 'time' in candle_dict and 'datetime' not in candle_dict: + candle_dict['datetime'] = candle_dict.pop('time') + normalized_candles.append(candle_dict) + + timeframes.append(TimeframeData( + period=p, + candles=[CandleItem(**c) for c in normalized_candles], + candle_count=len(normalized_candles), + fetched_at=cached.get("timestamp", ""), + )) + + return SymbolDataResponse( + symbol=symbol, + data_type=data_type, + current_price=cached.get("current_price"), + timeframes=timeframes, + source="cache" if cached.get("is_fresh", False) else "cache_stale", + ) + except HTTPException: + raise + except Exception as e: + logger.error(f"获取数据失败: symbol={symbol}, period={period}, end_time={end_time}") + logger.error(f"错误: {e}") + logger.error(f"错误堆栈: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}") @router.get("/latest/{symbol}/{period}") diff --git a/app/services/__pycache__/cache.cpython-311.pyc b/app/services/__pycache__/cache.cpython-311.pyc index 5333b966f4bc8f61e3535cd23878cd3abda5bf1e..aca66dc2339ef10269cdf5481fe01b0fb03b5ebe 100644 GIT binary patch delta 2553 zcmaJ>Yit|G5x(Q&c;xX&iab6{iQ-YBBvO=2QV+ySx!j>DQNsP8pVmYerByyY9YFV^`(<1$oqJIh;i33CsaG?~I68&RQv{8%x z=q@Q*65M8SJ3BMq%P?dT>1n~)c`R@8~?4nCm~{BxSrh_3>+Y;U{UfD&5gG$T#l zfVUyM`5W*6!u`8&#{?w=k*?*D&fbnr&I+mtS_mPaRZzV^1f#i!s?YZIYzpW~O0+m5 zoJveiizQ}y?82$>u^C}XjE_^>7ls!aO*NW-FKD3I@d}-!BsJ)gD0o=ofZwqh;kP_b zYAK0Yq~L3gX0=Yz!F!$%M<;C9yE+8*93knRhKEcZDyf%fNe}B92fPMJ15}b`hLq?e zBQa-;HT$<660^uc(rABD^@-*qk|2JDW_2W)Ng`jjJd{3 zrZX1!cALYJG@mAt7RmfHaq^$ZI;jrkjSf!G?u~nns{$_z)LtNIm8>9_XsLxI2+5jg z-H|?0q2r-h9bjBfBpw|B9fzwSlC_eOgBIwj3qBi{v`Mx-iQB4)dmc~RHZ#ynkc4De zw8Q5-_GsPSbCSf1#MKWks9z-BASLVni`)D^;z~A&Ia|44V6R1YJ{JAz7d0&PIQDg- zvz8~iAIsj8N!qg-i~>foNLq#vRF_pdy@FoGA{o(RRcx@`6KG0oT8DH_EaQ=6v*gtw ziIh|ltC94JE>vwzH1adl1V+#==um+h&R-xQ=k15LU8hs$L@+rk#QXX6>q{F8x7IT^ zzq-9-;I~Ju`uq7WKZP3~+}gPLo6VcQ4y<3hx0${Y?AZKt{;OZydAM?a{r7PF%1^53 zBj;TW)6Z|*yY_J9r`1h17Sii?K0f~C{m(Yc?=Elt=-PImXe0uUdLNJ_*vfZ#$r6>g zI5{&h3vhRpG?gwSM=nyKNTeTGevglm``{J+P-+C@Iv}2#8XFfuAC|OJvtvRWbRiz} zVufKcJ~uf?QevuW^?#$^ z7KB6`cS!9;cCP?;b;*2MOq_y4K4aZ167QoK{}6RsnkaX`)4oqqWr}DH$&T*qtJznV z)#+!EOgj~wN7e<4?%=9BT5v~KIN2S|y9X8bV8*;wXG;$iIam6bBFmMD7H!AhFv*Y) ztd1M)^Up1e=B*uywIicLp&A-x+EsLT%7jxNsgO*p=;m*=zu&$%l6Oaxlsh83W6QDM z4*hCq<@J0|OzDYbju*}DGGS*U>o(8)*`IgkZDGY0E)#6sQ)F4Cv-=+SI~0Ha%E0Qt zaA9EhPjUIg%lUzm%D~CIe^l|0&g<9e{YrgEuJ0=bqN{;}g}}i-aB|>aJ`hs^vH4?L zTV<8X?klXTi4MHfF$Z*6U@@uBRoH7Bn)+hk{V(cii1?=ARy zS5Ca;y*I4M;UUSMRUOxmTlo8=U?SQ1unD{%X8fd*DZ71TWc16)zVh5v@KD2 zOIWdl<;wRDiZGgGvp0W$;T48o94IjRWoCcT8(ywoHZHTvtl}M%jSXwwaK@00-Z+q_ z8`cKdg<_YyMd8jP>Yp?>1dh?RyQW0l*}VNJ%Ho zjQwCr6oO_L4RVfZiSD%l=ix}pQ_L@o~@Hfl47{Vp609m(D3JVugf_?P`b@-n2ujoKe0Xe<{B-^S`cM~*U; zkXNzqn~;yTYO{zv3y()(od$#vKRgVcM@vuz&cQFD?`eUEB$(>zJ&Icc;9LPt65zxD zJ{7>*1H2Ky!R`{ki+S8w|E8aqo)ykdiG$z+B;wzHd?ojvt~;t!vAM12beGweNRlcO zm=buBZd0Y|^0(EwuY2Do{|oDmnd|@n delta 1191 zcmY*XUuauZ7(eGG_vYr_+@xL7kgm)Bq)kj)o2D~aySd@^AcH#R9~=W`v!0vTY+dZR zS;yv$bh8f?C$4;ZaMFhgf(`~lE{OP|FJpp;9ZZOY!C()*M)u-^4}K@5YrSxO=lA`- zzxTV>`Ea%2|Hd#hg3q7xq59L*sy{-1_-<$qrFod7S^5IZ&?GIuby|^F0e+_WX~R@a zrC+3UpPI%7mgUoNFYyvK%v4ijUNgirJOMmWvya8_j$>A6CyN=TQR`s|I4%w7$z}KH zWH&PfA4q+A8tv3BJA-z5$1Z2l&TQFO-7v#7g$<&XeXb2*M}tWwfqu8XmK~NS}{*5dF5cZ@0``BHTZnBnfJj$ zp|8{@R1Y_28yo{8!cs1pmrbk5Pl>D%0V0*ZjbQl@;C43MJA>p$p^Q*MmPjW8zh&

bJ?eqYvW;9>ou=Xg7 zYbREw|AEom%QBB+S?8nNbxA*qjL!;SpcisIyKEuW;Ar9K|AJNv6UupX{Y9K#3zL#| z5xE}+v<|_MQrO9k_R?$vHA_NBBzP3BP4%4Fe6Lxx&T|XRpa?Hwc+t7>;_H+yz~ixj z6E*a3@h5dx#Lo#fCE$I8J1#ChG#8b59l;WjiS7D6HfL4OUNE?{9+a;kt-wSXToSKByL?;Tbm7;8#mfH7j0h78;$k!1_vK Optional[Dict]: """ 从缓存中获取完整的多周期数据。 + Args: + db: 数据库会话 + symbol: 品种代码 + data_type: 数据类型 + periods: 周期列表 + end_time: 结束时间(可选),默认为当前时间 + max_candles: 每个周期最大K线数量,默认100 + Returns: 与采集脚本相同格式的数据,或 None """ @@ -208,10 +218,51 @@ def get_cached_data( newest = max(r.fetched_at for r in records) is_fresh = (now - newest).total_seconds() < CACHE_TTL_SECONDS + # 如果未指定结束时间,默认为当前时间 + filter_end_time = end_time if end_time else now + + # 确保filter_end_time是naive datetime(无时区) + if filter_end_time.tzinfo is not None: + filter_end_time = filter_end_time.replace(tzinfo=None) + timeframes = {} current_price = None for r in records: - timeframes[r.period] = json.loads(r.candles_json) + candles = json.loads(r.candles_json) + + # 过滤结束时间之前的K线数据 + filtered_candles = [] + for candle in candles: + candle_time = candle.get('datetime') or candle.get('time') + if candle_time: + # 解析K线时间 + if isinstance(candle_time, str): + try: + candle_dt = datetime.fromisoformat(candle_time.replace('Z', '+00:00')) + # 转换为naive datetime进行比较 + if candle_dt.tzinfo is not None: + candle_dt = candle_dt.replace(tzinfo=None) + except: + filtered_candles.append(candle) + continue + else: + candle_dt = candle_time + # 如果是aware datetime,转换为naive + if candle_dt.tzinfo is not None: + candle_dt = candle_dt.replace(tzinfo=None) + + # 只保留结束时间之前的数据 + if candle_dt <= filter_end_time: + filtered_candles.append(candle) + else: + filtered_candles.append(candle) + + # 限制K线数量,超过max_candles则取最新的max_candles条 + if len(filtered_candles) > max_candles: + filtered_candles = filtered_candles[-max_candles:] + + timeframes[r.period] = filtered_candles + if current_price is None: current_price = r.current_price diff --git a/app/static/index.html b/app/static/index.html index 6c155fb..d386778 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -917,6 +917,10 @@ +

+ + +