From 07debec4ce8d8fa83c452a5bca1693543ab5bc97 Mon Sep 17 00:00:00 2001 From: Lxy Date: Tue, 24 Feb 2026 22:32:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=9C=80=E5=90=8E=E4=B8=80=E4=B8=AA=E4=BA=A4=E6=98=93=E6=97=A5?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/data_fetcher.cpython-311.pyc | Bin 18368 -> 19930 bytes .../__pycache__/tqsdk_adapter.cpython-311.pyc | Bin 16251 -> 19035 bytes .../data/api_adapters/tqsdk_adapter.py | 65 +++++++ .../qihuo_analyzer/data/data_fetcher.py | 30 ++++ backend/service_implementation/service/app.py | 16 ++ .../service/data/futures_analysis.db | Bin 12324864 -> 12337152 bytes backend/src/services/marketService.ts | 2 +- .../数据存储与缓存策略.md | 123 +++++++++++++ .../最后交易日获取接口.md | 161 ++++++++++++++++++ 9 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 docs/开发文档/数据存储与缓存策略.md create mode 100644 docs/开发文档/最后交易日获取接口.md diff --git a/backend/service_implementation/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc b/backend/service_implementation/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc index d4ab71b9534d62a5dc4be4780f39027c82abfea1..1ba04d7ba6065de75bb7b7179e4ce1b73543e020 100644 GIT binary patch delta 2196 zcmaKtZ%k8H6u{qouPrSU%D=W?s}!V|E}|e%I{z>#4EZ7w++sktuJ#orBH(>xAOlN` z+gzpsxkk<0B08No{lHd}F^2Q+7WZXoSrgbxB>MoU*%mRIi6541=R9Cj#&-MqyQk-# zo_p@S=bm??hsdW%!o~RbSPt6#s~LhMA=zw*ug%r? zwW8pU^J~-5UTO6zG2@V`E(7%1dS0}FY!frLat?dQB&4U3OQAgT4W4+|^98-6oYKS?$mUH=!j(*IoX@uL+ug7QB)eJ z0c9(q25}70gXl%{0pz$wuN3gK&<3Ahq9;&&lD(bdB&R|nIsLq*4K=4jI~@o4U@OYJ zq5wQhMd|Or8cH`BAUe=Fbh5tD8}QG&r5$Ma7NQd%>sqMa<7;e|{B(91IC2JY7T{3P zcTj#8aTtLMatPF*tV01tF9`sf%|;W^W@jk4^0r{~pnMr1Yg=fu*y;;N^a|USw_5Wh zC_607U1SMe%x&$U-a!I5cz^<*yK15F-c^!VGZeXJf?;m_2ojTGsiRn*!7e zdm*d*?Y`agx{?+cA-#bRLd(~0Q4w88Dy<;KTd2B?P@?<>rEl5y?hK2v)&%%15y0He z$Aa9E%`#swv1J=e*h^_9o+B)5T(6>hC(qt5*)9kHHLEe%S+Bv!!mfl8A79IfLYHtr z*gDs@kv6VfsO8#-L)H2nIDc^Li>aOy(;s$E^&XoX>Yg0>baMFo^eHxd@&dCuv)NHy zRZt4hwYEh~9*rEC?jM;N>6siFwymgzmb-a#Zh85NSV$P9wOU}3$!pi|A0L<=I6M9R znK?i6leFUDJQu|L$kh4I9$dZfXyk)nl|xPSU?!*T@&`Q1HG*vx8=7``MDKp(MC2tN z9NXLM+w*S&<=kEe2a*baVa?`pJnpEi+#B##_yfM(e!6Ng|33u5O!^r9+0D%)$LWfG zGNvEjF=lj*8l8i0MvNsU*VB83v>A@q75#&I`lN8Ob1)wOPeflF)fb2L#p5+8=CPDDqbX}5 zDTUFL!mxhf^Ay^Q8hk-7VGK3^EEWk}6*1bWv8E<#Hgqv#!$}Y8g18dTl6NkX^ zBqtJ)Z{)g2Ct+K1SMkvGrI^x0;fm@LSodZ4&bAJy%>4p$8&Yke`uU+QKA@X7<2X^n zlGLVPCl94JMmheu<6Z3dl`2;?Tg`~9g5#7rwD#Iu``%o;*t!(F{!i|#wK>$XR)h)| zXn<>f2&`otilV=WH;^%u+XM zsS9uSM=bSGOMO_Ic2650PACnpDnTZcMzrpz)*aTm#|;@_Z3fLuhN-F?gjPcj-8Sra z%0Uzg9GPtY#?8UcK}|8c=|Kd(mFO#oS_D>bx&xs`coDB6>JYCXb|SEL(`yK&{QZei z49=uOU;(0dXDDtDRf-fw2bYPhD@)=_g-})5Q@+8JsxBHRnBkxsbPbhV-JpmIu~MV9 z4V2Ar(ETT<;vRF$VUYP|iH1OwH7(vI(JRq|tf``1qrAf=*e?}nB$KH(NtON>0v;FV WV@3b_v!gWzj--USb?!1NY#8*1 zvP=#cC5H4;C?R=Yw5&AWg1{#qE*L&U&_iTVPzl-j-dPwl#^-$ZJ7@R)&b>z)CbI*? z{xCH)*}y+ub8=wRF1uOsw}GX! z4e=-VZ2~xVv>zM-et=g;j{`Gk1&6^gZ~_Fu6qo_}=6@lHyc=o(R-kv8jtTu7Sdpv- zmQ$Z3tO;sS{dZxT!D>!k0aRayC32c3L=-y7qb5d!U2Q>y_OYw`d))fb`}d+hEtQ@8 m40VL7XclufD0O?22*2R}{jAr&Zk#=0H;~N>hV+eJy_MfNto^Y7 diff --git a/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc index b32ae97da4426a0053cb3d9535a9d851b06e6906..31db358435a39d162926c62d27f29d252e3fed99 100644 GIT binary patch delta 3016 zcmcImeN0=|6@S;?Uw|HB)3$q_ZNM4( zbI11Yo_o)^=bm%!d*}Ecr~WjDGJlehp;3Da{y^iA%?lYc$hxKGJOIT z^$FPm@&gS!Mt-Hu&lV3;?`izVSGQp?PZJcMq!l12NeZ(AQc@u)%#k=4UP+!Z#45@D+?*Mq zSC(1{3cY;ofH|wWBnU|Z`6(I0cvUGrD7{LPbA2O0Az7NHpvUO4!RxB8zU~KK1X@v%g$D_x-g? zr&njst}L8hS@_w?ohxe>$=ZeYNJx!m`~hq;ZYzFt_srV--POD2Ru=9U&ApT=J3A{| zTFi=M#+ZtIZEIFSdk3-fE?^NF~geQ@W)wevUE-u}fz4e7S#5~EH+Cavn3M7*g; zaSI%e;v&{H?Vl*5la&uXd@y%y?b?sm-nyJTETv43foReamy)esx%KJo_a5E-p+$># zQY8!y;mxLa9Hz9$%`lj+gRL;xUEHf3 zr1kwt*~y*G6ApHGKkFI~c<}D+RL;=K;r}VU;nU2zrbcdB&MNwX*_}~7Uwx&HXUIcC zrNK~iNw=u4it4Lw)-QOt=N+8BDx!DB^iI-Na9&eba_JlEVxdm6&QO>YJ(dyaj*6Le z1*0>}O|3JMY~zyIa&6a@Q!%q`p(^v^-xrn+ertad@U7x6nXS3k5 zWO_Pg+PgSos*jrLBc_I!sbS&eMSFMD-p##ml(Tn7>^(7iPsG%-SaLjCa-4IIMoN6K z5+A2FE~|67yhB{gLBPC25p{b^-Oj1om-5Y=+WggWmX3xaH*LbQDx1^P+}uNfej}>d zn5vdj)qbKXn0ccMK1;^(m~qddac|VPH)5=f8EY5H7i}F;TL*We`gdy~YDMboi}03vlOt3mraED%w@Tk8<}v-7Xd0M~F`DOT`pPW&P(8_|Je3 zkwV)}M94my8U29_+9oqP=&}W2KMV%)(AL=j{UY(IK=a%3KMcb(1OOcuzXbq&9)AyD z4&XAtI{>!;2$^XpLc7B6Hv9#d_lZr{tm_ONZq_Y4bGwOWpM(Pf5^Ac?q^~CZGcxY- su`WLzB)@5zMjsHnU5DA=%fn_5nc0f0t9m%9CgHwV zN&4ZX_$jZOm8ctOf@hA3N-u`}3^P-UsA_^9$HX8qgh%Eo|W3{%CCu^`?KMfySKMS}!+5zj{GLnKH-h*TjTy5v< z$5>h-G=V=3xZBp-(S#GNKPr#W)(4N{@H}Ee7LZKj8gc` Optional[str]: + """获取最后一个交易日 + + Args: + symbol: 合约代码,如 'CU2603' + + Returns: + str: 最后一个交易日,格式为 'YYYY-MM-DD' + """ + try: + if TQSDK_AVAILABLE and self.api: + # 转换合约代码为TQSDK格式 + tq_symbol = self._convert_symbol(symbol) + print(f"使用TQSDK格式合约代码: {tq_symbol}") + + # 获取日线数据 + klines = self.api.get_kline_serial(tq_symbol, 86400, data_length=30) # 86400秒 = 1天 + + # 等待数据准备就绪 + import time + start_time = time.time() + timeout = 5 # 5秒超时 + + while True: + if hasattr(klines, 'datetime') and len(klines.datetime) > 0: + break + if time.time() - start_time > timeout: + print("获取K线数据超时") + return None + time.sleep(0.1) + + # 转换为DataFrame + data = { + 'datetime': klines.datetime, + 'close': klines.close + } + df = pd.DataFrame(data) + df['datetime'] = pd.to_datetime(df['datetime'], unit='ns') + + # 过滤掉成交量为0的日期(非交易日) + # 注意:TQSDK的K线数据中,如果当天没有交易,可能不会生成数据 + # 所以我们只需要取最后一条数据的日期 + if not df.empty: + last_trading_day = df['datetime'].iloc[-1].strftime('%Y-%m-%d') + print(f"最后一个交易日: {last_trading_day}") + return last_trading_day + else: + print("无法获取K线数据") + return None + else: + # 返回模拟数据 + print("无法获取真实数据,使用模拟最后交易日") + # 模拟返回前一天的日期 + import datetime + last_trading_day = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') + print(f"使用模拟最后交易日: {last_trading_day}") + return last_trading_day + except Exception as e: + print(f"获取最后交易日失败:{e}") + # 返回模拟数据 + import datetime + last_trading_day = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') + print(f"使用模拟最后交易日: {last_trading_day}") + return last_trading_day + def _get_mock_all_symbols(self) -> List[str]: """获取模拟品种列表""" # 返回exchange_map中映射的所有品种 diff --git a/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py b/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py index 0dd3543..380ccb1 100644 --- a/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py +++ b/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py @@ -463,6 +463,36 @@ class DataFetcher: 'NI': 'NI2603', # 镍 'SN': 'SN2603' # 锡 } + + def get_last_trading_day(self, symbol: str) -> Optional[str]: + """获取最后一个交易日 + + Args: + symbol: 合约代码,如 'CU2603' + + Returns: + str: 最后一个交易日,格式为 'YYYY-MM-DD' + """ + try: + # 使用适配器的get_last_trading_day方法 + result = self.adapter.get_last_trading_day(symbol) + if result: + return result + else: + # 如果适配器返回空,使用模拟数据 + print("使用模拟最后交易日") + return self._get_mock_last_trading_day() + except Exception as e: + print(f"获取最后交易日失败:{e}") + return self._get_mock_last_trading_day() + + def _get_mock_last_trading_day(self) -> str: + """获取模拟最后交易日""" + # 模拟返回前一天的日期 + import datetime + last_trading_day = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') + print(f"使用模拟最后交易日: {last_trading_day}") + return last_trading_day # 导入numpy diff --git a/backend/service_implementation/service/app.py b/backend/service_implementation/service/app.py index dc3b923..da88e51 100644 --- a/backend/service_implementation/service/app.py +++ b/backend/service_implementation/service/app.py @@ -68,6 +68,22 @@ def get_main_contracts(): except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 500 +# 最后交易日获取接口 +@app.route('/api/last-trading-day', methods=['GET']) +def get_last_trading_day(): + try: + symbol = request.args.get('symbol', 'CU2603') # 默认使用CU2603合约 + print(f"正在获取最后交易日,合约:{symbol}") + last_trading_day = data_fetcher.get_last_trading_day(symbol) + print(f"获取到最后交易日:{last_trading_day}") + + return jsonify({'status': 'success', 'data': { + 'symbol': symbol, + 'last_trading_day': last_trading_day + }}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + # K线数据获取接口 @app.route('/api/kline', methods=['GET']) def get_kline(): diff --git a/backend/service_implementation/service/data/futures_analysis.db b/backend/service_implementation/service/data/futures_analysis.db index fa05b4bc6db5aecc449fc501a83d1a2ce2ccef71..d81896dad637cf5f6a59700dbd799e51d65f04ba 100644 GIT binary patch delta 7735 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 delta 2476 zcmZA2dsGuw9tZH5$;@QPgJcwW$is*h6%axZ6nx^Nz6B~)Z7ZUvYpqpmtxA1GS-`rA zAeG-%gG7Ad1Az&dWJBB7-R`M-JbO@TEz+~1d%DNAT0z0qy6S#||Lq^2oI8`bcYgPG z@9&Py)-Bi^T|#-d5Xvz6OBI%hY-nTN=0r}KTG^f$!!k|QEm73!junkbNhzsTD^vV? z0iL98DKCl&#w7|${k$NS%pVr=RGJ@cENaUvZOdeV0}b#XfCyTUAOLir2Ll)(5KLfJ z+cM>(ENL&g9?K?Ei$fkk=i{A0!l*P((?!o6@(e0Za7yaDWAY-7 zV;DMfTwY53y>bGl`H8Y=LIh3R7AsQM1fwW2#dk3#W!FieWIh-qQe2Qxua=#V>v^_@ zj>S78DX(`eujbat=S7Y=O6CUnD6co`?x^(*vZSNz7CxLhuLTHHk{>;e6)F3_Z--a< zGW(ZicxC7Z{&o-yfM5uLPzZy85DpO#2~jWzENX^#@Lx|z^O!FXApbp?^P;OY?V5Ay z`Ml^bj+XV)h0_C066rt7qqmdq=SV%-`s;X#EIdB1)R!L(LtrSp0x>WQhQkPmg*b?Z zk&pn1V1*=j6_Q~Tq(CZ+hB1%^=`a??!FZSe6JZi$z+{*LQ(+ps2Gd~%%!FAm8|J`V z@W-49S&$8{!+gkr1@Hzeghj9z-h{W{ZCC=iuoUuO87zl&j5_70Dt!4>IXR=Hmc{5nuM0R4)otaDOyk(qS*@ zs4nR^TGCNd(&2D*9OF+)M>N&g#`;UD!B(59pbJVJ$+WtB=Mm@W~Hj)(rLib)S zEWJ`z3&-IEoP<+w8qPo+oP~369xgyVT!aQ_geLV$nUb=MH>5BtNw7FSi!g36<`}I; zgW;~>Zw7~9n=#2~H2i4z%5coEy&@x+dMwT`niS>c^{M`xot?&>Vb)T29PSz)Dq0?w zGyInp${Xx|vPb3V(nDn^Wj|5|_obC;zElQP-STK@WREh4^6I<-86PQA$SgOB6x*vr zQOhG`4P`x5wN&v~iKMc5O%fgMQT9;oZ%Pb(*Q+=w>no3-K6|XJVAbN^lwywRA1gCn zj&FLZh}3vojv(Jtg;z&CRYEj$;Ds`mM^>l%gA1k3IZdK?c09wdQ~H7zNB7N!WE^7Z zkKIZn>mN&0=R8*?ur%lqLMimeIHyOMNi%zu51dZc(x1D>tY&&x6Cc=VNHNIz`})fi zZE^0Q*iO%Q_FhHCK-#ybI#QKydtPN}-_IVICf>z#7e_k-`_5t7ddIVfmegJ_(T)I@ zB%a-E8qN%*UGdIv(*N6I?(3t*4{@pzt^W=w`)s81BY)2UYT-SPjq6*9s#9K`<|a6Y zQD?n7n6f5c;Hfjxt)n5M2VL$+~GDHZI zlQr}&OG7Z_nOqWCuQf}&set*KuIkMLsH=+;DOq$KrGz+VfclTC&8LhMRNFj|0=pGS zJ$S2my+zCX%H%U`3}rdgH)EJK_Bz|h?q}a&r?G>XXQuB>Zqp~+E^Y~z#tkqPnC3{0 zQVAJsXDq{obAnA+AjI-7_;2_+eg{9FAFg?(xu!X-*`~?X#Bfi!R_>$_M%@-~7!{;e z^Yl}IODC{T7%_vgzd^bBCy9@$>lTmvMfigb=B(-dQVx6hpVir&iVO(=OMhX#<--cgG7H zx0I4zc*E5Dz23T1dJ=?(wt=5k*%9S;eXj z-%cGipnos^aL4mG=ifs8w4>6KtsEcL_vKdGfAA>W%Vyn}mWA4qE|070FRaf$W)@e@ z(5wdUR8Dx8vO48>)n>`5VX4)u-Av`n$A(bX;!%=%vB`U#t2!+3RCCiFNUsN`hg3}y zL{(|_;*}5YZVjol{ZmxSLR&UzPxC&nS<_EHi?o(=uc#Hio$vAfFmwBU=X2bo-PB#3 z7fFtrUP;~1;jJ8M7L8XJQ;lhA;Df-jz>&r)|KBT@_eXSqUvl7yff|LDAyh85@anI| HmH_U5kk~2s diff --git a/backend/src/services/marketService.ts b/backend/src/services/marketService.ts index 19f0f62..1ba324f 100644 --- a/backend/src/services/marketService.ts +++ b/backend/src/services/marketService.ts @@ -316,7 +316,7 @@ export const fetchKlineData = async (symbol: string, period: string) => { // 获取K线数据 logger.log(`获取合约${contractSymbol}K线数据,周期: ${duration}...`); - const klineResponse = await serviceImplementationClient.getKlineData(contractSymbol, duration, 30); + const klineResponse = await serviceImplementationClient.getKlineData(contractSymbol, duration, 50); const klineData = klineResponse.data; if (klineData.length > 0) { diff --git a/docs/开发文档/数据存储与缓存策略.md b/docs/开发文档/数据存储与缓存策略.md new file mode 100644 index 0000000..cdbe97f --- /dev/null +++ b/docs/开发文档/数据存储与缓存策略.md @@ -0,0 +1,123 @@ +# 数据存储与缓存策略 + +## 1. 缓存服务 (Redis + MySQL) + +### 代码位置 +- **文件**:`backend/src/services/cacheService.ts` +- **核心方法**: + - `set(key, value, options)`: 写入Redis缓存 + - `saveToMySQL(symbol, type, data)`: 写入MySQL数据库 + +### 写入场景 +1. **数据首次获取**: + - 当从数据源获取新数据时,会同时写入Redis和MySQL + - 代码路径:`cacheService.get()` → 数据源获取 → `set()` + `saveToMySQL()` + +2. **MySQL数据同步到Redis**: + - 当从MySQL获取数据时,会将数据同步到Redis + - 代码路径:`cacheService.get()` → MySQL获取 → `set()` + +## 2. 市场服务 (使用缓存服务) + +### 代码位置 +- **文件**:`backend/src/services/marketService.ts` + +### 写入场景 +1. **市场概览数据**: + - 方法:`fetchMarketOverview()` + - 缓存键:`market:overview` + - 过期时间:5分钟 + +2. **品种详情数据**: + - 方法:`fetchMarketDetail(symbol)` + - 缓存键:`market:detail:{symbol}` + - 过期时间:5分钟 + +3. **K线数据**: + - 方法:`fetchKlineData(symbol, period)` + - 缓存键:`market:kline:{symbol}:{period}` + - 过期时间:10分钟 + +4. **市场热点数据**: + - 方法:`fetchMarketHotspots()` + - 缓存键:`market:hotspots` + - 过期时间:5分钟 + +## 3. Python服务数据存储 (SQLite) + +### 代码位置 +- **文件**:`backend/service_implementation/qihuo_analyzer/data/data_storage.py` + +### 写入场景 +1. **分析结果**: + - 方法:`save_analysis_result(result)` + - 表:`analysis_results` + - 场景:当AI分析完成后保存分析结果 + +2. **K线数据**: + - 方法:`save_kline_data(symbol, duration, df)` + - 表:`kline_data` + - 场景:当获取K线数据后保存到数据库 + +3. **交易建议**: + - 方法:`save_trade_recommendation(recommendation)` + - 表:`trade_recommendations` + - 场景:当生成交易建议后保存 + +4. **风险监控数据**: + - 方法:`save_risk_monitoring(monitoring_data)` + - 表:`risk_monitoring` + - 场景:当监控交易风险时保存 + +## 4. Flask API服务 (调用Python存储) + +### 代码位置 +- **文件**:`backend/service_implementation/service/app.py` + +### 写入场景 +1. **K线数据**: + - 路径:`/api/kline` + - 代码:当数据库中没有K线数据时,从数据源获取并保存 + - 调用:`data_storage.save_kline_data()` + +2. **分析结果**: + - 路径:`/api/analyze` + - 代码:当AI分析完成后保存分析结果 + - 调用:`data_storage.save_analysis_result()` + +3. **风险监控数据**: + - 路径:`/api/risk` + - 代码:当监控交易风险时保存 + - 调用:`data_storage.save_risk_monitoring()` + +## 5. 数据流动路径 + +### 前端 → 后端 API → 缓存服务 → 数据源 +1. 前端请求数据 +2. 后端API调用市场服务 +3. 市场服务调用缓存服务 +4. 缓存服务按优先级获取数据: + - Redis → MySQL → 数据源 +5. 如果从数据源获取数据,同步写入MySQL和Redis + +### Python服务内部 +1. Flask API接收请求 +2. 调用数据获取器获取数据 +3. 将数据保存到SQLite数据库 +4. 返回数据给前端 + +## 6. 缓存策略 + +- **Redis**:短期缓存,用于高频访问数据 + - 市场概览:5分钟 + - 品种详情:5分钟 + - K线数据:10分钟 + - 市场热点:5分钟 + +- **MySQL**:持久化存储,用于长期数据 + - 存储所有从数据源获取的数据 + +- **SQLite**:Python服务本地存储 + - 存储分析结果、K线数据、交易建议、风险监控数据 + +这种多层缓存策略确保了数据的快速访问和持久化存储,同时减少了对外部数据源的请求频率。 \ No newline at end of file diff --git a/docs/开发文档/最后交易日获取接口.md b/docs/开发文档/最后交易日获取接口.md new file mode 100644 index 0000000..eacd90a --- /dev/null +++ b/docs/开发文档/最后交易日获取接口.md @@ -0,0 +1,161 @@ +# 最后交易日获取接口 + +## 1. 功能概述 + +本接口用于获取期货合约的最后一个交易日,支持通过TQSDK实时获取或使用模拟数据作为fallback。 + +## 2. 实现方案 + +### 2.1 核心代码位置 + +- **TQSDK适配器**:`backend/service_implementation/qihuo_analyzer/data/api_adapters/tqsdk_adapter.py` + - 实现了`get_last_trading_day(symbol)`方法,使用TQSDK获取最后一个交易日 + +- **数据获取器**:`backend/service_implementation/qihuo_analyzer/data/data_fetcher.py` + - 实现了`get_last_trading_day(symbol)`方法,作为适配器方法的包装 + +- **API服务**:`backend/service_implementation/service/app.py` + - 添加了`/api/last-trading-day`接口,提供HTTP访问方式 + +### 2.2 技术实现 + +#### TQSDK适配器实现 + +```python +def get_last_trading_day(self, symbol: str) -> Optional[str]: + """获取最后一个交易日 + + Args: + symbol: 合约代码,如 'CU2603' + + Returns: + str: 最后一个交易日,格式为 'YYYY-MM-DD' + """ + try: + if TQSDK_AVAILABLE and self.api: + # 转换合约代码为TQSDK格式 + tq_symbol = self._convert_symbol(symbol) + + # 获取日线数据 + klines = self.api.get_kline_serial(tq_symbol, 86400, data_length=30) # 86400秒 = 1天 + + # 等待数据准备就绪 + # 处理数据... + + # 转换为DataFrame并提取最后交易日 + if not df.empty: + last_trading_day = df['datetime'].iloc[-1].strftime('%Y-%m-%d') + return last_trading_day + else: + # 返回模拟数据 + import datetime + last_trading_day = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') + return last_trading_day + except Exception as e: + # 异常处理,返回模拟数据 + import datetime + last_trading_day = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') + return last_trading_day +``` + +#### API接口实现 + +```python +# 最后交易日获取接口 +@app.route('/api/last-trading-day', methods=['GET']) +def get_last_trading_day(): + try: + symbol = request.args.get('symbol', 'CU2603') # 默认使用CU2603合约 + last_trading_day = data_fetcher.get_last_trading_day(symbol) + + return jsonify({'status': 'success', 'data': { + 'symbol': symbol, + 'last_trading_day': last_trading_day + }}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 +``` + +## 3. 使用方法 + +### 3.1 API接口调用 + +**请求URL**:`http://localhost:5000/api/last-trading-day` + +**请求方法**:GET + +**参数说明**: +- `symbol`(可选):合约代码,如 'CU2603',默认值为 'CU2603' + +**返回格式**: + +```json +{ + "status": "success", + "data": { + "symbol": "CU2603", + "last_trading_day": "2026-02-23" + } +} +``` + +**示例请求**: +``` +GET http://localhost:5000/api/last-trading-day?symbol=AU2603 +``` + +**示例响应**: +```json +{ + "status": "success", + "data": { + "symbol": "AU2603", + "last_trading_day": "2026-02-23" + } +} +``` + +### 3.2 代码调用 + +```python +from qihuo_analyzer.data.data_fetcher import DataFetcher + +data_fetcher = DataFetcher() +data_fetcher.connect() +last_trading_day = data_fetcher.get_last_trading_day('CU2603') +print(f"最后一个交易日: {last_trading_day}") # 输出:最后一个交易日: 2026-02-23 +``` + +## 4. 技术特点 + +1. **实时数据**:通过TQSDK获取实时的最后交易日数据 +2. **容错机制**:当TQSDK不可用时,自动使用模拟数据 +3. **异常处理**:实现了多层异常捕获,确保接口稳定性 +4. **灵活调用**:支持通过API接口或直接代码调用 +5. **默认值**:当未指定合约代码时,默认使用CU2603合约 + +## 5. 注意事项 + +1. **网络连接**:使用TQSDK获取数据需要稳定的网络连接 +2. **合约代码格式**:请使用正确的合约代码格式,如 'CU2603' +3. **数据延迟**:由于需要从TQSDK获取数据,可能会有一定的网络延迟 +4. **模拟数据**:当TQSDK不可用时,会返回前一天的日期作为模拟数据 + +## 6. 故障排查 + +### 6.1 常见问题 + +| 问题 | 可能原因 | 解决方案 | +|------|---------|--------| +| 返回模拟数据 | TQSDK连接失败或网络问题 | 检查网络连接,确保TQSDK账号配置正确 | +| 接口返回错误 | 合约代码格式错误 | 使用正确的合约代码格式,如 'CU2603' | +| 数据延迟 | 网络延迟或TQSDK服务器响应慢 | 耐心等待,或使用模拟数据 | + +### 6.2 日志查看 + +- **Python服务日志**:在启动Python服务的终端中查看 +- **API日志**:通过访问API端点,查看返回结果和错误信息 + +## 7. 总结 + +本接口提供了一种便捷的方式来获取期货合约的最后一个交易日,支持实时数据和模拟数据两种模式。通过简单的API调用或代码调用,用户可以轻松获取所需的最后交易日信息,为交易策略和风险管理提供支持。 \ No newline at end of file