From 5b7e95f4d95d736ffd7cb55a92ce9564003f4e98 Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Tue, 9 Sep 2025 14:15:29 +0800 Subject: [PATCH] support import binance data optimize orb strategy --- .../db/__pycache__/db_manager.cpython-312.pyc | Bin 12110 -> 11983 bytes .../db_market_data.cpython-312.pyc | Bin 16944 -> 17144 bytes core/db/db_binance_data.py | 296 +++++++++--------- core/db/db_manager.py | 7 - core/db/db_market_data.py | 22 +- .../__pycache__/orb_trade.cpython-312.pyc | Bin 35498 -> 39211 bytes core/trade/orb_trade.py | 219 ++++++++++--- huge_volume_main.py | 8 +- orb_trade_main.py | 153 +++++---- play.py | 79 ++++- sql/query/sql_playground.sql | 23 +- .../crypto_binance_data_essential_indexes.sql | 54 ++++ sql/table/crypto_binance_data_indexes.sql | 71 +++++ ..._binance_huge_volume_essential_indexes.sql | 69 ++++ 14 files changed, 729 insertions(+), 272 deletions(-) create mode 100644 sql/table/crypto_binance_data_essential_indexes.sql create mode 100644 sql/table/crypto_binance_data_indexes.sql create mode 100644 sql/table/crypto_binance_huge_volume_essential_indexes.sql diff --git a/core/db/__pycache__/db_manager.cpython-312.pyc b/core/db/__pycache__/db_manager.cpython-312.pyc index 1dabfb7890352ee878ddea34edf8faac9dfcb3a2..57b95f5563f9d7809279a6e4662af375200d25b1 100644 GIT binary patch delta 1488 zcma)*O>7%Q6vtpN%G(r=co}5@i~77y=*bk+xWg` zSIK5z9)m_1yEknkY^czCVw02HSW07R`xwyXDzlwkfKUsfCBmuV4Qua z>#2viS<^U#hC4Eja~a1Qed#g95HckAA_kb|SfVpv&I0C`Weo4MWgb0L<_R7eDAcO1 zI)S`iaqSM_a7?raXl)l*cRC2j=6bcGjYvluk3-Uk=Xw@XvWX$ll}HB1MMY-hgd6FuV|3&h>{CpU@|KWg`sXlXb;c z=&Y}l(5`ArtH_pC420^c2RB6wFM8YZ)r}bYM$%b7(bx~U&{U|d`XO5?V|YG*^X3Di zC%LMFz85hs8HbV?QPMoLfYL57Ti1N;|K_$O%$3*G7Z7`P$RuGe!FG=JV~H&e12uA2-bgMfVPJI_ovd{DXtm(x;^v-m# zms~qCT#T#NW(e4)!`yvc!rZ=2G_bp;z@7(xGX(yo2L5JT0lQZz4uqO%0-gcE!Eh-` zn)y^oBR4dq6lIeWPd%Ci#S?%@kS%SMgK)|6tR-q~I`k1(cnlWEdex)Pz;p$00etcW z*R9YsZeBrj5Zng)cH-SaT1mZS-$ML#by~bW_5OreD@NmiW-L&*tZLb+QI}mRoIKVC z`MPYEI)#e6y6RB6z-|^!>NUvPO3kgmw7E^Uyj^k5(3e>Ju0qcm!KvR7Pz3!8s=zC0 delta 1708 zcma)6UrbwN6#s7TpWe1o5K1X69sMJe!rF`lm>8HM9V$y)oEWp14ZC}9SXt?XuNM*1 zEBd4{PJ-XeG-hf{d>|Oz(!_^7DJ;Hd^p)J0Jt=WMq3+3;=s8~nnuQ0yHotqmbIy0p z@0@$S`{Vf^F8IG|Xz&tPZ>@iHDV)0L|5+5)tAC2$3PPlMOV*_$F)-2yfFo*V(GklF z#j*t37%UpEj(Go;gf2GR`254&=$S(3K}aL?0;EM6L-{B|2B0hg@Z<1wf^5titlnu{ zmE)bbcWY*;WN4P7Smrcj}>w30-+eM=;9h0#T6`8UrjA2~>Y? zEl7g{c;Y0&qX20?eGKI>gvSwl2$-CXBk)}PY&G20eG2yz2u~n936NH6!OoP=xY(; z)zSExw1P!AN`8LcDCza=4|w|y<$cgzP7sIg{>Dmmj5r0}(t}~c%t8eE*;>`Q(}Iv; zS5hZ}Q_#_C#6v6r?0i+otcxuiX=xF3moDnk=kf;j1C}kZ*Oj1a*3IIfaK|9A<%&G( zsoCn$^09DF)T2~HdbXm>HlTafRw@b|swg?K>egH{M01^wK}fgNxOdcga8IhZ*_sew zpZHW371g0N*V_q+*sATWsG3Vx@V9}J081dx0{i=Q%aDx4n*;1lQtjZG(}(6!>hoN* zJr&RYjjy=`D8Y6wktFFSWnl^S=YWksmX(*qb7V#U2$@H%KiYJ-rwJ-1+#k zU%&ix=e-*{SHAe=jazrGe(*>Pt|@*eD88P$89&AB-s3C$Hu4M1ucWUwIec2D(f>sN zM=?q>g_6N9siRP%yjZm88MOFno9SYqZ0&FKf@V=Za`-*iyT$t#*}Jf4(B)}8Z{=TM zU43nt1eE_8*?pkH6Z)w&vE3B7=3Db^ha=n3PX3SI^C?09n-jZ!(i~)yeg9k<7#|^D z4U8WXzv=B7?^8A#MUXd}J)qv~5KxY)D0laRyw!&KtvKqp5~$zmlTkh7r>C>17bt2~DCp&ZaWgvk5u$zP3x?abti#zP9e+uI7UuzqH>`=JO>zU#66G zW|J);jOWu#YGjM%%#1`+m>+o^LAx zf8T@J9j#UbWZixB;^t0F`x+j^_PSv?J_9>p1&+W<=)mi+7FObqumV=$ex^iU65+db zDv*DW6Q5;HXSj-{YNYbBNF-ZFw%*SU3FrigH=@czrp#Y-$cpGBVNFwo&QsgZnMAFS zQVb3IM^OMLY-Ym~2*5m$*)WTk8!*Ho7RO9Q_<>p7s*t&Gfy{?Bh?5ly0P#y~NGU^^ zL5f8tB#cF6ZpJae(%$HjxiwOYt`J8utrJ}K4S*3P_LdsmN|{>#OH4irf+;BT|5f1g zT!<&%JyL-cWFCJ%Z=AN0ykeP?`CH8ZNW{uqh!1f{mC-5Yb~=im>UUiwHy8$K1JD?vo}}42*BNa|nym9(v99>-J(D9UCe^yA zJKhoRNi(q6lC+e~$!Wlr>(ea$>WUP?5?j*dh`Wfd%0ztQ?&C*%v+iR@d@Jq&XvS*K z9U;Yy4ikM*{Lph8zS~~)tTN5#_;$E8ms0WBkEufpPrHTcy7wz;^fpWK1UIFCyal5dZ)H delta 704 zcmey-%DAD0k@qw&FBbz4yvW>^!8nokDC3ifm!lY!C+jhWGOA2&VN_>SoxFz8m{D!= z14dOw^~niL(t^@0tWg4-3^16YG1-Vo7c8f0u9?D_%ACRl!~#Gpn97{Sk)jAxFPwr+ zMhPe*QpvBWwfO+kL1U(5*2xDIL~Q&R85pKB)G#-2)vz>hq%hU8)-cvEr7+K7s%6Wo zsbOwZD*>v2fEwo2P1AXdro;~mKx^CjpoAA$m&?&>R5oLg4JcS*09WGn9Br{ zVPvRe1ez{d!_p{R!(79X?ZUuN1XNwaT+33+oWcfi2S_G`eGUuIEuuixoGBbI@f6NE z%s^3~s~b5|xS*meDcm5@$p^W$#Ohh=*;07c@UCWtI6jy`ldp8&WJimZ$@}GOnf+7cSBHQf;>=3qFoZB=;R}IbxcLAlfCTa z7|(3Zv|q)fWXKxCxFPui1BgDr453drL+A^!311itC)+tmFm_K4ck+|!13AYHL;ypb h;TMNZZhlH>PO4qel*#*@Gz48Z7=su;F@PzsG63;Tt@8i? diff --git a/core/db/db_binance_data.py b/core/db/db_binance_data.py index c4ea848..cf967c2 100644 --- a/core/db/db_binance_data.py +++ b/core/db/db_binance_data.py @@ -7,68 +7,65 @@ logger = logging.logger class DBBinanceData: - def __init__( - self, - db_url: str - ): + def __init__(self, db_url: str): self.db_url = db_url self.table_name = "crypto_binance_data" self.columns = [ - "symbol", - "bar", - "timestamp", - "date_time", - "date_time_us", - "open", - "high", - "low", - "close", - "pre_close", - "close_change", - "pct_chg", - "volume", - "volCcy", - "volCCyQuote", - "buy_sz", - "sell_sz", - # 技术指标字段 - "ma1", - "ma2", - "dif", - "dea", - "macd", - "macd_signal", - "macd_divergence", - "kdj_k", - "kdj_d", - "kdj_j", - "kdj_signal", - "kdj_pattern", - "sar", - "sar_signal", - "ma5", - "ma10", - "ma20", - "ma30", - "ma_cross", - "ma5_close_diff", - "ma10_close_diff", - "ma20_close_diff", - "ma30_close_diff", - "ma_close_avg", - "ma_long_short", - "ma_divergence", - "rsi_14", - "rsi_signal", - "boll_upper", - "boll_middle", - "boll_lower", - "boll_signal", - "boll_pattern", - "k_length", - "k_shape", - "k_up_down", - "create_time", + "symbol", + "bar", + "timestamp", + "date_time", + "date_time_us", + "open", + "high", + "low", + "close", + "pre_close", + "close_change", + "pct_chg", + "volume", + "volCcy", + "volCCyQuote", + "buy_sz", + "sell_sz", + # 技术指标字段 + "ma1", + "ma2", + "dif", + "dea", + "macd", + "macd_signal", + "macd_divergence", + "kdj_k", + "kdj_d", + "kdj_j", + "kdj_signal", + "kdj_pattern", + "sar", + "sar_signal", + "ma5", + "ma10", + "ma20", + "ma30", + "ma_cross", + "ma5_close_diff", + "ma10_close_diff", + "ma20_close_diff", + "ma30_close_diff", + "ma_close_avg", + "ma_long_short", + "ma_divergence", + "rsi_14", + "rsi_signal", + "boll_upper", + "boll_middle", + "boll_lower", + "boll_signal", + "boll_pattern", + "k_length", + "k_shape", + "k_up_down", + "create_time", ] self.db_manager = DBData(db_url, self.table_name, self.columns) @@ -85,7 +82,7 @@ class DBBinanceData: return self.db_manager.insert_data_to_mysql(df) - + def insert_data_to_mysql_fast(self, df: pd.DataFrame): """ 快速插入K线行情数据(方案2:使用executemany批量插入) @@ -97,9 +94,9 @@ class DBBinanceData: if df is None or df.empty: logger.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): """ 分块插入K线行情数据(方案3:适合大数据量) @@ -112,9 +109,9 @@ class DBBinanceData: if df is None or df.empty: logger.warning("DataFrame为空,无需写入数据库。") return - + self.db_manager.insert_data_to_mysql_chunk(df, chunk_size) - + def insert_data_to_mysql_simple(self, df: pd.DataFrame): """ 简单插入K线行情数据(方案4:直接使用to_sql,忽略重复) @@ -125,9 +122,9 @@ class DBBinanceData: if df is None or df.empty: logger.warning("DataFrame为空,无需写入数据库。") return - + self.db_manager.insert_data_to_mysql_simple(df) - + def query_latest_data(self, symbol: str, bar: str): """ 查询最新数据 @@ -142,8 +139,10 @@ class DBBinanceData: """ condition_dict = {"symbol": symbol, "bar": bar} return self.db_manager.query_data(sql, condition_dict, return_multi=False) - - def query_data_before_timestamp(self, symbol: str, bar: str, timestamp: int, limit: int = 100): + + def query_data_before_timestamp( + self, symbol: str, bar: str, timestamp: int, limit: int = 100 + ): """ 根据时间戳查询之前的数据 :param symbol: 交易对 @@ -157,20 +156,25 @@ class DBBinanceData: ORDER BY timestamp DESC LIMIT :limit """ - condition_dict = {"symbol": symbol, "bar": bar, "timestamp": timestamp, "limit": limit} + condition_dict = { + "symbol": symbol, + "bar": bar, + "timestamp": timestamp, + "limit": limit, + } return self.db_manager.query_data(sql, condition_dict, return_multi=True) - + def query_data_by_technical_indicators( - self, - symbol: str, - bar: str, - start: str = None, + self, + symbol: str, + bar: str, + start: str = None, end: str = None, macd_signal: str = None, kdj_signal: str = None, rsi_signal: str = None, boll_signal: str = None, - ma_cross: str = None + ma_cross: str = None, ): """ 根据技术指标查询数据 @@ -186,7 +190,7 @@ class DBBinanceData: """ conditions = ["symbol = :symbol", "bar = :bar"] condition_dict = {"symbol": symbol, "bar": bar} - + if macd_signal: conditions.append("macd_signal = :macd_signal") condition_dict["macd_signal"] = macd_signal @@ -202,7 +206,7 @@ class DBBinanceData: if ma_cross: conditions.append("ma_cross = :ma_cross") condition_dict["ma_cross"] = ma_cross - + # 处理时间范围 if start: start_timestamp = transform_date_time_to_timestamp(start) @@ -214,23 +218,23 @@ class DBBinanceData: if end_timestamp: conditions.append("timestamp <= :end") condition_dict["end"] = end_timestamp - + where_clause = " AND ".join(conditions) sql = f""" SELECT * FROM crypto_binance_data WHERE {where_clause} ORDER BY timestamp DESC """ - + return self.db_manager.query_data(sql, condition_dict, return_multi=True) - + def query_macd_signals( - self, - symbol: str, - bar: str, + self, + symbol: str, + bar: str, signal: str = None, - start: str = None, - end: str = None + start: str = None, + end: str = None, ): """ 查询MACD信号数据 @@ -242,11 +246,11 @@ class DBBinanceData: """ conditions = ["symbol = :symbol", "bar = :bar"] condition_dict = {"symbol": symbol, "bar": bar} - + if signal: conditions.append("macd_signal = :signal") condition_dict["signal"] = signal - + # 处理时间范围 if start: start_timestamp = transform_date_time_to_timestamp(start) @@ -258,24 +262,24 @@ class DBBinanceData: if end_timestamp: conditions.append("timestamp <= :end") condition_dict["end"] = end_timestamp - + where_clause = " AND ".join(conditions) sql = f""" SELECT * FROM crypto_binance_data WHERE {where_clause} ORDER BY timestamp DESC """ - + return self.db_manager.query_data(sql, condition_dict, return_multi=True) - + def query_kdj_signals( - self, - symbol: str, - bar: str, + self, + symbol: str, + bar: str, signal: str = None, pattern: str = None, - start: str = None, - end: str = None + start: str = None, + end: str = None, ): """ 查询KDJ信号数据 @@ -288,14 +292,14 @@ class DBBinanceData: """ conditions = ["symbol = :symbol", "bar = :bar"] condition_dict = {"symbol": symbol, "bar": bar} - + if signal: conditions.append("kdj_signal = :signal") condition_dict["signal"] = signal if pattern: conditions.append("kdj_pattern = :pattern") condition_dict["pattern"] = pattern - + # 处理时间范围 if start: start_timestamp = transform_date_time_to_timestamp(start) @@ -307,25 +311,25 @@ class DBBinanceData: if end_timestamp: conditions.append("timestamp <= :end") condition_dict["end"] = end_timestamp - + where_clause = " AND ".join(conditions) sql = f""" SELECT * FROM crypto_binance_data WHERE {where_clause} ORDER BY timestamp DESC """ - + return self.db_manager.query_data(sql, condition_dict, return_multi=True) - + def query_ma_signals( - self, - symbol: str, - bar: str, + self, + symbol: str, + bar: str, cross: str = None, long_short: str = None, divergence: str = None, - start: str = None, - end: str = None + start: str = None, + end: str = None, ): """ 查询均线信号数据 @@ -339,7 +343,7 @@ class DBBinanceData: """ conditions = ["symbol = :symbol", "bar = :bar"] condition_dict = {"symbol": symbol, "bar": bar} - + if cross: conditions.append("ma_cross = :cross") condition_dict["cross"] = cross @@ -349,7 +353,7 @@ class DBBinanceData: if divergence: conditions.append("ma_divergence = :divergence") condition_dict["divergence"] = divergence - + # 处理时间范围 if start: start_timestamp = transform_date_time_to_timestamp(start) @@ -361,24 +365,24 @@ class DBBinanceData: if end_timestamp: conditions.append("timestamp <= :end") condition_dict["end"] = end_timestamp - + where_clause = " AND ".join(conditions) sql = f""" SELECT * FROM crypto_binance_data WHERE {where_clause} ORDER BY timestamp DESC """ - + return self.db_manager.query_data(sql, condition_dict, return_multi=True) - + def query_bollinger_signals( - self, - symbol: str, - bar: str, + self, + symbol: str, + bar: str, signal: str = None, pattern: str = None, - start: str = None, - end: str = None + start: str = None, + end: str = None, ): """ 查询布林带信号数据 @@ -391,14 +395,14 @@ class DBBinanceData: """ conditions = ["symbol = :symbol", "bar = :bar"] condition_dict = {"symbol": symbol, "bar": bar} - + if signal: conditions.append("boll_signal = :signal") condition_dict["signal"] = signal if pattern: conditions.append("boll_pattern = :pattern") condition_dict["pattern"] = pattern - + # 处理时间范围 if start: start_timestamp = transform_date_time_to_timestamp(start) @@ -410,22 +414,18 @@ class DBBinanceData: if end_timestamp: conditions.append("timestamp <= :end") condition_dict["end"] = end_timestamp - + where_clause = " AND ".join(conditions) sql = f""" SELECT * FROM crypto_binance_data WHERE {where_clause} ORDER BY timestamp DESC """ - + return self.db_manager.query_data(sql, condition_dict, return_multi=True) - + def get_technical_statistics( - self, - symbol: str, - bar: str, - start: str = None, - end: str = None + self, symbol: str, bar: str, start: str = None, end: str = None ): """ 获取技术指标统计信息 @@ -436,7 +436,7 @@ class DBBinanceData: """ conditions = ["symbol = :symbol", "bar = :bar"] condition_dict = {"symbol": symbol, "bar": bar} - + # 处理时间范围 if start: start_timestamp = transform_date_time_to_timestamp(start) @@ -448,7 +448,7 @@ class DBBinanceData: if end_timestamp: conditions.append("timestamp <= :end") condition_dict["end"] = end_timestamp - + where_clause = " AND ".join(conditions) sql = f""" SELECT @@ -470,20 +470,31 @@ class DBBinanceData: FROM crypto_binance_data WHERE {where_clause} """ - + return self.db_manager.query_data(sql, condition_dict, return_multi=False) - - def query_market_data_by_symbol_bar(self, symbol: str, bar: str, start: str = None, end: str = None): + + def query_market_data_by_symbol_bar( + self, + symbol: str, + bar: str, + fields: list = None, + start: str = None, + end: str = None, + ): """ 根据交易对和K线周期查询数据 :param symbol: 交易对 :param bar: K线周期 + :param fields: 字段列表 :param start: 开始时间 :param end: 结束时间 """ + if fields is None: + fields = ["*"] + fields_str = ", ".join(fields) if start is None and end is None: - sql = """ - SELECT * FROM crypto_binance_data + sql = f""" + SELECT {fields_str} FROM crypto_binance_data WHERE symbol = :symbol AND bar = :bar ORDER BY timestamp ASC """ @@ -502,24 +513,29 @@ class DBBinanceData: if start is not None and end is not None: if start > end: start, end = end, start - sql = """ - SELECT * FROM crypto_binance_data + sql = f""" + SELECT {fields_str} FROM crypto_binance_data WHERE symbol = :symbol AND bar = :bar AND timestamp BETWEEN :start AND :end ORDER BY timestamp ASC """ - condition_dict = {"symbol": symbol, "bar": bar, "start": start, "end": end} + condition_dict = { + "symbol": symbol, + "bar": bar, + "start": start, + "end": end, + } elif start is not None: - sql = """ - SELECT * FROM crypto_binance_data + sql = f""" + SELECT {fields_str} FROM crypto_binance_data WHERE symbol = :symbol AND bar = :bar AND timestamp >= :start ORDER BY timestamp ASC """ condition_dict = {"symbol": symbol, "bar": bar, "start": start} elif end is not None: - sql = """ - SELECT * FROM crypto_binance_data + sql = f""" + SELECT {fields_str} FROM crypto_binance_data WHERE symbol = :symbol AND bar = :bar AND timestamp <= :end ORDER BY timestamp ASC """ condition_dict = {"symbol": symbol, "bar": bar, "end": end} - return self.db_manager.query_data(sql, condition_dict, return_multi=True) \ No newline at end of file + return self.db_manager.query_data(sql, condition_dict, return_multi=True) diff --git a/core/db/db_manager.py b/core/db/db_manager.py index dc05b03..1a64776 100644 --- a/core/db/db_manager.py +++ b/core/db/db_manager.py @@ -218,13 +218,6 @@ class DBData: :param db_url: 数据库连接URL """ try: - engine = create_engine( - self.db_url, - pool_size=5, # 连接池大小 - max_overflow=10, # 允许的最大溢出连接 - pool_timeout=30, # 连接超时时间(秒) - pool_recycle=1800, # 连接回收时间(秒),避免长时间闲置 - ) with self.db_engine.connect() as conn: result = conn.execute(text(sql), condition_dict) if return_multi: diff --git a/core/db/db_market_data.py b/core/db/db_market_data.py index 65c1c17..adff462 100644 --- a/core/db/db_market_data.py +++ b/core/db/db_market_data.py @@ -473,17 +473,21 @@ class DBMarketData: return self.db_manager.query_data(sql, condition_dict, return_multi=False) - def query_market_data_by_symbol_bar(self, symbol: str, bar: str, start: str = None, end: str = None): + def query_market_data_by_symbol_bar(self, symbol: str, bar: str, fields: list = None, start: str = None, end: str = None): """ 根据交易对和K线周期查询数据 :param symbol: 交易对 :param bar: K线周期 + :param fields: 字段列表 :param start: 开始时间 :param end: 结束时间 """ + if fields is None: + fields = ["*"] + fields_str = ", ".join(fields) if start is None and end is None: - sql = """ - SELECT * FROM crypto_market_data + sql = f""" + SELECT {fields_str} FROM crypto_market_data WHERE symbol = :symbol AND bar = :bar ORDER BY timestamp ASC """ @@ -502,22 +506,22 @@ class DBMarketData: if start is not None and end is not None: if start > end: start, end = end, start - sql = """ - SELECT * FROM crypto_market_data + sql = f""" + SELECT {fields_str} FROM crypto_market_data WHERE symbol = :symbol AND bar = :bar AND timestamp BETWEEN :start AND :end ORDER BY timestamp ASC """ condition_dict = {"symbol": symbol, "bar": bar, "start": start, "end": end} elif start is not None: - sql = """ - SELECT * FROM crypto_market_data + sql = f""" + SELECT {fields_str} FROM crypto_market_data WHERE symbol = :symbol AND bar = :bar AND timestamp >= :start ORDER BY timestamp ASC """ condition_dict = {"symbol": symbol, "bar": bar, "start": start} elif end is not None: - sql = """ - SELECT * FROM crypto_market_data + sql = f""" + SELECT {fields_str} FROM crypto_market_data WHERE symbol = :symbol AND bar = :bar AND timestamp <= :end ORDER BY timestamp ASC """ diff --git a/core/trade/__pycache__/orb_trade.cpython-312.pyc b/core/trade/__pycache__/orb_trade.cpython-312.pyc index 1c695d07facbbcdfcdb3650a4159d2658ff55238..1f5b9b166b23e6b22b29d1ac82e44a87f35db13e 100644 GIT binary patch delta 11221 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 Lx=(JCV_p9r(n;7% delta 7994 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 diff --git a/core/trade/orb_trade.py b/core/trade/orb_trade.py index 2d60af7..fa26987 100644 --- a/core/trade/orb_trade.py +++ b/core/trade/orb_trade.py @@ -9,10 +9,12 @@ from openpyxl.drawing.image import Image import openpyxl from openpyxl.styles import Font from PIL import Image as PILImage +from datetime import datetime, timedelta import core.logger as logging from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE from core.db.db_market_data import DBMarketData +from core.db.db_binance_data import DBBinanceData from core.db.db_huge_volume_data import DBHugeVolumeData from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp @@ -35,11 +37,14 @@ class ORBStrategy: commission_per_share=0.0005, profit_target_multiple=10, is_us_stock=False, + is_binance=False, direction=None, by_sar=False, symbol_bar_data=None, + symbol_1h_data=None, price_range_mean_as_R=False, by_big_k=False, + by_1h_k=False, ): """ 初始化ORB策略参数 @@ -63,10 +68,14 @@ class ORBStrategy: :param commission_per_share: 每股交易佣金(美元,默认0.0005) :param profit_target_multiple: 盈利目标倍数(默认10倍$R,即10R) :param is_us_stock: 是否是美股 + :param is_binance: 是否是Binance :param direction: 方向,None=自动,Long=多头,Short=空头 :param by_sar: 是否根据SAR指标生成信号,True=是,False=否 + :param symbol_bar_data: 5分钟K线数据 + :param symbol_1h_data: 1小时K线数据 :param price_range_mean_as_R: 是否将价格振幅均值作为$R,True=是,False=否 :param by_big_k: 是否根据K线实体部分,亦即abs(open-close)超过high-low的50%,True=是,False=否 + :param by_1h_k: 是否根据1小时K线,True=是,False=否 """ logger.info( f"初始化ORB策略参数:股票代码={symbol},K线周期={bar},开始日期={start_date},结束日期={end_date},初始账户资金={initial_capital},最大杠杆倍数={max_leverage},单次交易风险比例={risk_per_trade},每股交易佣金={commission_per_share}" @@ -90,10 +99,15 @@ class ORBStrategy: mysql_host = MYSQL_CONFIG.get("host", "localhost") mysql_port = MYSQL_CONFIG.get("port", 3306) mysql_database = MYSQL_CONFIG.get("database", "okx") + self.is_us_stock = is_us_stock + self.is_binance = is_binance self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" - self.db_market_data = DBMarketData(self.db_url) - self.is_us_stock = is_us_stock + if self.is_binance: + self.db_market_data = DBBinanceData(self.db_url) + else: + self.db_market_data = DBMarketData(self.db_url) + self.output_chart_folder = r"./output/trade_sandbox/orb_strategy/chart/" self.output_excel_folder = r"./output/trade_sandbox/orb_strategy/excel/" os.makedirs(self.output_chart_folder, exist_ok=True) @@ -110,6 +124,7 @@ class ORBStrategy: if self.by_sar: self.sar_desc = "考虑SAR" self.symbol_bar_data = symbol_bar_data + self.symbol_1h_data = symbol_1h_data self.price_range_mean_as_R = price_range_mean_as_R if self.price_range_mean_as_R: self.price_range_mean_as_R_desc = "R为振幅均值" @@ -121,6 +136,11 @@ class ORBStrategy: self.by_big_k_desc = "K线实体过50%" else: self.by_big_k_desc = "无K线要求" + self.by_1h_k = by_1h_k + if self.by_1h_k: + self.by_1h_k_desc = "参照1小时K线" + else: + self.by_1h_k_desc = "不参照1小时K线" def run(self): """ @@ -132,7 +152,12 @@ class ORBStrategy: if len(self.trades) > 0: self.plot_equity_curve() self.output_trade_summary() - return self.symbol_bar_data, self.trades_df, self.trades_summary_df + return ( + self.symbol_bar_data, + self.symbol_1h_data, + self.trades_df, + self.trades_summary_df, + ) def fetch_intraday_data(self): """ @@ -146,47 +171,10 @@ class ORBStrategy: f"开始获取{self.symbol}数据:{self.start_date}至{self.end_date},间隔{self.bar}" ) if self.symbol_bar_data is None or len(self.symbol_bar_data) == 0: - data = self.db_market_data.query_market_data_by_symbol_bar( - self.symbol, self.bar, start=self.start_date, end=self.end_date - ) - data = pd.DataFrame(data) - data.sort_values(by="date_time", inplace=True) - # 保留核心列:开盘价、最高价、最低价、收盘价、成交量 - data["Open"] = data["open"] - data["High"] = data["high"] - data["Low"] = data["low"] - data["Close"] = data["close"] - data["Volume"] = data["volume"] - if self.is_us_stock: - date_time_field = "date_time_us" - else: - date_time_field = "date_time" - data[date_time_field] = pd.to_datetime(data[date_time_field]) - # data["Date"]为日期,不包括时分秒,即date_time如果是2025-01-01 10:00:00,则Date为2025-01-01 - data["Date"] = data[date_time_field].dt.date - # 将Date转换为datetime64[ns]类型以确保类型一致 - data["Date"] = pd.to_datetime(data["Date"]) - # 最小data["Date"] - self.start_date = data["Date"].min().strftime("%Y-%m-%d") - # 最大data["Date"] - self.end_date = data["Date"].max().strftime("%Y-%m-%d") - self.data = data[ - [ - "symbol", - "bar", - "Date", - date_time_field, - "Open", - "High", - "Low", - "Close", - "Volume", - "sar_signal", - ] - ].copy() - self.data.rename(columns={date_time_field: "date_time"}, inplace=True) + self.data = self.get_full_data(bar=self.bar) self.calculate_price_range_mean() self.symbol_bar_data = self.data.copy() + self.symbol_1h_data = self.get_full_data(bar="1H") else: self.data = self.symbol_bar_data.copy() @@ -210,6 +198,91 @@ class ORBStrategy: f"成功获取{self.symbol}数据:{len(self.data)}根{self.bar}K线,开始日期={self.start_date},结束日期={self.end_date}" ) + def get_full_data(self, bar: str = "5m"): + """ + 分段获取数据,并将数据合并为完整数据 + 分段依据:如果end_date与start_date相差超过一年,则每次取一年数据 + """ + data = pd.DataFrame() + start_date = datetime.strptime(self.start_date, "%Y-%m-%d") + end_date = datetime.strptime(self.end_date, "%Y-%m-%d") + timedelta(days=1) + fields = [ + "symbol", + "bar", + "date_time", + "date_time_us", + "open", + "high", + "low", + "close", + "volume", + "sar_signal", + "ma5", + "ma10", + "ma20", + "ma30", + "dif", + "macd", + ] + while start_date < end_date: + current_end_date = min(start_date + timedelta(days=180), end_date) + start_date_str = start_date.strftime("%Y-%m-%d") + current_end_date_str = current_end_date.strftime("%Y-%m-%d") + logger.info( + f"获取{self.symbol}数据:{start_date_str}至{current_end_date_str}" + ) + current_data = self.db_market_data.query_market_data_by_symbol_bar( + self.symbol, bar, fields, start=start_date_str, end=current_end_date_str + ) + if current_data is not None and len(current_data) > 0: + current_data = pd.DataFrame(current_data) + data = pd.concat([data, current_data]) + start_date = current_end_date + data.drop_duplicates(inplace=True) + if self.is_us_stock: + date_time_field = "date_time_us" + else: + date_time_field = "date_time" + data.sort_values(by=date_time_field, inplace=True) + data.reset_index(drop=True, inplace=True) + # 保留核心列:开盘价、最高价、最低价、收盘价、成交量 + data["Open"] = data["open"] + data["High"] = data["high"] + data["Low"] = data["low"] + data["Close"] = data["close"] + data["Volume"] = data["volume"] + data[date_time_field] = pd.to_datetime(data[date_time_field]) + # data["Date"]为日期,不包括时分秒,即date_time如果是2025-01-01 10:00:00,则Date为2025-01-01 + data["Date"] = data[date_time_field].dt.date + # 将Date转换为datetime64[ns]类型以确保类型一致 + data["Date"] = pd.to_datetime(data["Date"]) + # 最小data["Date"] + self.start_date = data["Date"].min().strftime("%Y-%m-%d") + # 最大data["Date"] + self.end_date = data["Date"].max().strftime("%Y-%m-%d") + data = data[ + [ + "symbol", + "bar", + "Date", + date_time_field, + "Open", + "High", + "Low", + "Close", + "Volume", + "sar_signal", + "ma5", + "ma10", + "ma20", + "ma30", + "dif", + "macd", + ] + ] + data.rename(columns={date_time_field: "date_time"}, inplace=True) + return data + def calculate_shares(self, account_value, entry_price, stop_price, risk_assumed): """ 根据ORB公式计算交易股数 @@ -247,7 +320,7 @@ class ORBStrategy: - 第二根5分钟K线:根据第一根K线方向生成多空信号 """ logger.info( - f"开始生成ORB策略信号:{self.direction_desc},根据SAR指标:{self.by_sar}" + f"开始生成ORB策略信号:{self.direction_desc},根据SAR指标:{self.by_sar},{self.by_1h_k_desc}" ) if self.data is None: raise ValueError("请先调用fetch_intraday_data获取数据") @@ -261,6 +334,7 @@ class ORBStrategy: # 第一根5分钟K线(开盘区间) first_candle = daily_data.iloc[0] + current_date = first_candle["Date"] high1 = first_candle["High"] low1 = first_candle["Low"] open1 = first_candle["Open"] @@ -273,6 +347,25 @@ class ORBStrategy: if (abs(open1 - close1) / (high1 - low1)) < 0.5: continue + ma5_1h = None + ma10_1h = None + dif_1h = None + macd_1h = None + if self.by_1h_k: + if self.symbol_1h_data is None or len(self.symbol_1h_data) == 0: + continue + if len(self.symbol_1h_data) < 2: + continue + symbol_1h_date_data = self.symbol_1h_data[ + self.symbol_1h_data["Date"] == current_date + ] + if len(symbol_1h_date_data) > 0: + first_candle_1h = symbol_1h_date_data.iloc[0] + ma5_1h = first_candle_1h["ma5"] + ma10_1h = first_candle_1h["ma10"] + dif_1h = first_candle_1h["dif"] + macd_1h = first_candle_1h["macd"] + # 第二根5分钟K线(entry信号) second_candle = daily_data.iloc[1] entry_price = second_candle["Open"] # entry价格=第二根K线开盘价 @@ -283,6 +376,22 @@ class ORBStrategy: open1 < close1 and (self.direction == "Long" or self.direction is None) and ((self.by_sar and sar_signal == "SAR多头") or not self.by_sar) + and ( + ( + self.by_1h_k + and ( + ma5_1h is not None + and ma10_1h is not None + and ma5_1h > ma10_1h + ) + and ( + dif_1h is not None + and macd_1h is not None + and (dif_1h > 0 or macd_1h > 0) + ) + ) + or not self.by_1h_k + ) ): # 第一根K线收涨→多头信号 signal = "Long" @@ -291,6 +400,22 @@ class ORBStrategy: open1 > close1 and (self.direction == "Short" or self.direction is None) and ((self.by_sar and sar_signal == "SAR空头") or not self.by_sar) + and ( + ( + self.by_1h_k + and ( + ma5_1h is not None + and ma10_1h is not None + and ma5_1h < ma10_1h + ) + and ( + dif_1h is not None + and macd_1h is not None + and (dif_1h < 0 or macd_1h < 0) + ) + ) + or not self.by_1h_k + ) ): # 第一根K线收跌→空头信号 signal = "Short" @@ -477,6 +602,7 @@ class ORBStrategy: "BySar": self.sar_desc, "PriceRangeMeanAsR": self.price_range_mean_as_R_desc, "ByBigK": self.by_big_k_desc, + "By1hK": self.by_1h_k_desc, "Symbol": self.symbol, "Bar": self.bar, "Date": date, @@ -576,6 +702,7 @@ class ORBStrategy: self.trades_summary["根据SAR"] = self.sar_desc self.trades_summary["R算法"] = self.price_range_mean_as_R_desc self.trades_summary["K线条件"] = self.by_big_k_desc + self.trades_summary["1小时K线条件"] = self.by_1h_k_desc self.trades_summary["股票代码"] = self.symbol self.trades_summary["K线周期"] = self.bar self.trades_summary["开始日期"] = self.start_date @@ -660,7 +787,7 @@ class ORBStrategy: markersize=4, ) plt.title( - f"{symbol} {bar} {self.direction_desc} {self.sar_desc} {self.price_range_mean_as_R_desc} {self.by_big_k_desc}", + f"{symbol} {bar} {self.direction_desc} {self.sar_desc} {self.price_range_mean_as_R_desc} {self.by_big_k_desc} {self.by_1h_k_desc}", fontsize=14, fontweight="bold", ) @@ -704,7 +831,7 @@ class ORBStrategy: fontsize=10, ) plt.tight_layout() - self.chart_save_path = f"{self.output_chart_folder}/{symbol}_{bar}_{self.direction_desc}_{self.sar_desc}_{self.price_range_mean_as_R_desc}_{self.by_big_k_desc}_orb.png" + self.chart_save_path = f"{self.output_chart_folder}/{symbol}_{bar}_{self.direction_desc}_{self.sar_desc}_{self.price_range_mean_as_R_desc}_{self.by_big_k_desc}_{self.by_1h_k_desc}_orb.png" plt.savefig(self.chart_save_path, dpi=150, bbox_inches="tight") plt.close() @@ -714,7 +841,7 @@ class ORBStrategy: """ start_date = self.start_date.replace("-", "") end_date = self.end_date.replace("-", "") - output_file_name = f"orb_{self.symbol}_{self.bar}_{start_date}_{end_date}_{self.direction_desc}_{self.sar_desc}_{self.price_range_mean_as_R_desc}_{self.by_big_k_desc}.xlsx" + output_file_name = f"orb_{self.symbol}_{self.bar}_{start_date}_{end_date}_{self.direction_desc}_{self.sar_desc}_{self.price_range_mean_as_R_desc}_{self.by_big_k_desc}_{self.by_1h_k_desc}.xlsx" output_file_path = os.path.join(self.output_excel_folder, output_file_name) logger.info(f"导出{output_file_path}") with pd.ExcelWriter(output_file_path) as writer: diff --git a/huge_volume_main.py b/huge_volume_main.py index 790fb0d..b78b537 100644 --- a/huge_volume_main.py +++ b/huge_volume_main.py @@ -678,9 +678,9 @@ def batch_import_binance_data_by_csv(): def test_import_binance_data_by_csv(): huge_volume_main = HugeVolumeMain(threshold=2.0, is_us_stock=False, is_binance=True) - file_path = "./data/binance/spot/2020-08-11/SOL-USDT_1h.csv" + file_path = "./data/binance/spot/2020-08-12/XRP-USDT_1h.csv" huge_volume_main.import_binance_data_by_csv( - file_path, "SOL-USDT", "1H", [50, 80, 100, 120] + file_path, "XRP-USDT", "1H", [50, 80, 100, 120] ) @@ -696,8 +696,8 @@ def test_send_huge_volume_data_to_wechat(): if __name__ == "__main__": - test_import_binance_data_by_csv() - # batch_import_binance_data_by_csv() + # test_import_binance_data_by_csv() + batch_import_binance_data_by_csv() # batch_update_volume_spike(threshold=2.0, is_us_stock=True) # test_send_huge_volume_data_to_wechat() # batch_initial_detect_volume_spike(threshold=2.0) diff --git a/orb_trade_main.py b/orb_trade_main.py index aa2ea03..9360c37 100644 --- a/orb_trade_main.py +++ b/orb_trade_main.py @@ -1,5 +1,5 @@ from core.trade.orb_trade import ORBStrategy -from config import US_STOCK_MONITOR_CONFIG, OKX_MONITOR_CONFIG +from config import US_STOCK_MONITOR_CONFIG, OKX_MONITOR_CONFIG, BINANCE_MONITOR_CONFIG import core.logger as logging from datetime import datetime from openpyxl import Workbook @@ -12,13 +12,21 @@ logger = logging.logger def main(): - is_us_stock_list = [True, False] + # is_us_stock_list = [True, False] + is_us_stock_list = [False] + is_binance = True bar = "5m" direction_list = [None, "Long", "Short"] by_sar_list = [False, True] price_range_mean_as_R_list = [False, True] by_big_k_list = [False, True] - start_date = "2024-01-01" + by_1h_k_list = [False, True] + if is_binance: + start_date = BINANCE_MONITOR_CONFIG.get("volume_monitor", {}).get("initial_date", "2017-08-16 00:00:00") + if len(start_date) > 10: + start_date = start_date[:10] + else: + start_date = "2024-01-01" end_date = datetime.now().strftime("%Y-%m-%d") profit_target_multiple = 10 initial_capital = 25000 @@ -34,57 +42,68 @@ def main(): for by_sar in by_sar_list: for price_range_mean_as_R in price_range_mean_as_R_list: for by_big_k in by_big_k_list: - if is_us_stock: - symbols = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( - "symbols", ["QQQ"] - ) - else: - symbols = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( - "symbols", ["BTC-USDT"] - ) - for symbol in symbols: - logger.info( - f"开始回测 {symbol}, 交易周期:{bar}, 开始日期:{start_date}, 结束日期:{end_date}, 是否是美股:{is_us_stock}, 交易方向:{direction}, 是否使用SAR:{by_sar}, 是否使用R为entry减stop:{price_range_mean_as_R}, 是否使用K线实体过50%:{by_big_k}" - ) - symbol_bar_data = None - found_symbol_bar_data = False - for symbol_data_dict in symbol_data_cache: - if ( - symbol_data_dict["symbol"] == symbol - and symbol_data_dict["bar"] == bar - ): - symbol_bar_data = symbol_data_dict["data"] - found_symbol_bar_data = True - break - - orb_strategy = ORBStrategy( - symbol=symbol, - bar=bar, - start_date=start_date, - end_date=end_date, - is_us_stock=is_us_stock, - direction=direction, - by_sar=by_sar, - profit_target_multiple=profit_target_multiple, - initial_capital=initial_capital, - max_leverage=max_leverage, - risk_per_trade=risk_per_trade, - commission_per_share=commission_per_share, - symbol_bar_data=symbol_bar_data, - price_range_mean_as_R=price_range_mean_as_R, - by_big_k=by_big_k, - ) - symbol_bar_data, trades_df, trades_summary_df = orb_strategy.run() - if symbol_bar_data is None or len(symbol_bar_data) == 0: - continue - if not found_symbol_bar_data: - symbol_data_cache.append( - {"symbol": symbol, "bar": bar, "data": symbol_bar_data} + for by_1h_k in by_1h_k_list: + if is_us_stock: + symbols = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( + "symbols", ["QQQ"] ) - if trades_summary_df is None or len(trades_summary_df) == 0: - continue - trades_summary_df_list.append(trades_summary_df) - trades_df_list.append(trades_df) + else: + if is_binance: + symbols = BINANCE_MONITOR_CONFIG.get("volume_monitor", {}).get( + "symbols", ["BTC-USDT"] + ) + else: + symbols = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( + "symbols", ["BTC-USDT"] + ) + for symbol in symbols: + logger.info( + f"开始回测 {symbol}, 交易周期:{bar}, 开始日期:{start_date}, 结束日期:{end_date}, 是否是美股:{is_us_stock}, 交易方向:{direction}, 是否使用SAR:{by_sar}, 是否使用R为entry减stop:{price_range_mean_as_R}, 是否使用K线实体过50%:{by_big_k}" + ) + symbol_bar_data = None + symbol_1h_data = None + found_symbol_bar_data = False + for symbol_data_dict in symbol_data_cache: + if ( + symbol_data_dict["symbol"] == symbol + and symbol_data_dict["bar"] == bar + ): + symbol_bar_data = symbol_data_dict["data"] + symbol_1h_data = symbol_data_dict["1h_data"] + found_symbol_bar_data = True + break + + orb_strategy = ORBStrategy( + symbol=symbol, + bar=bar, + start_date=start_date, + end_date=end_date, + is_us_stock=is_us_stock, + is_binance=is_binance, + direction=direction, + by_sar=by_sar, + profit_target_multiple=profit_target_multiple, + initial_capital=initial_capital, + max_leverage=max_leverage, + risk_per_trade=risk_per_trade, + commission_per_share=commission_per_share, + symbol_bar_data=symbol_bar_data, + symbol_1h_data=symbol_1h_data, + price_range_mean_as_R=price_range_mean_as_R, + by_big_k=by_big_k, + by_1h_k=by_1h_k, + ) + symbol_bar_data, symbol_1h_data, trades_df, trades_summary_df = orb_strategy.run() + if symbol_bar_data is None or len(symbol_bar_data) == 0: + continue + if not found_symbol_bar_data: + symbol_data_cache.append( + {"symbol": symbol, "bar": bar, "data": symbol_bar_data, "1h_data": symbol_1h_data} + ) + if trades_summary_df is None or len(trades_summary_df) == 0: + continue + trades_summary_df_list.append(trades_summary_df) + trades_df_list.append(trades_df) total_trades_df = pd.concat(trades_df_list) total_trades_summary_df = pd.concat(trades_summary_df_list) statitics_dict = statistics_summary(total_trades_summary_df) @@ -117,6 +136,9 @@ def main(): statitics_dict["max_total_return_record_df_K_count"].to_excel( writer, sheet_name="最大总收益率_K线条件", index=False ) + statitics_dict["max_total_return_record_df_1hK_count"].to_excel( + writer, sheet_name="最大总收益率_1小时K线条件", index=False + ) chart_path = r"./output/trade_sandbox/orb_strategy/chart/" os.makedirs(chart_path, exist_ok=True) copy_chart_to_excel(chart_path, output_file_path) @@ -170,6 +192,7 @@ def statistics_summary(trades_summary_df: pd.DataFrame): summary["根据SAR"] = max_total_return_record["根据SAR"] summary["R算法"] = max_total_return_record["R算法"] summary["K线条件"] = max_total_return_record["K线条件"] + summary["1小时K线条件"] = max_total_return_record["1小时K线条件"] summary["总收益率%"] = max_total_return_record["总收益率%"] summary["自然收益率%"] = max_total_return_record["自然收益率%"] max_total_return_record_list.append(summary) @@ -192,14 +215,14 @@ def statistics_summary(trades_summary_df: pd.DataFrame): # 其它(如Series、list、dict、ndarray等)转字符串 return str(v) - for key_col in ["方向", "根据SAR", "R算法", "K线条件"]: + for key_col in ["方向", "根据SAR", "R算法", "K线条件", "1小时K线条件"]: if key_col in max_total_return_record_df.columns: max_total_return_record_df[key_col] = max_total_return_record_df[ key_col ].apply(_to_hashable_scalar) # 分组统计 max_total_return_record_df_grouped_count = ( - max_total_return_record_df.groupby(["方向", "根据SAR", "R算法", "K线条件"], dropna=False) + max_total_return_record_df.groupby(["方向", "根据SAR", "R算法", "K线条件", "1小时K线条件"], dropna=False) .size() .reset_index(name="数量") ) @@ -251,18 +274,27 @@ def statistics_summary(trades_summary_df: pd.DataFrame): by="数量", ascending=False, inplace=True ) max_total_return_record_df_K_count.reset_index(drop=True, inplace=True) + + # 统计1小时K线条件的记录数目 + max_total_return_record_df_1hK_count = ( + max_total_return_record_df.groupby(["1小时K线条件"], dropna=False) + .size() + .reset_index(name="数量") + ) + max_total_return_record_df_1hK_count.sort_values( + by="数量", ascending=False, inplace=True + ) + max_total_return_record_df_1hK_count.reset_index(drop=True, inplace=True) else: # 构造空结果,保证下游写入Excel不报错 max_total_return_record_df_grouped_count = pd.DataFrame( - columns=["方向", "根据SAR", "R算法", "K线条件", "数量"] + columns=["方向", "根据SAR", "R算法", "K线条件", "1小时K线条件", "数量"] ) - max_total_return_record_df_direction_count = pd.DataFrame( - columns=["方向", "数量"] - ) + max_total_return_record_df_direction_count = pd.DataFrame(columns=["方向", "数量"]) max_total_return_record_df_sar_count = pd.DataFrame(columns=["根据SAR", "数量"]) max_total_return_record_df_R_count = pd.DataFrame(columns=["R算法", "数量"]) max_total_return_record_df_K_count = pd.DataFrame(columns=["K线条件", "数量"]) - + max_total_return_record_df_1hK_count = pd.DataFrame(columns=["1小时K线条件", "数量"]) result = { "statistics_summary_df": statistics_summary_df, "max_total_return_record_df": max_total_return_record_df, @@ -271,6 +303,7 @@ def statistics_summary(trades_summary_df: pd.DataFrame): "max_total_return_record_df_sar_count": max_total_return_record_df_sar_count, "max_total_return_record_df_R_count": max_total_return_record_df_R_count, "max_total_return_record_df_K_count": max_total_return_record_df_K_count, + "max_total_return_record_df_1hK_count": max_total_return_record_df_1hK_count, } return result diff --git a/play.py b/play.py index 453b77a..621dce7 100644 --- a/play.py +++ b/play.py @@ -2,6 +2,10 @@ import logging from core.biz.quant_trader import QuantTrader from core.biz.strategy import QuantStrategy +from config import MYSQL_CONFIG +from sqlalchemy import create_engine, exc, text +import pandas as pd + logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s: %(message)s') def main() -> None: @@ -94,5 +98,78 @@ def main() -> None: else: logging.warning("无效选择,请重新输入") + +def test_query(): + 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") + db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" + db_engine = create_engine( + db_url, + pool_size=25, # 连接池大小 + max_overflow=10, # 允许的最大溢出连接 + pool_timeout=30, # 连接超时时间(秒) + pool_recycle=60, # 连接回收时间(秒),避免长时间闲置 + ) + sql = "SELECT symbol, min(date_time), max(date_time) FROM okx.crypto_binance_data where bar='5m' group by symbol;" + condition_dict = {} + return_multi = True + try: + result = query_data(db_engine, sql, condition_dict, return_multi) + if result is not None and len(result) > 0: + data = pd.DataFrame(result) + data.columns = ["symbol", "min_date_time", "max_date_time"] + print(data) + # output to excel + data.to_excel("./data/binance/crypto_binance_data_5m.xlsx", index=False) + except Exception as e: + print(f"查询数据出错: {e}") + return None + +def transform_data_type(data: dict): + """ + 遍历字典,将所有Decimal类型的值转换为float类型 + """ + from decimal import Decimal + for key, value in data.items(): + if isinstance(value, Decimal): + data[key] = float(value) + return data + + +def query_data(db_engine, sql: str, condition_dict: dict, return_multi: bool = True): + """ + 查询数据 + :param sql: 查询SQL + :param db_url: 数据库连接URL + """ + try: + with db_engine.connect() as conn: + result = conn.execute(text(sql), condition_dict) + if return_multi: + result = result.fetchall() + if result: + result_list = [ + transform_data_type(dict(row._mapping)) for row in result + ] + return result_list + else: + return None + else: + result = result.fetchone() + if result: + result_dict = transform_data_type(dict(result._mapping)) + return result_dict + else: + return None + except Exception as e: + print(f"查询数据出错: {e}") + return None + if __name__ == "__main__": - main() + # main() + test_query() diff --git a/sql/query/sql_playground.sql b/sql/query/sql_playground.sql index 41b21c5..b63cf22 100644 --- a/sql/query/sql_playground.sql +++ b/sql/query/sql_playground.sql @@ -1,11 +1,24 @@ +use okx; + +select DISTINCT symbol from crypto_huge_volume +where symbol in ("QQQ", "TQQQ", "MSFT", "AAPL", "GOOG", "NVDA", "META", "AMZN", "TSLA", "AVGO") and bar="30m" +and window_size=120 +group by symbol + select * from crypto_market_data -where symbol = "TQQQ" and bar="5m" -order by date_time_us DESC; +where symbol="PLTR" and bar="5m" +order by date_time_us -select * from crypto_huge_volume -where symbol = "QQQ" and bar="5m" -order by date_time; +select * from crypto_market_data +where symbol = "BTC-USDT" and bar="1H" #and open between 114271 and 114272 # AND date_time > "2025-08-21" +order by date_time desc; + +select count(1) from crypto_market_data where +symbol in ("QQQ", "TQQQ", "MSFT", "AAPL", "GOOG", "NVDA", "META", "AMZN", "TSLA", "AVGO") + +select count(1) from crypto_huge_volume where +symbol in ("QQQ", "TQQQ", "MSFT", "AAPL", "GOOG", "NVDA", "META", "AMZN", "TSLA", "AVGO") select symbol, bar, date_time, close, diff --git a/sql/table/crypto_binance_data_essential_indexes.sql b/sql/table/crypto_binance_data_essential_indexes.sql new file mode 100644 index 0000000..3a32c20 --- /dev/null +++ b/sql/table/crypto_binance_data_essential_indexes.sql @@ -0,0 +1,54 @@ +-- crypto_binance_data表核心索引优化脚本 +-- 针对500万+数据量的关键查询性能优化 +-- 基于实际代码中的查询模式分析 + +-- 核心索引1: 主要查询模式索引 +-- 覆盖: WHERE symbol = ? AND bar = ? ORDER BY timestamp +-- 这是代码中最常见的查询模式 +CREATE INDEX idx_symbol_bar_timestamp ON crypto_binance_data (symbol, bar, timestamp); + +-- 核心索引2: 时间范围查询索引 +-- 覆盖: WHERE symbol = ? AND bar = ? AND timestamp BETWEEN ? AND ? +-- 覆盖: WHERE symbol = ? AND bar = ? AND timestamp >= ? AND timestamp <= ? +# CREATE INDEX idx_symbol_bar_timestamp_range ON crypto_binance_data (symbol, bar, timestamp); + +-- 核心索引3: 分组查询索引 +-- 覆盖: GROUP BY symbol, bar +-- 覆盖: SELECT symbol, min(date_time), max(date_time) FROM crypto_binance_data WHERE bar='5m' GROUP BY symbol +CREATE INDEX idx_bar_symbol ON crypto_binance_data (bar, symbol); + +-- 核心索引4: 技术指标查询索引 +-- 覆盖: WHERE symbol = ? AND bar = ? AND macd_signal = ? +-- 覆盖: WHERE symbol = ? AND bar = ? AND kdj_signal = ? +-- 覆盖: WHERE symbol = ? AND bar = ? AND rsi_signal = ? +-- 覆盖: WHERE symbol = ? AND bar = ? AND boll_signal = ? +-- 覆盖: WHERE symbol = ? AND bar = ? AND ma_cross = ? +CREATE INDEX idx_symbol_bar_indicators ON crypto_binance_data (symbol, bar, macd_signal, kdj_signal, rsi_signal, boll_signal, ma_cross); + +-- 核心索引5: 时间戳排序索引 +-- 覆盖: ORDER BY timestamp DESC/ASC +-- 优化: 查询最新数据 ORDER BY timestamp DESC LIMIT 1 +CREATE INDEX idx_timestamp_desc ON crypto_binance_data (timestamp DESC); + +-- 核心索引6: 日期时间查询索引 +-- 覆盖: WHERE date_time >= ? AND date_time <= ? +-- 覆盖: ORDER BY date_time +CREATE INDEX idx_date_time ON crypto_binance_data (date_time); + +-- 核心索引7: 成交量查询索引 +-- 覆盖: WHERE symbol = ? AND bar = ? AND volume > ? +-- 覆盖: ORDER BY volume DESC +CREATE INDEX idx_symbol_bar_volume ON crypto_binance_data (symbol, bar, volume); + +-- 核心索引8: 价格查询索引 +-- 覆盖: WHERE symbol = ? AND bar = ? AND close > ? AND close < ? +-- 覆盖: ORDER BY close DESC/ASC +CREATE INDEX idx_symbol_bar_close ON crypto_binance_data (symbol, bar, close); + +-- 执行前检查现有索引 +-- SHOW INDEX FROM crypto_binance_data; + +-- 执行后验证索引效果 +-- EXPLAIN SELECT * FROM crypto_binance_data WHERE symbol = 'BTC-USDT' AND bar = '5m' ORDER BY timestamp DESC LIMIT 100; +-- EXPLAIN SELECT symbol, min(date_time), max(date_time) FROM crypto_binance_data WHERE bar='5m' GROUP BY symbol; +-- EXPLAIN SELECT * FROM crypto_binance_data WHERE symbol = 'BTC-USDT' AND bar = '5m' AND timestamp BETWEEN 1640995200000 AND 1641081600000; diff --git a/sql/table/crypto_binance_data_indexes.sql b/sql/table/crypto_binance_data_indexes.sql new file mode 100644 index 0000000..2338ac2 --- /dev/null +++ b/sql/table/crypto_binance_data_indexes.sql @@ -0,0 +1,71 @@ +-- crypto_binance_data表索引优化脚本 +-- 针对500万+数据量的查询性能优化 + +-- 1. 主要查询索引 - 覆盖最常见的查询模式 +-- 支持: WHERE symbol = ? AND bar = ? ORDER BY timestamp +CREATE INDEX idx_symbol_bar_timestamp ON crypto_binance_data (symbol, bar, timestamp); + +-- 2. 时间范围查询索引 - 优化时间范围查询 +-- 支持: WHERE symbol = ? AND bar = ? AND timestamp BETWEEN ? AND ? +CREATE INDEX idx_symbol_bar_timestamp_range ON crypto_binance_data (symbol, bar, timestamp); + +-- 3. 按symbol分组查询索引 - 优化GROUP BY symbol查询 +-- 支持: GROUP BY symbol, bar +CREATE INDEX idx_symbol_bar ON crypto_binance_data (symbol, bar); + +-- 4. 时间戳排序索引 - 优化ORDER BY timestamp查询 +-- 支持: ORDER BY timestamp DESC/ASC +CREATE INDEX idx_timestamp ON crypto_binance_data (timestamp); + +-- 5. 技术指标查询索引 - 优化技术指标相关查询 +-- 支持: WHERE symbol = ? AND bar = ? AND macd_signal = ? +CREATE INDEX idx_symbol_bar_macd_signal ON crypto_binance_data (symbol, bar, macd_signal); + +-- 支持: WHERE symbol = ? AND bar = ? AND kdj_signal = ? +CREATE INDEX idx_symbol_bar_kdj_signal ON crypto_binance_data (symbol, bar, kdj_signal); + +-- 支持: WHERE symbol = ? AND bar = ? AND rsi_signal = ? +CREATE INDEX idx_symbol_bar_rsi_signal ON crypto_binance_data (symbol, bar, rsi_signal); + +-- 支持: WHERE symbol = ? AND bar = ? AND boll_signal = ? +CREATE INDEX idx_symbol_bar_boll_signal ON crypto_binance_data (symbol, bar, boll_signal); + +-- 支持: WHERE symbol = ? AND bar = ? AND ma_cross = ? +CREATE INDEX idx_symbol_bar_ma_cross ON crypto_binance_data (symbol, bar, ma_cross); + +-- 6. 复合技术指标查询索引 - 优化多条件技术指标查询 +-- 支持: WHERE symbol = ? AND bar = ? AND macd_signal = ? AND kdj_signal = ? +CREATE INDEX idx_symbol_bar_macd_kdj ON crypto_binance_data (symbol, bar, macd_signal, kdj_signal); + +-- 7. 日期时间查询索引 - 优化按日期时间查询 +-- 支持: WHERE date_time >= ? AND date_time <= ? +CREATE INDEX idx_date_time ON crypto_binance_data (date_time); + +-- 8. 交易对时间索引 - 优化特定交易对的时间查询 +-- 支持: WHERE symbol = ? ORDER BY timestamp +CREATE INDEX idx_symbol_timestamp ON crypto_binance_data (symbol, timestamp); + +-- 9. 周期时间索引 - 优化特定周期的时间查询 +-- 支持: WHERE bar = ? ORDER BY timestamp +CREATE INDEX idx_bar_timestamp ON crypto_binance_data (bar, timestamp); + +-- 10. 统计查询优化索引 - 优化COUNT, AVG等聚合查询 +-- 支持: SELECT COUNT(*) FROM crypto_binance_data WHERE symbol = ? AND bar = ? +CREATE INDEX idx_symbol_bar_volume ON crypto_binance_data (symbol, bar, volume); + +-- 11. 价格相关查询索引 - 优化价格相关查询 +-- 支持: WHERE symbol = ? AND bar = ? AND close > ? AND close < ? +CREATE INDEX idx_symbol_bar_close ON crypto_binance_data (symbol, bar, close); + +-- 12. 成交量相关查询索引 - 优化成交量相关查询 +-- 支持: WHERE symbol = ? AND bar = ? AND volume > ? +CREATE INDEX idx_symbol_bar_volume_high ON crypto_binance_data (symbol, bar, volume); + +-- 删除可能存在的重复或低效索引 +-- 注意:在实际执行前请先检查现有索引,避免删除正在使用的索引 + +-- 查看当前索引状态 +-- SHOW INDEX FROM crypto_binance_data; + +-- 分析查询性能 +-- EXPLAIN SELECT * FROM crypto_binance_data WHERE symbol = 'BTC-USDT' AND bar = '5m' ORDER BY timestamp DESC LIMIT 100; diff --git a/sql/table/crypto_binance_huge_volume_essential_indexes.sql b/sql/table/crypto_binance_huge_volume_essential_indexes.sql new file mode 100644 index 0000000..a358f94 --- /dev/null +++ b/sql/table/crypto_binance_huge_volume_essential_indexes.sql @@ -0,0 +1,69 @@ +-- crypto_binance_huge_volume表核心索引优化脚本 +-- 针对2000万+数据量的关键查询性能优化 +-- 基于实际代码中的查询模式分析 + +-- 核心索引1: 主要查询模式索引 +-- 覆盖: WHERE symbol = ? AND bar = ? AND window_size = ? ORDER BY timestamp +-- 这是代码中最常见的查询模式,用于获取特定交易对、周期、窗口大小的数据 +CREATE INDEX idx_symbol_bar_window_size_timestamp ON crypto_binance_huge_volume (symbol, bar, window_size, timestamp); + +-- 核心索引2: 时间范围查询索引 +-- 覆盖: WHERE symbol = ? AND bar = ? AND window_size = ? AND timestamp BETWEEN ? AND ? +-- 覆盖: WHERE symbol = ? AND bar = ? AND window_size = ? AND timestamp >= ? AND timestamp <= ? +# CREATE INDEX idx_symbol_bar_window_size_timestamp_range ON crypto_binance_huge_volume (symbol, bar, window_size, timestamp); + +-- 核心索引3: 巨量交易查询索引 +-- 覆盖: WHERE huge_volume = 1 +-- 覆盖: WHERE symbol = ? AND bar = ? AND window_size = ? AND huge_volume = 1 +CREATE INDEX idx_huge_volume_symbol_bar_window_size ON crypto_binance_huge_volume (huge_volume, symbol, bar, window_size); + +-- 核心索引4: 成交量比率排序索引 +-- 覆盖: ORDER BY volume_ratio DESC +-- 覆盖: WHERE huge_volume = 1 ORDER BY volume_ratio DESC +CREATE INDEX idx_volume_ratio_desc ON crypto_binance_huge_volume (volume_ratio DESC); + +-- 核心索引5: 价格分位数高点查询索引 +-- 覆盖: WHERE close_80_high = 1, close_90_high = 1 +-- 覆盖: WHERE symbol = ? AND bar = ? AND window_size = ? AND close_80_high = 1 +CREATE INDEX idx_close_80_90_high ON crypto_binance_huge_volume (close_80_high, close_90_high, symbol, bar, window_size); + +-- 核心索引6: 价格分位数低点查询索引 +-- 覆盖: WHERE close_20_low = 1, close_10_low = 1 +-- 覆盖: WHERE symbol = ? AND bar = ? AND window_size = ? AND close_20_low = 1 +CREATE INDEX idx_close_20_10_low ON crypto_binance_huge_volume (close_20_low, close_10_low, symbol, bar, window_size); + +-- 核心索引7: 最高价分位数查询索引 +-- 覆盖: WHERE high_80_high = 1, high_90_high = 1, high_20_low = 1, high_10_low = 1 +CREATE INDEX idx_high_percentiles ON crypto_binance_huge_volume (high_80_high, high_90_high, high_20_low, high_10_low, symbol, bar, window_size); + +-- 核心索引8: 最低价分位数查询索引 +-- 覆盖: WHERE low_80_high = 1, low_90_high = 1, low_20_low = 1, low_10_low = 1 +CREATE INDEX idx_low_percentiles ON crypto_binance_huge_volume (low_80_high, low_90_high, low_20_low, low_10_low, symbol, bar, window_size); + +-- 核心索引9: 复合量价尖峰查询索引 +-- 覆盖: WHERE huge_volume = 1 AND (close_80_high = 1 OR close_20_low = 1) +-- 覆盖: WHERE huge_volume = 1 AND (close_90_high = 1 OR close_10_low = 1) +CREATE INDEX idx_huge_volume_price_spike ON crypto_binance_huge_volume (huge_volume, close_80_high, close_20_low, close_90_high, close_10_low, symbol, bar, window_size); + +-- 核心索引10: 分组统计查询索引 +-- 覆盖: GROUP BY symbol, bar, window_size +-- 覆盖: GROUP BY symbol, bar +CREATE INDEX idx_symbol_bar_window_size_group ON crypto_binance_huge_volume (symbol, bar, window_size); + +-- 核心索引11: 时间戳排序索引 +-- 覆盖: ORDER BY timestamp DESC/ASC +-- 优化: 查询最新数据 ORDER BY timestamp DESC LIMIT 1 +CREATE INDEX idx_timestamp_desc ON crypto_binance_huge_volume (timestamp DESC); + +-- 核心索引12: 日期时间查询索引 +-- 覆盖: WHERE date_time >= ? AND date_time <= ? +-- 覆盖: ORDER BY date_time +CREATE INDEX idx_date_time ON crypto_binance_huge_volume (date_time); + +-- 执行前检查现有索引 +-- SHOW INDEX FROM crypto_binance_huge_volume; + +-- 执行后验证索引效果 +-- EXPLAIN SELECT * FROM crypto_binance_huge_volume WHERE symbol = 'BTC-USDT' AND bar = '5m' AND window_size = 50 ORDER BY timestamp DESC LIMIT 100; +-- EXPLAIN SELECT * FROM crypto_binance_huge_volume WHERE huge_volume = 1 AND close_80_high = 1 ORDER BY volume_ratio DESC LIMIT 10; +-- EXPLAIN SELECT COUNT(*) FROM crypto_binance_huge_volume WHERE symbol = 'BTC-USDT' AND bar = '5m' AND window_size = 50;