From 2073f3966f3caee73a9ff0ff7b5016cb73fe797c Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Tue, 5 Aug 2025 15:30:50 +0800 Subject: [PATCH] complete market data monitor main logic --- .../__pycache__/huge_volume.cpython-312.pyc | Bin 17275 -> 16955 bytes .../__pycache__/market_data.cpython-312.pyc | Bin 0 -> 12738 bytes core/biz/huge_volume.py | 48 +- ...{market_data_monitor.py => market_data.py} | 78 +++- core/biz/market_monitor.py | 249 ++++++++++ .../db_huge_volume_data.cpython-312.pyc | Bin 21027 -> 30364 bytes core/db/db_market_monitor.py | 439 ++++++++++++++++++ huge_volume_main.py | 4 +- market_data_main.py | 97 ++-- market_monitor_main.py | 164 +++++++ metrics_config.py | 166 +++++++ play.py | 4 +- sql/query/sql_playground.sql | 2 +- sql/table/crypto_market_monitor.sql | 13 + 14 files changed, 1182 insertions(+), 82 deletions(-) create mode 100644 core/biz/__pycache__/market_data.cpython-312.pyc rename core/biz/{market_data_monitor.py => market_data.py} (69%) create mode 100644 core/biz/market_monitor.py create mode 100644 core/db/db_market_monitor.py create mode 100644 market_monitor_main.py create mode 100644 metrics_config.py create mode 100644 sql/table/crypto_market_monitor.sql diff --git a/core/biz/__pycache__/huge_volume.cpython-312.pyc b/core/biz/__pycache__/huge_volume.cpython-312.pyc index 8f72f6f78a12ce55168352f6029772e47555f3d4..95af9d0f75023cce41492b8f421e8ae57978fff8 100644 GIT binary patch delta 3286 zcmai0d2Ce28K2p=Z|~Dydrz;|XS_D}zA!eyoCZvb4WR`Ymc{S+Vtw#-eMGfmlAs7$ z+N>EOn-I#9wkZ*zapFIU8q!*-qM}f#UZ7(2gj6CWE|FUGf!jt+{^@*gy(TzHJFES@ z-+b51oB6((_th)-gOk{F%V^Xg+P1tu)$`WY_e^12?8OL8BOV>Z96G4rGC0gLT?$Td zl0B&87*5%ZIQC84#c(PzsIX_Lc{Qiu8BWVDMpElT~k zOisnCIPf&^I?l)&I1~Mw>0ieic@_9}nK%peGI0zY&q~KrB*(LYR~j8log9JJ(lIj0 zHAS{Io!Xn9xB2aJe^zY6hR-W$7?JG^4TgE*R<^erhJ@ZuKF~QB8txwu^+UYS$qz(& zLws0N34CN&80ciFORA|UVd#IUT3(QGFKLdc!Z*@MiNw+oXAIF7Vr{XJSkD!H zA*1@XrA9LRp9tR?fLq9@x^1bJvglh$`N(2Br5EhA)s|=b`YI4ZrsvTEfg`}fx z0-v$d4P*s9%>m4rllgwEC|O$*I08E-w>}S<_qh~(+2pt(-R?}qr%tY9?nSZSA=uz1 z#b%$=ozf>~My)&$y3YsRWP?Se*E}st&jS5~HUbGDx4wL^UF+qtBhRE{rRQ?Tb+o$j#`Q8PWYoy0rI>$FKVi_Kh(9&EzGCu7 zlb*e1rC*w2Po8}hBxp)!P+uiHorbHl(xIhhS%W5wuKmOIH*RTEc#&15G zodWr1r>6gLkEl#93SC3$xobXRIp>=*137-5!{_HXUy!o)N#HeZuZ}|*r$zC#szQNT}yky z?vCmcX4}k`OReWy759$dG?C^FL|#8}($gA8}V=9u0+wL4Zgd-!HeboX6b+ZSq3NhbN^ zITbmj|H2Fca?aDx@UgyWS1r1w$=Ky*Ze?kJ``KMp>RY7@@G2Q=wXM0#t!EvrSYPQk)i|$jS&h2z06DqK-JWx>{NZyL!q26OW$<}$d8j}L5gk%zzZ%t}Q z6GUjDG4=@?DEhIepp(pqjycNqlc>t>e^!yzT zMTD*7NYzJrADx159Kb`$t4pzi1gd>y^g=|R?*XY7*g;?u6y8R{N%{!G4egV&L3o8y z@@{4Q;YChCIDaY@V-`7?-VU0vq7#tqpM7Aq5 z7>vLP(J8bObDe!t5FGb|i7GfWL~Bsk3mv9EI7Xp&KsXErM*-m4^1D|Tw?KO8f{u1@ zVn>3ZVP4eIdJYC6eE*O@$R~A&3t{c)cN(2RMEEI0dIKN^a30`wfD44LA5mSVG`vJU zssEdfF0KPtMB<6;{Jv^{>CL#XLZeW zG?g-DN**)RK>99*p~(4-6=@DSw{H4}wsG6GZGH>6zp-Gmyn>lj_GFckry+b!3GneP zybVxGK~x?c91IC6==THs7T_$|(d?Pb#t5B6j}!{FF4nn(=!2dT9&f^vCzf?S()(E3 z1C!;|(CLuGQchPV(T=iUEEa?&sZaYAFVZ=;Z@IR&yo>( zJ28ZH)5tlAqP2;v+(+hK%wDu6^)|X#fELIhB;a4v?FhHo49MwFAHz{-vg|e~=pv@f(Cc Sko?Up<~A0ueT)EQqyGX*RA89^ delta 3412 zcmbVPdr(x@89!(5-n%ar7WT!$0t>pxiXfmM0tWGcph;SZP2;QUhI>(PdF1S(#zHiS ziI_O!nvBuk1}u_#uw@j)PbWD~zhy%w`<1|OSl>*Kem zr`;%9ju^LPSV;O$cFRUZk}-}=vC0{WUADuM36Dw1R3wbnl__UIoGjT2*&UGGkjkEo zUfGb{l**}CAghND8;UGBVDEU@8S+g1!q7_jH$ulc+knD4mA-g)REen)Q{scHuhR(O z(@g>*z%v)n-+xG0M|O@7vco{guyI%%qB7<8{JGK)OvQ%+|DCa**?1mIv4PS!B%UU+ zI3_e{n z5h6cFLe{BEqC`!q*ct7QDg265P$ua#tlGj4-#cVrn4A>=sL}xI?o`73Td68XVlI>i z5Q-5>_-a$G5od(&G=&d+3gj@E%OeXalNp608%HHgBH8Z5k5AfX9Koa`II;b5WZDtD z;b@z2Y)(2hPj~FN;n=AO#Gaj~8!a8p9nYW6u1IE7YF6U#jQW#VL5Se?je1W$Grnb_ zb+T@<>|*)!;?`tvL(;Y3W=`(O(lbt8u8ElD`F*2Zqc4pAXxd%=nPZ8T@jpAJ{I*QH z7k}m`*9u_x+Ct)Rjo6qMU+uENQ+E}}DO?|Yon*t_GJUlKy`<7TZAKK{n-#Q8_GlXs3Ey)%B4U#N8FsJ1E69Z%}8~ttHRO#{ruCVd554}sv!)k0RaA>L}IWb zNFc!6@S_%nJ0sD~!Du9&&i!bT*uhX6jOuxPP?i7Pv=+j}IKW||IY?&Cp+rT}9vE*) z+JmzWS0apMmYsAgo`_FAn_Rl~@=M93ZON?mdjhp@qF-4Z$4cLIzFn@Fh{Zl_Uz9YL zX_*k^{}1Atj{+5&u9&+u{G9m(!zbdD(c&}EHeP7kXdEIvK7Q8d%%($z^EfrohABw% zQEH>nqvFuesVzAO7_#x!l{m|plH+2LflGh5r56KL}V*~^GmJ(4f4Ob z8u*y|bwQLB+YZRcZ$k zlcsKZchL|WwXWP5XMNIHf6;aEr#GCNM%K;C>XOd7$@mRt)5y9nY$a2_^!cgywP5h% z@W14Fo$w2r2+MyYK4SYO3+3PDH8lL$x@u!Bxn^>2EEBHz&B)8djWwog6$0`a9cwL{ ziiK-Wdh=Ucn{XOg1Hvi3)~QPn83{sD$% zt%`lhfp}Pz!utKjN_l(Lzi2mKU7bf&zP-AI{+*9i7cU@^+w--V_zT~!|9Sm*%bCWB z>WP=m)xwSBE7DA;wDK>iT>%?VxRch=^;AQyUks`#%#_$*G|ryk?wUW^3UNRO5c2r# zno8QtUCRPXazSwShbZ+U+lK5FfL{@YXL<}j4N`|BJIZ%0YdrKFO2+^WlhKxO=X-65 z7d2w^*3M=*5^LW&0Ank!NC_4!oDe>!AMMZtP~Uo>u34TEJWz|EhJ|!w&-;VHyl7!? zK7t8N%xJPGl>=<%We;pDDVv&F4H<7-Uf#YLhN%jXXf$N}-sQn_j=hHPD#GgsdTn+f z!vfVSTP@4${=q(36}zJSk+>>E`#afI{`*?*nmrg{ zCz?o+fdMFghV!cmy^#X|-F-|!gKh*o#X|W{ZmE~Kw=R;q6`jO=k?5eJT3{wvf;f90BjMVxvk31YoJKgqT`TrUzX2Lc^2CaNh;tJ07VO}i&sPSTVG6df-rLd$WHGHfwD12VHi(hYS@sgqlg}Tfy zNl3Gy0Y8)P;MpGj1&H2{y7`=*3cZ$HCgj<(}gCD0KL5CPfuTYdJd^p3A=VSzi`%H zJX=;VTTpT@OQx94tBFYTTR!`XxW>$9bY*Ip$kE;DEzO0?cyUXYw{kSAl2N|+b<7Bm8aMz{L7ZMtguKc K?h{0P7XJ-oRc){U diff --git a/core/biz/__pycache__/market_data.cpython-312.pyc b/core/biz/__pycache__/market_data.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ab8bbed0eb9c48a5937b2328cfa16594afb2179 GIT binary patch literal 12738 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 vA(nhXlzu{Nj1lIu>Pf;JCCr267gr9g pd.DataFrame: """ @@ -32,44 +33,24 @@ class HugeVolume: """ for percentile, suffix in percentiles: # 计算分位数 - data[f"close_{suffix}_percentile"] = ( - data["close"].rolling(window=window_size, min_periods=1).quantile(percentile) + data[f"{price_column}_{suffix}_percentile"] = ( + data[price_column].rolling(window=window_size, min_periods=1).quantile(percentile) ) # 判断价格是否达到分位数 if suffix in ["80", "90"]: # 高点分位数 - data[f"price_{suffix}_high"] = ( - data["close"] >= data[f"close_{suffix}_percentile"] + data[f"{price_column}_{suffix}_high"] = ( + data[price_column] >= data[f"{price_column}_{suffix}_percentile"] ).astype(int) else: # 低点分位数 - data[f"price_{suffix}_low"] = ( - data["close"] <= data[f"close_{suffix}_percentile"] + data[f"{price_column}_{suffix}_low"] = ( + data[price_column] <= data[f"{price_column}_{suffix}_percentile"] ).astype(int) return data - def _calculate_volume_price_spikes(self, data: pd.DataFrame) -> pd.DataFrame: - """ - 计算量价尖峰指标 - :param data: 数据DataFrame - :return: 包含量价尖峰指标的DataFrame - """ - # 80/20量价尖峰 - data["volume_80_20_price_spike"] = ( - (data["huge_volume"] == 1) - & ((data["price_80_high"] == 1) | (data["price_20_low"] == 1)) - ).astype(int) - - # 90/10量价尖峰 - data["volume_90_10_price_spike"] = ( - (data["huge_volume"] == 1) - & ((data["price_90_high"] == 1) | (data["price_10_low"] == 1)) - ).astype(int) - - return data - def detect_huge_volume( self, data: DataFrame, @@ -136,12 +117,17 @@ class HugeVolume: if "close" not in data.columns: logging.error("数据中缺少close列,无法进行价格检查") return data + if "high" not in data.columns: + logging.error("数据中缺少high列,无法进行价格检查") + return data + if "low" not in data.columns: + logging.error("数据中缺少low列,无法进行价格检查") + return data - # 计算分位数指标(80/20和90/10) - data = self._calculate_percentile_indicators(data, window_size) + for price_column in ["close", "high", "low"]: + # 计算分位数指标(80/20和90/10) + data = self._calculate_percentile_indicators(data, window_size, price_column) - # 计算量价尖峰指标 - data = self._calculate_volume_price_spikes(data) if only_output_huge_volume: data = data[(data["huge_volume"] == 1)] diff --git a/core/biz/market_data_monitor.py b/core/biz/market_data.py similarity index 69% rename from core/biz/market_data_monitor.py rename to core/biz/market_data.py index 2ddcdea..08a2555 100644 --- a/core/biz/market_data_monitor.py +++ b/core/biz/market_data.py @@ -8,7 +8,7 @@ import okx.TradingData as TradingData from core.utils import transform_date_time_to_timestamp logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s: %(message)s') -class MarketDataMonitor: +class MarketData: def __init__(self, api_key: str, secret_key: str, @@ -23,8 +23,55 @@ class MarketDataMonitor: api_key=api_key, api_secret_key=secret_key, passphrase=passphrase, flag=flag ) + + def get_realtime_kline_data(self, symbol: str = None, bar: str = '5m', end_time: int = None, limit: int = 50) -> Optional[pd.DataFrame]: + """ + 获取实时K线数据 + """ + if symbol is None: + symbol = "XCH-USDT" + if bar is None: + bar = "5m" + + if end_time is None: + end_time = int(time.time() * 1000) # 当前时间(毫秒) + else: + end_time = transform_date_time_to_timestamp(end_time) + if end_time is None: + logging.error(f"end_time参数解析失败: {end_time}") + return None + response = self.get_realtime_candlesticks_from_api(symbol, bar, end_time, limit) + if response: + candles = response["data"] + from_time = int(candles[-1][0]) + to_time = int(candles[0][0]) + from_time_str = pd.to_datetime(from_time, unit='ms', utc=True).tz_convert('Asia/Shanghai') + to_time_str = pd.to_datetime(to_time, unit='ms', utc=True).tz_convert('Asia/Shanghai') + logging.info(f"已获取{symbol}, 周期:{bar} {len(candles)} 条数据,从: {from_time_str} 到: {to_time_str}") + columns = ["timestamp", "open", "high", "low", "close", "volume", "volCcy", "volCCyQuote", "confirm"] + candles_pd = pd.DataFrame(candles, columns=columns) + for col in ['open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCCyQuote']: + candles_pd[col] = pd.to_numeric(candles_pd[col], errors='coerce') + dt_series = pd.to_datetime(candles_pd['timestamp'].astype(int), unit='ms', utc=True, errors='coerce').dt.tz_convert('Asia/Shanghai') + candles_pd['date_time'] = dt_series.dt.strftime('%Y-%m-%d %H:%M:%S') + # 将timestamp转换为整型 + candles_pd['timestamp'] = candles_pd['timestamp'].astype(int) + # 添加虚拟货币名称列,内容为symbol + candles_pd['symbol'] = symbol + # 添加bar列,内容为bar + candles_pd['bar'] = bar + candles_pd['create_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + candles_pd = candles_pd[['symbol', 'bar', 'timestamp', 'date_time', 'open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCCyQuote', 'create_time']] + candles_pd.sort_values('timestamp', inplace=True) + candles_pd.reset_index(drop=True, inplace=True) + + return candles_pd + else: + logging.warning(f"未获取到{symbol}, {bar} 最新数据,请稍后再试") + return None + - def get_historical_kline_data(self, symbol: str = None, start: str = None, bar: str = '1m', limit: int = 100, end_time: int = None) -> Optional[pd.DataFrame]: + def get_historical_kline_data(self, symbol: str = None, start: str = None, bar: str = '5m', limit: int = 100, end_time: int = None) -> Optional[pd.DataFrame]: """ 获取历史K线数据,支持start为北京时间字符串(%Y-%m-%d %H:%M:%S)或UTC毫秒级时间戳 :param symbol: 交易对 @@ -56,7 +103,7 @@ class MarketDataMonitor: while start_time < end_time: try: # after,真实逻辑是获得指定时间之前的数据 !!! - response = self.get_candlesticks_from_api(symbol, end_time, bar, limit) + response = self.get_historical_candlesticks_from_api(symbol, bar, end_time, limit) if response is None: logging.warning(f"请求失败,请稍后再试") break @@ -151,15 +198,15 @@ class MarketDataMonitor: df.loc[index, "sell_sz"] = sell_sz return df - def get_candlesticks_from_api(self, symbol, end_time, bar, limit): + def get_historical_candlesticks_from_api(self, symbol, bar, end_time, limit): response = None count = 0 while True: try: response = self.market_api.get_history_candlesticks( instId=symbol, - after=end_time, # 获取指定时间之前的数据, bar=bar, + after=end_time, # 获取指定时间之前的数据, limit=str(limit) ) if response: @@ -172,6 +219,27 @@ class MarketDataMonitor: time.sleep(10) return response + def get_realtime_candlesticks_from_api(self, symbol, bar, end_time, limit): + response = None + count = 0 + while True: + try: + response = self.market_api.get_candlesticks( + instId=symbol, + bar=bar, + after=end_time, + limit=str(limit) + ) + if response: + break + except Exception as e: + logging.error(f"请求出错: {e}") + count += 1 + if count > 3: + break + time.sleep(10) + return response + def get_data_from_db(self, symbol, bar, db_url): sql = """ SELECT * FROM crypto_market_data diff --git a/core/biz/market_monitor.py b/core/biz/market_monitor.py new file mode 100644 index 0000000..c25f22c --- /dev/null +++ b/core/biz/market_monitor.py @@ -0,0 +1,249 @@ +import pandas as pd +import numpy as np +from metrics_config import METRICS_CONFIG +from time import time + +import logging + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s") + + +def create_metrics_report(row: pd.Series, only_output_rise: bool = False): + """ + 创建指标报告 + """ + contents = [] + huge_volume = row["huge_volume"] + symbol = row["symbol"] + bar = row["bar"] + window_size = row["window_size"] + date_time = row["date_time"] + if huge_volume == 1: + logging.info( + f"symbol: {symbol} {bar} window_size: {window_size} date_time: {date_time} 巨量" + ) + else: + logging.info( + f"symbol: {symbol} {bar} window_size: {window_size} date_time: {date_time} 非巨量,此次不发送相关数据" + ) + return + + # fill -1 to nan + row = row.fillna(1) + + close = row["close"] + open = row["open"] + high = row["high"] + low = row["low"] + pct_chg = row["pct_chg"] + if only_output_rise and pct_chg < 0: + logging.info( + f"symbol: {symbol} {bar} window_size: {window_size} date_time: {date_time} 下跌,不发送相关数据" + ) + return + + contents.append(f"# 交易巨量报告") + contents.append(f"## {symbol} {bar} 滑动窗口: {window_size} 时间: {date_time}") + contents.append(f"### 价格信息") + contents.append(f"当前价格: {close}, 开盘价: {open}, 最高价: {high}, 最低价: {low}") + contents.append(f"涨跌幅: {pct_chg}") + + volume = row["volume"] + volCcy = row["volCcy"] + volCCyQuote = row["volCCyQuote"] + volume_ratio = row["volume_ratio"] + spike_intensity = row["spike_intensity"] + close_80_high = int(row["close_80_high"]) + close_20_low = int(row["close_20_low"]) + close_90_high = int(row["close_90_high"]) + close_10_low = int(row["close_10_low"]) + high_80_high = int(row["high_80_high"]) + high_90_high = int(row["high_90_high"]) + low_20_low = int(row["low_20_low"]) + low_10_low = int(row["low_10_low"]) + + contents.append(f"### 交易量信息") + contents.append( + f"交易量(张): {volume}, 交易量(币): {volCcy}, 交易量(货币): {volCCyQuote}" + ) + contents.append(f"交易量比率: {volume_ratio}, 尖峰强度: {spike_intensity}") + if close_90_high: + contents.append(f"当前价格处于滑动窗口期90%分位数高点") + elif close_80_high: + contents.append(f"当前价格处于滑动窗口期80%分位数高点") + elif close_20_low: + contents.append(f"当前价格处于滑动窗口期20%分位数低点") + elif close_10_low: + contents.append(f"当前价格处于滑动窗口期10%分位数低点") + + long_short_info = {"多": [], "空": []} + ma_long_short = str(row["ma_long_short"]) + ma_long_short_value = METRICS_CONFIG.get("ma_long_short", {}).get(ma_long_short, 1) + if ma_long_short_value > 1: + long_short_info["多"].append(f"均线势头: {ma_long_short}") + if ma_long_short_value < 1: + long_short_info["空"].append(f"均线势头: {ma_long_short}") + + macd_signal = str(row["macd_signal"]) + macd_divergence = str(row["macd_divergence"]) + kdj_signal = str(row["kdj_signal"]) + kdj_pattern = str(row["kdj_pattern"]) + rsi_signal = str(row["rsi_signal"]) + boll_signal = str(row["boll_signal"]) + boll_pattern = str(row["boll_pattern"]) + + is_long = False + is_short = False + is_over_buy = False + is_over_sell = False + if ( + macd_divergence == "顶背离" + or kdj_pattern in ["超超买", "超买"] + or rsi_signal in ["超超买", "超买"] + or boll_pattern in ["超超买", "超买"] + ): + is_over_buy = True + if ( + macd_divergence == "底背离" + or kdj_pattern in ["超超卖", "超卖"] + or rsi_signal in ["超超卖", "超卖"] + or boll_pattern in ["超超卖", "超卖"] + ): + is_over_sell = True + if ma_long_short == "多": + is_long = True + if ma_long_short == "空": + is_short = True + ma_divergence = str(row["ma_divergence"]) + + if is_long: + check_long_short = "多" + if is_over_buy: + check_over_buy = "超买" + else: + check_over_buy = "非超买" + ma_divergence_value = ( + METRICS_CONFIG.get("ma_divergence", {}) + .get(check_long_short, {}) + .get(check_over_buy, {}) + .get(ma_divergence, 1) + ) + if ma_divergence_value > 1: + long_short_info["多"].append(f"均线形态: {ma_divergence}") + if ma_divergence_value < 1: + long_short_info["空"].append(f"均线形态: {ma_divergence}") + if is_short: + if is_over_sell: + check_over_sell = "超卖" + else: + check_over_sell = "非超卖" + ma_divergence_value = ( + METRICS_CONFIG.get("ma_divergence", {}) + .get(check_long_short, {}) + .get(check_over_sell, {}) + .get(ma_divergence, 1) + ) + if ma_divergence_value > 1: + long_short_info["多"].append(f"均线形态: {ma_divergence}") + if ma_divergence_value < 1: + long_short_info["空"].append(f"均线形态: {ma_divergence}") + + ma_cross = str(row["ma_cross"]) + ma_cross_value = METRICS_CONFIG.get("ma_cross", {}).get(ma_cross, 1) + if ma_cross_value > 1: + long_short_info["多"].append(f"均线交叉: {ma_cross}") + if ma_cross_value < 1: + long_short_info["空"].append(f"均线交叉: {ma_cross}") + + macd_signal_value = METRICS_CONFIG.get("macd", {}).get(macd_signal, 1) + if macd_signal_value > 1: + long_short_info["多"].append(f"MACD信号: {macd_signal}") + if macd_signal_value < 1: + long_short_info["空"].append(f"MACD信号: {macd_signal}") + + macd_divergence_value = METRICS_CONFIG.get("macd", {}).get(macd_divergence, 1) + if macd_divergence_value > 1: + long_short_info["多"].append(f"MACD背离: {row['macd_divergence']}") + if macd_divergence_value < 1: + long_short_info["空"].append(f"MACD背离: {row['macd_divergence']}") + + kdj_signal_value = METRICS_CONFIG.get("kdj", {}).get(kdj_signal, 1) + if kdj_signal_value > 1: + long_short_info["多"].append(f"KDJ信号: {kdj_signal}") + if kdj_signal_value < 1: + long_short_info["空"].append(f"KDJ信号: {kdj_signal}") + + kdj_pattern_value = METRICS_CONFIG.get("kdj", {}).get(kdj_pattern, 1) + if kdj_pattern_value > 1: + long_short_info["多"].append(f"KDJ形态: {kdj_pattern}") + if kdj_pattern_value < 1: + long_short_info["空"].append(f"KDJ形态: {kdj_pattern}") + + rsi_signal_value = METRICS_CONFIG.get("rsi", {}).get(rsi_signal, 1) + if rsi_signal_value > 1: + long_short_info["多"].append(f"RSI形态: {rsi_signal}") + if rsi_signal_value < 1: + long_short_info["空"].append(f"RSI形态: {rsi_signal}") + + boll_signal_value = METRICS_CONFIG.get("boll", {}).get(boll_signal, 1) + if boll_signal_value > 1: + long_short_info["多"].append(f"BOLL信号: {boll_signal}") + if boll_signal_value < 1: + long_short_info["空"].append(f"BOLL信号: {boll_signal}") + + boll_pattern_value = METRICS_CONFIG.get("boll", {}).get(boll_pattern, 1) + if boll_pattern_value > 1: + long_short_info["多"].append(f"BOLL形态: {boll_pattern}") + if boll_pattern_value < 1: + long_short_info["空"].append(f"BOLL形态: {boll_pattern}") + + k_up_down = str(row["k_up_down"]) + k_shape = str(row["k_shape"]) + if is_over_buy: + k_shape_value = ( + METRICS_CONFIG.get("k_shape", {}) + .get("超买", {}) + .get(k_up_down, {}) + .get(k_shape, 1) + ) + if k_shape_value > 1: + long_short_info["多"].append(f"K线形态: {k_shape}") + if k_shape_value < 1: + long_short_info["空"].append(f"K线形态: {k_shape}") + if is_over_sell: + k_shape_value = ( + METRICS_CONFIG.get("k_shape", {}) + .get("超卖", {}) + .get(k_up_down, {}) + .get(k_shape, 1) + ) + if k_shape_value > 1: + long_short_info["多"].append(f"K线形态: {k_shape}") + if k_shape_value < 1: + long_short_info["空"].append(f"K线形态: {k_shape}") + + + if k_up_down == "阳线": + if is_long and not is_over_buy: + long_short_info["多"].append(f"量价关系: 非超买且放量上涨") + if is_short and is_over_sell: + long_short_info["多"].append(f"量价关系: 空头态势且超卖,但出现放量上涨,可能反转") + if k_up_down == "阴线": + if is_long and is_over_buy: + if close_80_high or close_90_high or high_80_high or high_90_high: + long_short_info["空"].append(f"量价关系: 多头态势且超买, 目前是价位高点,但出现放量下跌,可能反转") + if is_short and not is_over_sell: + long_short_info["空"].append(f"量价关系: 空头态势且非超卖,出现放量下跌") + + contents.append(f"### 技术指标信息") + long_info_list = long_short_info["多"] + short_info_list = long_short_info["空"] + if len(long_info_list) > 0: + contents.append(f"#### 多头指标信号") + contents.append(f"{"\n".join(long_info_list)}") + if len(short_info_list) > 0: + contents.append(f"#### 空头指标信号") + contents.append(f"{"\n".join(short_info_list)}") + + mark_down_text = "\n\n".join(contents) + return mark_down_text diff --git a/core/db/__pycache__/db_huge_volume_data.cpython-312.pyc b/core/db/__pycache__/db_huge_volume_data.cpython-312.pyc index 8669d470732c2d23d652af7eeb3e4301d48e0235..831f6ab068d995129b23ca28df0a5517d4e791af 100644 GIT binary patch delta 5624 zcmb_gdrVu`8TU1|d6@t)HgB+b8S^mMKmr8EJi;TiBq5I`4W-1hoj}9$8k&TyqwAtB zYuA+Y+bYzg3(1BvYhME; zQUXYy&-tD2{=VOL&Uf!U=YHok;ljtlrr)HbB&qQ4=Qrnv-=Dp{X;4sWl3+}DNT`Gf zP4`RcFAG&Lr@kUoLus5xup{V&zs03Vt;aRI6_EN}T6yGWk(OOYm|G|Lq!*sn9xg~! z$^SmanQlh3P{tBf5^6!zyb3Ks)UpaKMzm-ZT7qctDzp^Ql2vFKqNTBD{9vjT(J~qJ z)!+;i3gzIAFHE=Lh*cipn&Sr(D-bO|rL)z-Qv9!R8W$7pcLTMkh{$4Df!J0d6H`&T+UY*2}@YHq({FFzFY){_*LNQGbYINTa~?vLB0-%z9EF z?vC^dcaC8U#O7pP&LQjI9FmN5RJ=k3gJMM_OI}Pv5_KdA%r{m6gO~tIBr$fq9G_j! z!$V%ujiN1Bu1~AN398KeGn#KCznf#npVUv(Rar^c(EsDG5d1W$MA+Ple&P-^0W})4 zS!{z!eV5P*qP|Ax2wv8^0#LZ+3WHQJQ19KO9!Jf89XoI=5E};*8nf5LQxtRW`pmU>3C<7d(jKZsGPO=xF zh%90_gl4uB%%)Q4H93;2ZYNZ|zIQ31R_qAcGp-1E`*GZ+6%UZ_BV-PeVa(acoNW*v zMIeT90CG(=!q?y-Q!^@g*7V-GEIG(xtnd!cj32kfMMa8_5p5hAGtD3ceH_hfB}y&R zG52r5M_DaG2iP+%!>_WJ*OQt{L7+BQm$h2%5u&XkT5=FxgRQyEx()=r8u)vBM)3LE zWx>S~PMVtWi>HQso@tLwx7MPB;sR5n;x4>4phJVo@eh~(t^8);I((Mjl1#j!k;t`- zyuZMrPWb|XJ~Xy?u@&abS(!%(VLb8JVl<5ChvsSFZ78<5G3{*~zX)23-V{dY@~axL zAAdrcCniS5+46{ff|J&gsi>VkbYg)*`V+%`(LdxJ^~?nPA_)l416qoWKbVKRnq6jxdG}s32l@R@m`$7^5nLIyo zyqCP6RQR-dfy*n|>}lW=0H_YrMj|0CAX|-kt*hyNX6tGz$DxpO2RR{x#?R!<%QN^4GF0<;v=p#Gh_9@9yO%b zTQrZ}>Ny;JyME3MED6TjfTbbb1sr8}0bhZQxCs<1Am0P>3P^o9v#}LbwEhY-RW#YQ%u49G^Vxk_@`h$5b%PvML-H_B+NPr)`vOP9sME`>4>tc*lB z2FfN}5hvQkFj+y(_8j4!+hdn|hx&VJ_+R(-ETh2?u25x|=j~yYU`Y&Dno{*=;=;A? zdv`u=BmU{WC8W{TLixId-?i=DcL71gPS=*W<|%-@#K-|aoT4t<_a|aY-VpG!SI7vL z+Kq6wy)bO89le}wqvC}9w!<|L^6?_RlJ1ll; zIPL_*d9w-7o4efYEa8}gFu57-EGX@3J5E9apg6*SD;WJE5yJp?JI$r?^|YcZ{YJ)lBnPRNUJ)o%Y9Yp+RZ^l1WqGm|OOuUb zmJDG@N-b2*6jJT1ogy2d4C6^5_9($fi~=s4MZp~vg+rn!W#GZ1&2IqS!jO|ozbiNnqh#QI<8J`xrA=HyI z#}2aUNna>&2cERMM9-L?eWnlx$eX?bh)2jABQruqBy)nyESaas%#ryDnbTyRC3A+% zb7Wp1^CFpZWPr@eWENC-L>JFf^V?(=(OkvD6}8*QY?2L~jss14mFl!Atgh4c2!Bp9 zENiI|ej<2wXS*&4eJ(XK_A2c@9L`!WoJ$F7sT0qgnJ%h(yb z2D=m4GYe^yr%c(}(n7jP&$!PM5MQU{AAtk?$8z4FW@K9aozOBi;hP!tZx^q_Z~I%) P2lT=|;S&}6VQ&8iXzXK# delta 2797 zcmbtWZETZO6z*-;Ze6#vTf424tzWKV9i>|b-PXFX0kaGUAHrmVxplQI>)cnlU4>19 zU`SvQbmJJaA4F6nffzwjV-!qG2#JXVfgeUvjS`9eFfnNu#2*^sIq%zTT~JJ)w@h`Lt8BNR# zKjoSVEjVJD8&OI^LcG$7Uh}xZUIlU0*Q^{`^L{Ol@_}I3-&pI94D?5md7@cY>mM53 zm(;^6HF{W+??^{^7Du_}qORBJ;aI_E7B_DHpd^(eDJ?&mbxd{J;KW9KPioZXVkhtZ z>ck#(1uLq)zmO|LR;7(tMTH=5on$6zu zNQgg;e!DcO2t=b~ddO7qIyj`$s%VZ7z?kCKtu6uq}7x>UE8#BarSYGETu)D$CO?Ah}!MM>LAwa*uZdfDEu~@ zHF*{EWrJBTt?5h?HpwbO=dVZ0`_n#a>K-fYR}Y zU)y2zveVFSZBh|0FGdK;1@bFai!{H3oI4Th+2WTiqX^zb(25YtMi96JHj{0PeFAUU zT5$81JFX^XDlae-ah%KI>##{?y*N5B!bvjkBzLaJ+OTk5T4<~lA(^)?66Rrla3~Ou zg@q+!bgR>=VT1BNuG-J8g5Az>L5t@~qPO}PrfMRJ9s)m{U%pBZ!28I40pU>+=GZ8| zLnH8BjS)s1-69!lgX1gQ!o733uER%8Co3b%V{SIPct7!pi%>6u(7}at^^yJ0D>c@H zqz@(VPK_n2|7Lpxrh1+5uERdR-jB6zpd0LR{fTQ~?+MnLtP82i7)lu;Jwkfm(n?=; zy2?PUHw16GMj^SX{<%e!=YMYx*GVoF4 zCik4G6LgGX!ZAu*?zqPCgmd4Ej(|}HH~N;r=R5b%)sWlGftVS4OQ5B5DIMUn=X#ys zDHYk%Pn-}e?yRh-`%FT zKsg@-r`@YpYqcq(L}#9IrXIzRR2XXcqp6F2&rrQqmq zw2k9j-+$Jc$^MOs2Mb$M;nqqrwpN-aY^_Y(8f=JEF$?fWb@3|FNY5Lm+MbjRjEr#) zT#Oi=6)Lzi8cHhL@SXATz;IZ2q4{Qtrbm#|o5gn#bQ6RK1_%ZTMhKz=oFEPlBc*lc zusD$=yrc!xBcMtj`w|r#mZYRMc{{tK(ay@LliHFvKCo8dtd#FSr+!8|ou87Eky1zs zN~VI7J*jNL%#oBBXM)o`DJ7b4Xs{UnRXZ3jH41|TlH8)HimBMlx~cus&Ra^$tXlAQ zz?HEHr!|wCj0|qUT;N7i;I Optional[int]: + """ + 处理时间参数,统一转换为时间戳 + :param time_param: 时间参数(字符串或整数) + :return: 时间戳或None + """ + if time_param is None: + return None + time_param = transform_date_time_to_timestamp(time_param) + if time_param is None: + return None + return time_param + + def _build_query_conditions( + self, + symbol: Optional[str] = None, + bar: Optional[str] = None, + window_size: Optional[int] = None, + start: Optional[Union[str, int]] = None, + end: Optional[Union[str, int]] = None, + additional_conditions: Optional[List[str]] = None + ) -> tuple[List[str], Dict[str, Any]]: + """ + 构建查询条件 + :param symbol: 交易对 + :param bar: K线周期 + :param window_size: 窗口大小 + :param start: 开始时间 + :param end: 结束时间 + :param additional_conditions: 额外的查询条件 + :return: (条件列表, 参数字典) + """ + conditions = additional_conditions or [] + condition_dict = {} + + if symbol: + conditions.append("symbol = :symbol") + condition_dict["symbol"] = symbol + if bar: + conditions.append("bar = :bar") + condition_dict["bar"] = bar + if window_size: + conditions.append("window_size = :window_size") + condition_dict["window_size"] = window_size + + # 处理时间参数 + start_timestamp = self._process_time_parameter(start) + end_timestamp = self._process_time_parameter(end) + + if start_timestamp is not None: + conditions.append("timestamp >= :start") + condition_dict["start"] = start_timestamp + if end_timestamp is not None: + conditions.append("timestamp <= :end") + condition_dict["end"] = end_timestamp + + return conditions, condition_dict + + def insert_data_to_mysql(self, df: pd.DataFrame) -> None: + """ + 将市场监控数据保存到MySQL的crypto_market_monitor表 + 速度:⭐⭐⭐⭐⭐ 最快 + 内存:⭐⭐⭐⭐ 中等 + 适用场景:中小数据量(<10万条) + :param df: 市场监控数据DataFrame + """ + if df is None or df.empty: + logging.warning("DataFrame为空,无需写入数据库。") + return + + self.db_manager.insert_data_to_mysql(df) + + def insert_data_to_mysql_fast(self, df: pd.DataFrame) -> None: + """ + 快速插入市场监控数据(方案2:使用executemany批量插入) + 速度:⭐⭐⭐⭐ 很快 + 内存:⭐⭐⭐⭐⭐ 低 + 适用场景:中等数据量 + :param df: 市场监控数据DataFrame + """ + if df is None or df.empty: + logging.warning("DataFrame为空,无需写入数据库。") + return + + self.db_manager.insert_data_to_mysql_fast(df) + + def insert_data_to_mysql_chunk(self, df: pd.DataFrame, chunk_size: int = 1000) -> None: + """ + 分块插入市场监控数据(方案3:分批处理大数据量) + 速度:⭐⭐⭐ 中等 + 内存:⭐⭐⭐⭐⭐ 最低 + 适用场景:大数据量(>10万条) + :param df: 市场监控数据DataFrame + :param chunk_size: 每块大小 + """ + if df is None or df.empty: + logging.warning("DataFrame为空,无需写入数据库。") + return + + self.db_manager.insert_data_to_mysql_chunk(df, chunk_size) + + def insert_data_to_mysql_simple(self, df: pd.DataFrame) -> None: + """ + 简单插入市场监控数据(方案4:使用pandas to_sql) + 速度:⭐⭐ 较慢 + 内存:⭐⭐⭐ 较高 + 适用场景:小数据量,简单场景 + :param df: 市场监控数据DataFrame + """ + if df is None or df.empty: + logging.warning("DataFrame为空,无需写入数据库。") + return + + self.db_manager.insert_data_to_mysql_simple(df) + + def query_latest_data(self, symbol: str, bar: str, window_size: int) -> Optional[Dict[str, Any]]: + """ + 查询最新的市场监控数据 + :param symbol: 交易对 + :param bar: K线周期 + :param window_size: 窗口大小 + :return: 最新数据字典或None + """ + conditions = [ + "symbol = :symbol", + "bar = :bar", + "window_size = :window_size" + ] + condition_dict = { + "symbol": symbol, + "bar": bar, + "window_size": window_size + } + + sql = f""" + SELECT * FROM {self.table_name} + WHERE {' AND '.join(conditions)} + ORDER BY timestamp DESC + LIMIT 1 + """ + + result = self.db_manager.query_data(sql, condition_dict, return_multi=False) + return result + + def query_data_before_timestamp(self, symbol: str, bar: str, window_size: int, timestamp: int, limit: int = 100) -> Optional[List[Dict[str, Any]]]: + """ + 查询指定时间戳之前的数据 + :param symbol: 交易对 + :param bar: K线周期 + :param window_size: 窗口大小 + :param timestamp: 时间戳 + :param limit: 限制条数 + :return: 数据列表或None + """ + conditions = [ + "symbol = :symbol", + "bar = :bar", + "window_size = :window_size", + "timestamp < :timestamp" + ] + condition_dict = { + "symbol": symbol, + "bar": bar, + "window_size": window_size, + "timestamp": timestamp + } + + sql = f""" + SELECT * FROM {self.table_name} + WHERE {' AND '.join(conditions)} + ORDER BY timestamp DESC + LIMIT {limit} + """ + + result = self.db_manager.query_data(sql, condition_dict, return_multi=True) + return result + + def query_market_monitor_by_symbol_bar(self, symbol: str, bar: str, window_size: int, start: Optional[Union[str, int]] = None, end: Optional[Union[str, int]] = None) -> Optional[List[Dict[str, Any]]]: + """ + 根据交易对和K线周期查询市场监控数据 + :param symbol: 交易对 + :param bar: K线周期 + :param window_size: 窗口大小 + :param start: 开始时间 + :param end: 结束时间 + :return: 数据列表或None + """ + conditions, condition_dict = self._build_query_conditions( + symbol=symbol, + bar=bar, + window_size=window_size, + start=start, + end=end + ) + + if not conditions: + sql = f"SELECT * FROM {self.table_name} ORDER BY timestamp DESC" + else: + sql = f""" + SELECT * FROM {self.table_name} + WHERE {' AND '.join(conditions)} + ORDER BY timestamp DESC + """ + + result = self.db_manager.query_data(sql, condition_dict, return_multi=True) + return result + + def query_market_monitor_by_window_size(self, window_size: int, symbol: Optional[str] = None, bar: Optional[str] = None, start: Optional[Union[str, int]] = None, end: Optional[Union[str, int]] = None) -> Optional[List[Dict[str, Any]]]: + """ + 根据窗口大小查询市场监控数据 + :param window_size: 窗口大小 + :param symbol: 交易对(可选) + :param bar: K线周期(可选) + :param start: 开始时间(可选) + :param end: 结束时间(可选) + :return: 数据列表或None + """ + conditions, condition_dict = self._build_query_conditions( + symbol=symbol, + bar=bar, + window_size=window_size, + start=start, + end=end + ) + + if not conditions: + sql = f"SELECT * FROM {self.table_name} ORDER BY timestamp DESC" + else: + sql = f""" + SELECT * FROM {self.table_name} + WHERE {' AND '.join(conditions)} + ORDER BY timestamp DESC + """ + + result = self.db_manager.query_data(sql, condition_dict, return_multi=True) + return result + + def get_market_monitor_statistics(self, symbol: Optional[str] = None, bar: Optional[str] = None, window_size: Optional[int] = None, start: Optional[Union[str, int]] = None, end: Optional[Union[str, int]] = None) -> Optional[Dict[str, Any]]: + """ + 获取市场监控数据统计信息 + :param symbol: 交易对(可选) + :param bar: K线周期(可选) + :param window_size: 窗口大小(可选) + :param start: 开始时间(可选) + :param end: 结束时间(可选) + :return: 统计信息字典或None + """ + conditions, condition_dict = self._build_query_conditions( + symbol=symbol, + bar=bar, + window_size=window_size, + start=start, + end=end + ) + + where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else "" + + sql = f""" + SELECT + COUNT(*) as total_count, + COUNT(DISTINCT symbol) as symbol_count, + COUNT(DISTINCT bar) as bar_count, + COUNT(DISTINCT window_size) as window_size_count, + MIN(timestamp) as earliest_timestamp, + MAX(timestamp) as latest_timestamp, + AVG(report_file_byte_size) as avg_file_size, + SUM(report_file_byte_size) as total_file_size + FROM {self.table_name} + {where_clause} + """ + + result = self.db_manager.query_data(sql, condition_dict, return_multi=False) + return result + + def get_recent_market_monitor_data(self, symbol: Optional[str] = None, bar: Optional[str] = None, window_size: Optional[int] = None, limit: int = 100) -> Optional[List[Dict[str, Any]]]: + """ + 获取最近的市场监控数据 + :param symbol: 交易对(可选) + :param bar: K线周期(可选) + :param window_size: 窗口大小(可选) + :param limit: 限制条数 + :return: 数据列表或None + """ + conditions, condition_dict = self._build_query_conditions( + symbol=symbol, + bar=bar, + window_size=window_size + ) + + if not conditions: + sql = f"SELECT * FROM {self.table_name} ORDER BY timestamp DESC LIMIT {limit}" + else: + sql = f""" + SELECT * FROM {self.table_name} + WHERE {' AND '.join(conditions)} + ORDER BY timestamp DESC + LIMIT {limit} + """ + + result = self.db_manager.query_data(sql, condition_dict, return_multi=True) + return result + + def get_market_monitor_by_file_size_range(self, min_size: int, max_size: int, symbol: Optional[str] = None, bar: Optional[str] = None, window_size: Optional[int] = None, start: Optional[Union[str, int]] = None, end: Optional[Union[str, int]] = None) -> Optional[List[Dict[str, Any]]]: + """ + 根据文件大小范围查询市场监控数据 + :param min_size: 最小文件大小 + :param max_size: 最大文件大小 + :param symbol: 交易对(可选) + :param bar: K线周期(可选) + :param window_size: 窗口大小(可选) + :param start: 开始时间(可选) + :param end: 结束时间(可选) + :return: 数据列表或None + """ + conditions, condition_dict = self._build_query_conditions( + symbol=symbol, + bar=bar, + window_size=window_size, + start=start, + end=end + ) + + conditions.append("report_file_byte_size BETWEEN :min_size AND :max_size") + condition_dict["min_size"] = min_size + condition_dict["max_size"] = max_size + + sql = f""" + SELECT * FROM {self.table_name} + WHERE {' AND '.join(conditions)} + ORDER BY timestamp DESC + """ + + result = self.db_manager.query_data(sql, condition_dict, return_multi=True) + return result + + def get_market_monitor_by_symbol_list(self, symbols: List[str], bar: Optional[str] = None, window_size: Optional[int] = None, start: Optional[Union[str, int]] = None, end: Optional[Union[str, int]] = None) -> Optional[List[Dict[str, Any]]]: + """ + 根据交易对列表查询市场监控数据 + :param symbols: 交易对列表 + :param bar: K线周期(可选) + :param window_size: 窗口大小(可选) + :param start: 开始时间(可选) + :param end: 结束时间(可选) + :return: 数据列表或None + """ + if not symbols: + return None + + conditions, condition_dict = self._build_query_conditions( + bar=bar, + window_size=window_size, + start=start, + end=end + ) + + # 构建IN查询条件 + placeholders = [f":symbol_{i}" for i in range(len(symbols))] + conditions.append(f"symbol IN ({', '.join(placeholders)})") + for i, symbol in enumerate(symbols): + condition_dict[f"symbol_{i}"] = symbol + + sql = f""" + SELECT * FROM {self.table_name} + WHERE {' AND '.join(conditions)} + ORDER BY timestamp DESC + """ + + result = self.db_manager.query_data(sql, condition_dict, return_multi=True) + return result + + def delete_old_market_monitor_data(self, days: int = 30, symbol: Optional[str] = None, bar: Optional[str] = None, window_size: Optional[int] = None) -> int: + """ + 删除旧的市场监控数据 + :param days: 保留天数 + :param symbol: 交易对(可选) + :param bar: K线周期(可选) + :param window_size: 窗口大小(可选) + :return: 删除的记录数 + """ + import datetime + from sqlalchemy import text + + # 计算截止时间戳 + cutoff_time = datetime.datetime.now() - datetime.timedelta(days=days) + cutoff_timestamp = int(cutoff_time.timestamp() * 1000) # 转换为毫秒时间戳 + + conditions, condition_dict = self._build_query_conditions( + symbol=symbol, + bar=bar, + window_size=window_size + ) + + conditions.append("timestamp < :cutoff_timestamp") + condition_dict["cutoff_timestamp"] = cutoff_timestamp + + sql = f""" + DELETE FROM {self.table_name} + WHERE {' AND '.join(conditions)} + """ + + try: + with self.db_manager.db_engine.connect() as conn: + result = conn.execute(text(sql), condition_dict) + conn.commit() + deleted_count = result.rowcount + logging.info(f"删除了 {deleted_count} 条旧的市场监控数据") + return deleted_count + except Exception as e: + logging.error(f"删除旧数据时发生错误: {e}") + return 0 \ No newline at end of file diff --git a/huge_volume_main.py b/huge_volume_main.py index 04557c2..887a0f8 100644 --- a/huge_volume_main.py +++ b/huge_volume_main.py @@ -44,7 +44,7 @@ class HugeVolumeMain: for bar in self.market_data_main.bars: if start is None: start = MONITOR_CONFIG.get("volume_monitor", {}).get( - "initial_date", "2025-05-01 00:00:00" + "initial_date", "2025-05-15 00:00:00" ) data = self.detect_volume_spike( symbol, @@ -467,7 +467,7 @@ def batch_initial_detect_volume_spike(threshold: float = 2.0): window_sizes = [50, 80, 100, 120] huge_volume_main = HugeVolumeMain(threshold) start_date = MONITOR_CONFIG.get("volume_monitor", {}).get( - "initial_date", "2025-05-01 00:00:00" + "initial_date", "2025-05-15 00:00:00" ) for window_size in window_sizes: huge_volume_main.batch_initial_detect_volume_spike( diff --git a/market_data_main.py b/market_data_main.py index 3fd520b..203c703 100644 --- a/market_data_main.py +++ b/market_data_main.py @@ -2,7 +2,7 @@ import logging from datetime import datetime from time import sleep import pandas as pd -from core.biz.market_data_monitor import MarketDataMonitor +from core.biz.market_data import MarketData from core.db.db_market_data import DBMarketData from core.biz.metrics_calculation import MetricsCalculation from core.utils import ( @@ -26,7 +26,7 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(mes class MarketDataMain: def __init__(self): - self.market_data_monitor = MarketDataMonitor( + self.market_data = MarketData( api_key=API_KEY, secret_key=SECRET_KEY, passphrase=PASSPHRASE, @@ -113,7 +113,7 @@ class MarketDataMain: logging.info( f"获取行情数据: {symbol} {bar} 从 {start_date_time} 到 {end_date_time}" ) - data = self.market_data_monitor.get_historical_kline_data( + data = self.market_data.get_historical_kline_data( symbol=symbol, start=current_start_time_ts, bar=bar, @@ -172,43 +172,7 @@ class MarketDataMain: "create_time", ] ] - data["pre_close"] = None - data["close_change"] = None - data["pct_chg"] = None - data["ma1"] = None - data["ma2"] = None - data["dif"] = None - data["dea"] = None - data["macd"] = None - data["macd_signal"] = None - data["macd_divergence"] = None - data["kdj_k"] = None - data["kdj_d"] = None - data["kdj_j"] = None - data["kdj_signal"] = None - data["kdj_pattern"] = None - data["ma5"] = None - data["ma10"] = None - data["ma20"] = None - data["ma30"] = None - data["ma_cross"] = None - data["ma5_close_diff"] = None - data["ma10_close_diff"] = None - data["ma20_close_diff"] = None - data["ma30_close_diff"] = None - data["ma_close_avg"] = None - data["ma_long_short"] = None - data["ma_divergence"] = None - data["rsi_14"] = None - data["rsi_signal"] = None - data["boll_upper"] = None - data["boll_middle"] = None - data["boll_lower"] = None - data["boll_signal"] = None - data["boll_pattern"] = None - data["k_length"] = None - data["k_shape"] = None - data["k_up_down"] = None + data = self.add_new_columns(data) self.db_market_data.insert_data_to_mysql(data) current_min_start_time_ts = data["timestamp"].min() if current_min_start_time_ts < min_start_time_ts: @@ -233,7 +197,10 @@ class MarketDataMain: handle_data = self.db_market_data.query_market_data_by_symbol_bar( symbol=symbol, bar=bar, start=earliest_timestamp, end=None ) - if handle_data is not None and len(handle_data) > len(before_data): + if handle_data is not None: + if before_data is not None and len(handle_data) <= len(before_data): + logging.error(f"handle_data数据条数小于before_data数据条数: {symbol} {bar}") + return None if isinstance(handle_data, list): handle_data = pd.DataFrame(handle_data) elif isinstance(handle_data, dict): @@ -251,6 +218,54 @@ class MarketDataMain: logging.info(f"开始保存技术指标数据: {symbol} {bar}") self.db_market_data.insert_data_to_mysql(handle_data) return data + + def add_new_columns(self, data: pd.DataFrame): + """ + 添加新列 + """ + columns = data.columns.tolist() + if "buy_sz" not in columns: + data["buy_sz"] = -1 + if "sell_sz" not in columns: + data["sell_sz"] = -1 + data["pre_close"] = None + data["close_change"] = None + data["pct_chg"] = None + data["ma1"] = None + data["ma2"] = None + data["dif"] = None + data["dea"] = None + data["macd"] = None + data["macd_signal"] = None + data["macd_divergence"] = None + data["kdj_k"] = None + data["kdj_d"] = None + data["kdj_j"] = None + data["kdj_signal"] = None + data["kdj_pattern"] = None + data["ma5"] = None + data["ma10"] = None + data["ma20"] = None + data["ma30"] = None + data["ma_cross"] = None + data["ma5_close_diff"] = None + data["ma10_close_diff"] = None + data["ma20_close_diff"] = None + data["ma30_close_diff"] = None + data["ma_close_avg"] = None + data["ma_long_short"] = None + data["ma_divergence"] = None + data["rsi_14"] = None + data["rsi_signal"] = None + data["boll_upper"] = None + data["boll_middle"] = None + data["boll_lower"] = None + data["boll_signal"] = None + data["boll_pattern"] = None + data["k_length"] = None + data["k_shape"] = None + data["k_up_down"] = None + return data def calculate_metrics(self, data: pd.DataFrame): """ diff --git a/market_monitor_main.py b/market_monitor_main.py new file mode 100644 index 0000000..3904b14 --- /dev/null +++ b/market_monitor_main.py @@ -0,0 +1,164 @@ +from numpy import real +from market_data_main import MarketDataMain +from huge_volume_main import HugeVolumeMain +from core.biz.market_monitor import create_metrics_report +from core.db.db_market_monitor import DBMarketMonitor +from core.wechat import Wechat +from config import MONITOR_CONFIG, MYSQL_CONFIG +from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp + +import logging +import os +import pandas as pd +from datetime import datetime, timedelta +import json +import re + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s") + + +class MarketMonitorMain: + def __init__(self): + self.market_data_main = MarketDataMain() + self.huge_volume_main = HugeVolumeMain() + self.wechat = Wechat() + self.monitor_config = MONITOR_CONFIG + self.window_size = 100 + self.start_date = MONITOR_CONFIG.get("volume_monitor", {}).get( + "initial_date", "2025-05-01 00:00:00" + ) + self.latest_record_file_path = "./output/latest_record.json" + self.latest_record = self.get_latest_record() + self.output_folder = "./output/report/market_monitor/" + os.makedirs(self.output_folder, exist_ok=True) + + mysql_user = MYSQL_CONFIG.get("user", "xch") + mysql_password = MYSQL_CONFIG.get("password", "") + if not mysql_password: + raise ValueError("MySQL password is not set") + mysql_host = MYSQL_CONFIG.get("host", "localhost") + mysql_port = MYSQL_CONFIG.get("port", 3306) + mysql_database = MYSQL_CONFIG.get("database", "okx") + + self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" + + self.db_market_monitor = DBMarketMonitor(self.db_url) + + def get_latest_record(self): + """ + 获取最新记录 + """ + if os.path.exists(self.latest_record_file_path): + with open(self.latest_record_file_path, "r", encoding="utf-8") as f: + return json.load(f) + else: + with open(self.latest_record_file_path, "w", encoding="utf-8") as f: + json.dump({}, f, ensure_ascii=False, indent=4) + return {} + + def monitor_realtime_market( + self, + symbol: str, + bar: str, + only_output_huge_volume: bool = False, + only_output_rise: bool = False, + ): + """ + 监控最新市场数据 + 考虑到速度,暂不与数据库交互,直接从api获取数据 + """ + real_time_data = self.market_data_main.market_data.get_realtime_kline_data( + symbol=symbol, + bar=bar, + end_time=None, + limit=50, + ) + + if real_time_data is None or len(real_time_data) == 0: + logging.error(f"获取最新市场数据失败: {symbol}, {bar}") + return + + latest_realtime_timestamp = real_time_data["timestamp"].iloc[-1] + latest_record_timestamp = ( + self.latest_record.get(symbol, {}).get(bar, {}).get("timestamp", 0) + ) + latest_reatime_datetime = timestamp_to_datetime(latest_realtime_timestamp) + latest_record_datetime = timestamp_to_datetime(latest_record_timestamp) + if ( + latest_record_timestamp is not None + and latest_realtime_timestamp <= latest_record_timestamp + ): + logging.info( + f"最新市场数据时间戳 {latest_reatime_datetime} 小于等于最新记录时间戳 {latest_record_datetime}, 不进行监控" + ) + return + else: + self.latest_record[symbol] = {bar: {"timestamp": latest_realtime_timestamp}} + with open(self.latest_record_file_path, "w", encoding="utf-8") as f: + json.dump(self.latest_record, f, ensure_ascii=False, indent=4) + + + logging.info( + f"最新市场数据时间 {latest_reatime_datetime}, 上一次记录时间 {latest_record_datetime}" + ) + + real_time_data = self.market_data_main.add_new_columns(real_time_data) + logging.info(f"开始计算技术指标: {symbol} {bar}") + real_time_data = self.market_data_main.calculate_metrics(real_time_data) + logging.info(f"开始计算大成交量: {symbol} {bar} 窗口大小: {self.window_size}") + real_time_data = self.huge_volume_main.huge_volume.detect_huge_volume( + data=real_time_data, + window_size=self.window_size, + threshold=self.huge_volume_main.threshold, + check_price=True, + only_output_huge_volume=only_output_huge_volume, + output_excel=False, + ) + if real_time_data is None or len(real_time_data) == 0: + logging.error( + f"计算大成交量失败: {symbol} {bar} 窗口大小: {self.window_size}" + ) + return + + report = create_metrics_report(real_time_data, only_output_rise) + text_length = len(report.encode("utf-8")) + logging.info(f"发送报告到企业微信,字节数: {text_length}") + self.wechat.send_markdown(report) + self.latest_record[symbol][bar]["timestamp"] = latest_realtime_timestamp + with open(self.latest_record_file_path, "w", encoding="utf-8") as f: + json.dump(self.latest_record, f, ensure_ascii=False, indent=4) + # remove punction in latest_reatime_datetime + latest_reatime_datetime = re.sub(r"[\:\-\s]", "", latest_reatime_datetime) + report_file_name = f"{symbol}_{bar}_{self.window_size}_{latest_reatime_datetime}.md" + report_file_path = os.path.join(self.output_folder, report_file_name) + with open(report_file_path, "w", encoding="utf-8") as f: + f.write(report.replace(":", "_")) + report_file_byte_size = os.path.getsize(report_file_path) + report_data = { + "symbol": symbol, + "bar": bar, + "window_size": self.window_size, + "timestamp": latest_realtime_timestamp, + "date_time": latest_reatime_datetime, + "report": report, + "report_file_path": report_file_path, + "report_file_name": report_file_name, + "report_file_byte_size": report_file_byte_size + } + report_data = pd.DataFrame([report_data]) + logging.info(f"插入数据到数据库") + self.db_market_monitor.insert_data_to_mysql(report_data) + + def batch_monitor_realtime_market( + self, + only_output_huge_volume: bool = True, + only_output_rise: bool = False, + ): + for symbol in self.market_data_main.symbols: + for bar in self.market_data_main.bars: + self.monitor_realtime_market( + symbol, + bar, + only_output_huge_volume, + only_output_rise, + ) diff --git a/metrics_config.py b/metrics_config.py new file mode 100644 index 0000000..3ebabe9 --- /dev/null +++ b/metrics_config.py @@ -0,0 +1,166 @@ +METRICS_CONFIG = { + "macd": { + "金叉": 1.5, + "死叉": 0.5, + "底背离": 1.5, + "顶背离": 0.5, + }, + "kdj": { + "金叉": 1.5, + "死叉": 0.5, + "超超卖": 1.5, + "超卖": 1.2, + "超超买": 0.5, + "超买": 0.8, + }, + "rsi": { + "超卖": 1.2, + "超买": 0.8, + }, + "boll": { + "突破下轨": 1.2, + "击穿上轨": 0.8, + "超超卖": 1.5, + "超卖": 1.2, + "超超买": 0.5, + "超买": 0.8, + }, + "ma_long_short": { + "多": 1.2, + "空": 0.8, + }, + "ma_divergence": { + "多": { + "超买": { + "超发散": 0.8, + "粘合": 0.8, + }, + "非超买": { + "发散": 1.2, + "适中": 1.2, + "粘合": 1.5, + }, + }, + "空": { + "超卖": { + "超发散": 1.2, + "粘合": 1.2, + }, + "非超卖": { + "发散": 0.8, + "适中": 0.8, + "粘合": 0.8, + }, + }, + }, + "ma5102030": { + "5穿10": 1.1, + "5穿20": 1.2, + "5穿30": 1.3, + "10穿30": 1.3, + "10穿5": 0.8, + "20穿5": 0.7, + "30穿5": 0.6, + "30穿10": 0.5, + }, + "k_shape": { + "超买": { + "阳线": { + "一字": 0.8, + "长吊锤线": 0.8, + "吊锤线": 0.9, + "长倒T线": 0.8, + "倒T线": 0.9, + "长十字星": 0.8, + "十字星": 0.9, + "长上影线纺锤体": 0.8, + "长下影线纺锤体": 0.9, + "大实体": 1.1, + "超大实体": 1.2, + "超大实体+光头光脚": 1.3, + }, + "阴线": { + "一字": 0.7, + "长吊锤线": 0.7, + "吊锤线": 0.8, + "长倒T线": 0.7, + "倒T线": 0.8, + "长十字星": 0.7, + "十字星": 0.8, + "长上影线纺锤体": 0.7, + "长下影线纺锤体": 0.8, + "大实体": 0.7, + "超大实体": 0.6, + "光头光脚": 0.8, + "超大实体+光头光脚": 0.5, + }, + }, + "超卖": { + "阳线": { + "一字": 1.5, + "长吊锤线": 1.5, + "吊锤线": 1.2, + "长倒T线": 1.5, + "倒T线": 1.2, + "长十字星": 1.6, + "十字星": 1.3, + "长上影线纺锤体": 1.2, + "长下影线纺锤体": 1.5, + "小实体": 1.2, + "大实体": 1.5, + "超大实体": 1.8, + "光头光脚": 1.5, + "超大实体+光头光脚": 2, + }, + "阴线": { + "一字": 1.2, + "长吊锤线": 1.2, + "吊锤线": 1.1, + "长倒T线": 1.2, + "倒T线": 1.1, + "长十字星": 1.3, + "十字星": 1.1, + "长上影线纺锤体": 1.1, + "长下影线纺锤体": 1.2, + "大实体": 0.8, + "超大实体": 0.7, + "光头光脚": 0.9, + "超大实体+光头光脚": 0.6, + }, + }, + }, + "huge_volume": { + "阳线": { + "多": { + "非超买": { + "any": 1.2, + }, + }, + "空": { + "超卖": { + "close_20_low": 1.2, + "close_10_low": 1.3, + "low_20_low": 1.3, + "low_10_low": 1.5, + "any": 1.1, + }, + }, + }, + "阴线": { + "多": { + "超买": { + "close_80_high": 0.8, + "close_90_high": 0.7, + "high_80_high": 0.7, + "high_90_high": 0.6, + "any": 0.9, + }, + }, + "空": { + "非超卖": { + "any": 0.8, + }, + }, + }, + }, +} diff --git a/play.py b/play.py index c70c603..453b77a 100644 --- a/play.py +++ b/play.py @@ -1,6 +1,6 @@ import logging -from core.quant_trader import QuantTrader -from core.strategy import QuantStrategy +from core.biz.quant_trader import QuantTrader +from core.biz.strategy import QuantStrategy logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s: %(message)s') diff --git a/sql/query/sql_playground.sql b/sql/query/sql_playground.sql index af4b3f0..45f9454 100644 --- a/sql/query/sql_playground.sql +++ b/sql/query/sql_playground.sql @@ -4,7 +4,7 @@ order by timestamp ; select * from crypto_market_data WHERE symbol='XCH-USDT' and bar='5m' and date_time > '2025-08-04 15:00:00' -order by timestamp desc; +order by timestamp asc; delete FROM crypto_market_data where symbol != 'XCH-USDT'; diff --git a/sql/table/crypto_market_monitor.sql b/sql/table/crypto_market_monitor.sql new file mode 100644 index 0000000..4205684 --- /dev/null +++ b/sql/table/crypto_market_monitor.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS crypto_market_monitor ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + symbol VARCHAR(50) NOT NULL COMMENT '交易对', + bar VARCHAR(20) NOT NULL COMMENT 'K线周期', + window_size INT NOT NULL COMMENT '窗口大小, 50, 80, 100, 120', + timestamp BIGINT NOT NULL COMMENT '时间戳', + date_time VARCHAR(50) NOT NULL COMMENT '日期时间', + report TEXT NOT NULL COMMENT '报告', + report_file_path VARCHAR(255) NOT NULL COMMENT '报告文件路径', + report_file_name VARCHAR(255) NOT NULL COMMENT '报告文件名', + report_file_byte_size INT NOT NULL COMMENT '报告文件大小', + UNIQUE KEY idx_symbol_bar_window_size_timestamp (symbol, bar, window_size, timestamp) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场行情监控'; \ No newline at end of file