openfund-maker 2.2.9__py3-none-any.whl → 2.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maker/BestTopDownStrategyMaker.py +404 -0
- maker/StrategyMaker.py +487 -0
- maker/ThreeLineStrategyMaker.py +0 -1
- maker/main.py +71 -66
- {openfund_maker-2.2.9.dist-info → openfund_maker-2.3.2.dist-info}/METADATA +2 -1
- {openfund_maker-2.2.9.dist-info → openfund_maker-2.3.2.dist-info}/RECORD +8 -6
- {openfund_maker-2.2.9.dist-info → openfund_maker-2.3.2.dist-info}/WHEEL +0 -0
- {openfund_maker-2.2.9.dist-info → openfund_maker-2.3.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,404 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
import traceback
|
3
|
+
from typing import override
|
4
|
+
|
5
|
+
from maker.StrategyMaker import StrategyMaker
|
6
|
+
|
7
|
+
class BestTopDownStrategyMaker(StrategyMaker):
|
8
|
+
def __init__(self, config, platform_config, common_config, logger=None, exchangeKey='okx'):
|
9
|
+
super().__init__(config, platform_config, common_config, logger, exchangeKey)
|
10
|
+
self.htf_last_struct = {} # 缓存HTF的最后一个结构
|
11
|
+
self.logger = logger
|
12
|
+
|
13
|
+
@override
|
14
|
+
def reset_all_cache(self, symbol):
|
15
|
+
"""
|
16
|
+
重置所有缓存
|
17
|
+
"""
|
18
|
+
super().reset_all_cache(symbol)
|
19
|
+
self.htf_last_struct.pop(symbol, None)
|
20
|
+
self.clear_cache_historical_klines_df(symbol)
|
21
|
+
|
22
|
+
@override
|
23
|
+
def process_pair(self,symbol,pair_config):
|
24
|
+
self.logger.info("-" * 60)
|
25
|
+
"""_summary_
|
26
|
+
HTF (Daily & 4H)
|
27
|
+
1.1. Price's Current Trend 市场趋势
|
28
|
+
1.2. Who's In Control 供需控制
|
29
|
+
1.3. Key Support & Resistance Levels 关键位置
|
30
|
+
ATF (1H & 30 Min & 15 Min)
|
31
|
+
2.1. Market Condition
|
32
|
+
2.2. PD Arrays
|
33
|
+
2.3. Liquidity Areas
|
34
|
+
ETF (5 Min & 1 Min)
|
35
|
+
1. Reversal Signs
|
36
|
+
2. PD Arrays
|
37
|
+
3. Place Order
|
38
|
+
|
39
|
+
"""
|
40
|
+
try:
|
41
|
+
# 是否有持仓,有持仓不进行下单
|
42
|
+
if self.fetch_position(symbol=symbol) :
|
43
|
+
self.reset_all_cache(symbol)
|
44
|
+
self.logger.info(f"{symbol} : 有持仓合约,不进行下单。")
|
45
|
+
return
|
46
|
+
precision = self.get_precision_length(symbol)
|
47
|
+
|
48
|
+
top_down_strategy = pair_config.get('top_down_strategy',{})
|
49
|
+
|
50
|
+
"""
|
51
|
+
获取策略配置
|
52
|
+
"""
|
53
|
+
htf = str(top_down_strategy.get('HTF','4h'))
|
54
|
+
atf = str(top_down_strategy.get('ATF','15m'))
|
55
|
+
etf = str(top_down_strategy.get('ETF', '1m'))
|
56
|
+
|
57
|
+
self.logger.info(f"{symbol} : TopDownSMC策略 {htf}|{atf}|{etf} \n")
|
58
|
+
market_price = self.get_market_price(symbol=symbol)
|
59
|
+
|
60
|
+
"""
|
61
|
+
step 1 : Higher Time Frame Analysis
|
62
|
+
"""
|
63
|
+
step = "1"
|
64
|
+
# 初始化HTF趋势相关变量
|
65
|
+
htf_side, htf_struct, htf_trend = None, None, None
|
66
|
+
# HTF 缓存,减小流量损耗
|
67
|
+
htf_df = self.get_historical_klines_df_by_cache(symbol=symbol, tf=htf)
|
68
|
+
htf_struct =self.build_struct(symbol=symbol, data=htf_df)
|
69
|
+
|
70
|
+
htf_latest_struct = self.get_latest_struct(symbol=symbol, data=htf_struct)
|
71
|
+
htf_trend = htf_latest_struct[self.STRUCT_DIRECTION_COL]
|
72
|
+
htf_side = self.BUY_SIDE if htf_trend == self.BULLISH_TREND else self.SELL_SIDE
|
73
|
+
# 1.1. Price's Current Trend 市场趋势(HTF)
|
74
|
+
step = "1.1"
|
75
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} Price's Current Trend is {htf_trend}。")
|
76
|
+
# 1.2. Who's In Control 供需控制,Bullish 或者 Bearish | Choch 或者 BOS
|
77
|
+
step = "1.2"
|
78
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} struct is {htf_latest_struct[self.STRUCT_COL]}。")
|
79
|
+
|
80
|
+
# 1.3. HTF Key Support & Resistance Levels 支撑或阻力关键位置(HTF 看上下的供需区位置)
|
81
|
+
step = "1.3"
|
82
|
+
htf_OBs_df = self.find_OBs(symbol=symbol,struct=htf_struct)
|
83
|
+
|
84
|
+
if htf_OBs_df is None or len(htf_OBs_df) == 0:
|
85
|
+
self.logger.debug(f"{symbol} : {step}. HTF {htf} 未找到OB。")
|
86
|
+
return
|
87
|
+
else:
|
88
|
+
# self.logger.debug(f"{symbol} : {step}. HTF {htf} 找到OB。")
|
89
|
+
|
90
|
+
htf_support_OB = self.get_latest_OB(symbol=symbol,data=htf_OBs_df,trend=self.BULLISH_TREND)
|
91
|
+
if htf_support_OB :
|
92
|
+
htf_support_price = htf_support_OB.get(self.OB_MID_COL)
|
93
|
+
else:
|
94
|
+
htf_support_price = htf_struct.at[htf_struct.index[-1], self.STRUCT_LOW_COL]
|
95
|
+
|
96
|
+
htf_resistance_OB = self.get_latest_OB(symbol=symbol,data=htf_OBs_df,trend=self.BEARISH_TREND)
|
97
|
+
if htf_resistance_OB :
|
98
|
+
htf_resistance_price = htf_resistance_OB.get(self.OB_MID_COL)
|
99
|
+
else:
|
100
|
+
htf_resistance_price = htf_struct.at[htf_struct.index[-1], self.STRUCT_HIGH_COL]
|
101
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf}, Key Support={htf_support_price:.{precision}f} & Key Resistance={htf_resistance_price:.{precision}f} ")
|
102
|
+
#1.4. 检查关键支撑位和阻力位之间是否有利润空间。
|
103
|
+
step = "1.4"
|
104
|
+
# 计算支撑位和阻力位之间的利润空间百分比
|
105
|
+
htf_profit_percent = abs((htf_resistance_price - htf_support_price) / htf_support_price * 100)
|
106
|
+
min_profit_percent = pair_config.get('min_profit_percent', 4) # 默认最小利润空间为0.5%
|
107
|
+
|
108
|
+
if htf_profit_percent < min_profit_percent:
|
109
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 支撑位={htf_support_price:.{precision}f} 与阻力位={htf_resistance_price:.{precision}f} 之间利润空间{htf_profit_percent:.2f}% < {min_profit_percent}%,等待...")
|
110
|
+
return
|
111
|
+
else:
|
112
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 支撑位={htf_support_price:.{precision}f} 与阻力位={htf_resistance_price:.{precision}f} 之间利润空间{htf_profit_percent:.2f}% >= {min_profit_percent}%")
|
113
|
+
|
114
|
+
# 1.5. 检查当前价格是否在关键支撑位和阻力位,支撑位可以做多,阻力位可以做空。
|
115
|
+
step = "1.5"
|
116
|
+
htf_support_OB_top = None
|
117
|
+
if htf_support_OB :
|
118
|
+
htf_support_OB_top = htf_support_OB.get(self.OB_HIGH_COL)
|
119
|
+
htf_resistance_OB_bottom = None
|
120
|
+
if htf_resistance_OB :
|
121
|
+
htf_resistance_OB_bottom = htf_resistance_OB.get(self.OB_LOW_COL)
|
122
|
+
|
123
|
+
# 检查支撑位做多条件
|
124
|
+
if htf_support_OB_top is not None:
|
125
|
+
if market_price <= htf_support_OB_top:
|
126
|
+
# 价格进入支撑OB,可以开始做多
|
127
|
+
if htf_side != self.BUY_SIDE:
|
128
|
+
htf_side = self.BUY_SIDE
|
129
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 当前价格{market_price:.{precision}f} <= HTF_OB_TOP{htf_support_OB_top:.{precision}f}, 开始做多{htf_side}。")
|
130
|
+
else:
|
131
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 当前价格{market_price:.{precision}f} > HTF_OB_TOP{htf_support_OB_top:.{precision}f}, 无需做多{htf_side}。")
|
132
|
+
else:
|
133
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 未找到HTF_OB_TOP。")
|
134
|
+
|
135
|
+
# 检查阻力位做空条件
|
136
|
+
if htf_resistance_OB_bottom is not None:
|
137
|
+
if market_price >= htf_resistance_OB_bottom:
|
138
|
+
# 价格进入阻力OB,可以开始做空
|
139
|
+
if htf_side != self.SELL_SIDE:
|
140
|
+
htf_side = self.SELL_SIDE
|
141
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 当前价格{market_price:.{precision}f} >= HTF_OB_BOTTOM{htf_resistance_OB_bottom:.{precision}f}, 开始做空{htf_side}。")
|
142
|
+
else:
|
143
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 当前价格{market_price:.{precision}f} < HTF_OB_BOTTOM{htf_resistance_OB_bottom:.{precision}f}, 无需做空{htf_side}。")
|
144
|
+
else:
|
145
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 未找到HTF_OB_BOTTOM。")
|
146
|
+
|
147
|
+
"""
|
148
|
+
step 2 : Analysis Time Frames
|
149
|
+
"""
|
150
|
+
# 2. ATF Step
|
151
|
+
# 2.1 Market Condition 市场状况(ATF 看上下的供需区位置)
|
152
|
+
|
153
|
+
atf_side, atf_struct, atf_trend = None, None, None
|
154
|
+
atf_df = self.get_historical_klines_df(symbol=symbol, tf=atf)
|
155
|
+
atf_struct =self.build_struct(symbol=symbol, data=atf_df)
|
156
|
+
# 获取最新的市场结构,如果为空则返回None
|
157
|
+
atf_latest_struct = self.get_latest_struct(symbol=symbol, data=atf_struct)
|
158
|
+
if atf_latest_struct is None:
|
159
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} 未形成结构,等待... ")
|
160
|
+
return
|
161
|
+
atf_trend = atf_latest_struct[self.STRUCT_DIRECTION_COL]
|
162
|
+
atf_side = self.BUY_SIDE if atf_trend == self.BULLISH_TREND else self.SELL_SIDE
|
163
|
+
# 2.1. Price's Current Trend 市场趋势(HTF )
|
164
|
+
step = "2.1"
|
165
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} Price's Current Trend is {atf_trend}。")
|
166
|
+
# 2.2. Who's In Control 供需控制,Bullish 或者 Bearish | Choch 或者 BOS
|
167
|
+
step = "2.2"
|
168
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} struct is {atf_latest_struct[self.STRUCT_COL]}。")
|
169
|
+
# 2.3. 检查关键支撑位和阻力位之间是否有利润空间。
|
170
|
+
step = "2.3"
|
171
|
+
atf_OBs_df = self.find_OBs(symbol=symbol,struct=atf_struct)
|
172
|
+
atf_support_OB = self.get_latest_OB(symbol=symbol,data=atf_OBs_df,trend=self.BULLISH_TREND)
|
173
|
+
if atf_support_OB :
|
174
|
+
atf_support_price = atf_support_OB.get(self.OB_MID_COL)
|
175
|
+
else:
|
176
|
+
atf_support_price = atf_struct.at[atf_struct.index[-1], self.STRUCT_LOW_COL]
|
177
|
+
|
178
|
+
atf_resistance_OB = self.get_latest_OB(symbol=symbol,data=atf_OBs_df,trend=self.BEARISH_TREND)
|
179
|
+
if atf_resistance_OB :
|
180
|
+
atf_resistance_price = atf_resistance_OB.get(self.OB_MID_COL)
|
181
|
+
else:
|
182
|
+
atf_resistance_price = atf_struct.at[atf_struct.index[-1], self.STRUCT_HIGH_COL]
|
183
|
+
|
184
|
+
self.logger.info(f"{symbol} : {step}.1 ATF {atf}, Key Support={atf_support_price:.{precision}f} "
|
185
|
+
f"& Key Resistance={atf_resistance_price:.{precision}f} ")
|
186
|
+
# 计算支撑位和阻力位之间的利润空间百分比
|
187
|
+
atf_profit_percent = abs((atf_resistance_price - atf_support_price) / atf_support_price * 100)
|
188
|
+
|
189
|
+
if atf_profit_percent < min_profit_percent:
|
190
|
+
self.logger.info(f"{symbol} : {step}.2 ATF {atf} 支撑位={atf_support_price:.{precision}f} 与阻力位={atf_resistance_price:.{precision}f} "
|
191
|
+
f"之间利润空间{atf_profit_percent:.2f}% < {min_profit_percent}%,等待...")
|
192
|
+
return
|
193
|
+
else:
|
194
|
+
self.logger.info(f"{symbol} : {step}.2 ATF {atf} 支撑位={atf_support_price:.{precision}f} 与阻力位={atf_resistance_price:.{precision}f} "
|
195
|
+
f"之间利润空间{atf_profit_percent:.2f}% >= {min_profit_percent}%,允许下单...")
|
196
|
+
|
197
|
+
|
198
|
+
|
199
|
+
# 2.4. ATF 方向要和 HTF方向一致
|
200
|
+
step = "2.4"
|
201
|
+
|
202
|
+
if htf_side != atf_side:
|
203
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} is {atf_side} 与 HTF {htf} is {htf_side} 不一致,等待...")
|
204
|
+
return
|
205
|
+
else:
|
206
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} is {atf_side} 与 HTF {htf} is {htf_side} 一致。")
|
207
|
+
|
208
|
+
#2.5. 反转结构CHOCH, check Liquidity Areas ,检查当前结构是否是流动性摄取。
|
209
|
+
step = "2.5"
|
210
|
+
# if "CHOCH" in atf_struct[self.STRUCT_COL] or "BOS" in atf_struct[self.STRUCT_COL]:
|
211
|
+
# 2.5.1. Equal Lows & Equal Highs
|
212
|
+
end_idx = atf_latest_struct[self.STRUCT_HIGH_INDEX_COL] if atf_side == self.BUY_SIDE else atf_latest_struct[self.STRUCT_LOW_INDEX_COL]
|
213
|
+
last_EQ = self.find_EQH_EQL(symbol=symbol, data=atf_df, trend=atf_trend, end_idx=end_idx, pair_config=pair_config)
|
214
|
+
if last_EQ and last_EQ[self.HAS_EQ_KEY]:
|
215
|
+
price_eq = last_EQ[self.EQUAL_HIGH_COL] if atf_side == self.BUY_SIDE else last_EQ[self.EQUAL_LOW_COL]
|
216
|
+
self.logger.info(f"{symbol} : {step}.1 ATF {atf} {atf_side} find EQ {price_eq}")
|
217
|
+
# 检查是否Liquidity Sweeps
|
218
|
+
if (atf_side == self.BUY_SIDE and atf_latest_struct[self.STRUCT_HIGH_COL] > price_eq) \
|
219
|
+
or (atf_side == self.SELL_SIDE and atf_latest_struct[self.STRUCT_LOW_COL] < price_eq):
|
220
|
+
|
221
|
+
atf_side = self.SELL_SIDE if atf_side == self.BUY_SIDE else self.BUY_SIDE
|
222
|
+
self.logger.info(f"{symbol} : {step}.1 ATF {atf} Liquidity Sweeps , Reverse the ATF {atf} {atf_side} side。")
|
223
|
+
else:
|
224
|
+
self.logger.info(f"{symbol} : {step}.1 ATF {atf} is not found Liquidity Sweeps .")
|
225
|
+
else:
|
226
|
+
self.logger.info(f"{symbol} : {step}.1 ATF {atf} is not found EQ .")
|
227
|
+
|
228
|
+
# FIXME 2.5.2. Dynamic Trendlines and Channels
|
229
|
+
# atf_pre_struct = atf_struct[atf_struct[self.STRUCT_DIRECTION_COL].notna()].iloc[-2] # 看前一个结构是否为动态趋势
|
230
|
+
# atf_start_index = min(atf_pre_struct[self.STRUCT_LOW_INDEX_COL] ,atf_pre_struct[self.STRUCT_HIGH_INDEX_COL])
|
231
|
+
# atf_end_index = max(atf_latest_struct[self.STRUCT_LOW_INDEX_COL] ,atf_latest_struct[self.STRUCT_HIGH_INDEX_COL])
|
232
|
+
|
233
|
+
# is_dynamic_trendlines = self.identify_dynamic_trendlines(symbol=symbol, data=atf_struct, trend=atf_trend, start_idx=atf_start_index, end_idx=atf_end_index)
|
234
|
+
# if is_dynamic_trendlines :
|
235
|
+
# self.logger.info(f"{symbol} : {step}.2 ATF {atf} {atf_trend} find Dynamic Trendlines .")
|
236
|
+
# else:
|
237
|
+
# self.logger.info(f"{symbol} : {step}.2 ATF {atf} {atf_trend} not find Dynamic Trendlines .")
|
238
|
+
|
239
|
+
|
240
|
+
# 2.6. 在HTF供需区范围,找ATF的PDArray,FVG和OB,供需区,计算监测下单区域范围。
|
241
|
+
step = "2.6"
|
242
|
+
atf_pdArrays_df = self.find_PDArrays(symbol=symbol,struct=atf_struct,side=atf_side)
|
243
|
+
|
244
|
+
# 不同的结构,不同位置,如果是Choch则等待价格进入PDArray,如果是BOS则等待价格进入折价区
|
245
|
+
# 划分 折价(discount)区和溢价(premium)区
|
246
|
+
atf_struct_high = atf_latest_struct[self.STRUCT_HIGH_COL]
|
247
|
+
atf_struct_low = atf_latest_struct[self.STRUCT_LOW_COL]
|
248
|
+
atf_struct_mid = atf_latest_struct[self.STRUCT_MID_COL]
|
249
|
+
|
250
|
+
if "CHOCH" in atf_struct[self.STRUCT_COL]:
|
251
|
+
# 找PDArray,Bullish 则PDArray的mid要小于 atf_struct_mid,Bearish 则PDArray的mid要大于 atf_struct_mid
|
252
|
+
# atf_discount_mid = (atf_struct_mid + atf_struct_high) / 2 if atf_trend == self.BEARISH_TREND else (atf_struct_mid + atf_struct_low) / 2
|
253
|
+
mask = atf_pdArrays_df[self.PD_MID_COL] >= atf_struct_mid if atf_side == self.BUY_SIDE else atf_pdArrays_df[self.PD_MID_COL] <= atf_struct_mid
|
254
|
+
atf_pdArrays_df = atf_pdArrays_df[mask]
|
255
|
+
if len(atf_pdArrays_df) == 0:
|
256
|
+
self.logger.info(f"{symbol} : {step}.1. ATF {atf} 未找到PDArray,不下单")
|
257
|
+
return
|
258
|
+
else:
|
259
|
+
# 找到最新的PDArray
|
260
|
+
atf_vaild_pdArray = atf_pdArrays_df.iloc[-1]
|
261
|
+
self.logger.info(f"{symbol} : {step}.1. ATF {atf} 找到PDArray\n"
|
262
|
+
f"{atf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}。")
|
263
|
+
|
264
|
+
|
265
|
+
#SMS
|
266
|
+
elif "SMS" in atf_struct[self.STRUCT_COL]:
|
267
|
+
mask = atf_pdArrays_df[self.PD_MID_COL] >= atf_struct_mid if atf_side == self.BUY_SIDE else atf_pdArrays_df[self.PD_MID_COL] <= atf_struct_mid
|
268
|
+
atf_pdArrays_df = atf_pdArrays_df[mask]
|
269
|
+
if len(atf_pdArrays_df) == 0:
|
270
|
+
self.logger.info(f"{symbol} : {step}.1. ATF {atf} 在{atf_struct_mid:.{precision}f}未找到PDArray,不下单")
|
271
|
+
return
|
272
|
+
else:
|
273
|
+
# 找到最新的PDArray
|
274
|
+
atf_vaild_pdArray = atf_pdArrays_df.iloc[-1]
|
275
|
+
self.logger.info(f"{symbol} : {step}.1. ATF {atf} 找到PDArray\n"
|
276
|
+
f"{atf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}。")
|
277
|
+
|
278
|
+
|
279
|
+
#BMS
|
280
|
+
else:
|
281
|
+
atf_premium_mid = (atf_struct_mid + atf_struct_low) / 2 if atf_side == self.BUY_SIDE else (atf_struct_mid + atf_struct_high) / 2
|
282
|
+
mask = atf_pdArrays_df[self.PD_HIGH_COL] >= atf_premium_mid if atf_side == self.BUY_SIDE else atf_pdArrays_df[self.PD_LOW_COL] <= atf_premium_mid
|
283
|
+
atf_pdArrays_df = atf_pdArrays_df[mask]
|
284
|
+
if len(atf_pdArrays_df) == 0:
|
285
|
+
self.logger.info(f"{symbol} : {step}.1. ATF {atf} ,在{atf_premium_mid:.{precision}f}未找到PDArray,不下单")
|
286
|
+
return
|
287
|
+
else:
|
288
|
+
# 找到最新的PDArray
|
289
|
+
atf_vaild_pdArray = atf_pdArrays_df.iloc[-1]
|
290
|
+
self.logger.info(f"{symbol} : {step}.1. ATF {atf} 找到PDArray\n"
|
291
|
+
f"{atf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}")
|
292
|
+
|
293
|
+
|
294
|
+
|
295
|
+
step = "2.7"
|
296
|
+
|
297
|
+
# 2.7. 等待价格进入 PDArray
|
298
|
+
|
299
|
+
if not (market_price <= atf_vaild_pdArray[self.PD_HIGH_COL] and market_price >= atf_vaild_pdArray[self.PD_LOW_COL]):
|
300
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} market_price={market_price:.{precision}f} 未达到PDArray范围。"
|
301
|
+
f"PD_HIGH={atf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f} "
|
302
|
+
f"PD_LOW={atf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} ")
|
303
|
+
|
304
|
+
return
|
305
|
+
else:
|
306
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} market_price={market_price:.{precision}f} 已到达PDArray范围。"
|
307
|
+
f"PD_HIGH={atf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f} "
|
308
|
+
f"PD_LOW={atf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} ")
|
309
|
+
|
310
|
+
|
311
|
+
# 3. ETF Step
|
312
|
+
step = "3.1"
|
313
|
+
etf_side, etf_struct, etf_trend = None, None, None
|
314
|
+
etf_df = self.get_historical_klines_df(symbol=symbol, tf=etf)
|
315
|
+
etf_struct =self.build_struct(symbol=symbol, data=etf_df)
|
316
|
+
etf_latest_struct = self.get_latest_struct(symbol=symbol, data=etf_struct)
|
317
|
+
|
318
|
+
# 初始化ETF趋势相关变量
|
319
|
+
if etf_latest_struct is None:
|
320
|
+
self.logger.info(f"{symbol} : {step}. ETF {etf} 未形成结构,等待... ")
|
321
|
+
return
|
322
|
+
etf_trend = etf_latest_struct[self.STRUCT_DIRECTION_COL]
|
323
|
+
etf_side = self.BUY_SIDE if etf_trend == self.BULLISH_TREND else self.SELL_SIDE
|
324
|
+
|
325
|
+
# 3.1. Price's Current Trend 市场趋势(ETF )
|
326
|
+
step = "3.1"
|
327
|
+
self.logger.info(f"{symbol} : {step}. ETF {etf} Price's Current Trend is {etf_trend}。")
|
328
|
+
# 3.2. Who's In Control 供需控制,Bullish 或者 Bearish | Choch 或者 BOS
|
329
|
+
step = "3.2"
|
330
|
+
self.logger.info(f"{symbol} : {step}. ETF {etf} struct is {etf_latest_struct[self.STRUCT_COL]}。")
|
331
|
+
|
332
|
+
|
333
|
+
# 3.3 Reversal Signs 反转信号
|
334
|
+
step = "3.3"
|
335
|
+
|
336
|
+
if atf_side != etf_side:
|
337
|
+
|
338
|
+
self.logger.info(f"{symbol} : {step}. ETF {etf} 市场结构{etf_latest_struct[self.STRUCT_COL]}未反转,等待...")
|
339
|
+
return
|
340
|
+
else:
|
341
|
+
self.logger.info(f"{symbol} : {step}. ETF {etf} 市场结构{etf_latest_struct[self.STRUCT_COL]}已反转。")
|
342
|
+
|
343
|
+
# TODO "CHOCH"|"BOS" 的PDArray 入场位置不一样
|
344
|
+
|
345
|
+
# 3.4 找 PD Arrays 价格区间(ETF 看上下的供需区位置)
|
346
|
+
step = "3.4"
|
347
|
+
etf_pdArrays_df = self.find_PDArrays(symbol=symbol,struct=etf_struct,side=etf_side)
|
348
|
+
# 划分 折价(discount)区和溢价(premium)区
|
349
|
+
etf_struct_high = etf_latest_struct[self.STRUCT_HIGH_COL]
|
350
|
+
etf_struct_low = etf_latest_struct[self.STRUCT_LOW_COL]
|
351
|
+
etf_struct_mid = etf_latest_struct[self.STRUCT_MID_COL]
|
352
|
+
mask = etf_pdArrays_df[self.PD_MID_COL] >= etf_struct_mid if etf_side == self.SELL_SIDE else etf_pdArrays_df[self.PD_MID_COL] <= etf_struct_mid
|
353
|
+
etf_pdArrays_df = etf_pdArrays_df[mask]
|
354
|
+
if len(etf_pdArrays_df) == 0:
|
355
|
+
self.logger.info(f"{symbol} : {step}.1. ETF {etf} 未找到PDArray,不下单")
|
356
|
+
return
|
357
|
+
else:
|
358
|
+
# 找到最新的PDArray
|
359
|
+
etf_vaild_pdArray = etf_pdArrays_df.iloc[-1]
|
360
|
+
self.logger.info(f"{symbol} : {step}.1. ETF {etf} 找到PDArray.\n"
|
361
|
+
f"{etf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}。")
|
362
|
+
|
363
|
+
|
364
|
+
if not (market_price <= etf_vaild_pdArray[self.PD_HIGH_COL] and market_price >= etf_vaild_pdArray[self.PD_LOW_COL]):
|
365
|
+
self.logger.info(f"{symbol} : {step}.2. ETF {etf} market_price={market_price:.{precision}f} 未达到PDArray范围。"
|
366
|
+
f"PD_HIGH={etf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} "
|
367
|
+
f"PD_LOW={etf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f}")
|
368
|
+
|
369
|
+
return
|
370
|
+
else:
|
371
|
+
self.logger.info(f"{symbol} : {step}.2. ETF {etf} market_price={market_price:.{precision}f} 已到达PDArray范围。"
|
372
|
+
f"PD_HIGH={etf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} "
|
373
|
+
f"PD_LOW={etf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f}")
|
374
|
+
|
375
|
+
# 3.5 Place Order 下单
|
376
|
+
step = "3.5"
|
377
|
+
# order_price = self.toDecimal(etf_vaild_pdArray[self.PD_HIGH_COL] if etf_trend == self.BULLISH_TREND else etf_vaild_pdArray[self.PD_LOW_COL] )
|
378
|
+
order_price = self.toDecimal(etf_vaild_pdArray[self.PD_MID_COL])
|
379
|
+
|
380
|
+
latest_order_price = self.toDecimal(self.place_order_prices.get(symbol,0))
|
381
|
+
if order_price == latest_order_price:
|
382
|
+
self.logger.info(f"{symbol} : {step}. ETF {etf}, 下单价格 {order_price:.{precision}} 未变化,不进行下单。")
|
383
|
+
return
|
384
|
+
|
385
|
+
self.cancel_all_orders(symbol=symbol)
|
386
|
+
self.place_order(symbol=symbol, price=order_price, side=etf_side, pair_config=pair_config)
|
387
|
+
self.place_order_prices[symbol] = order_price # 记录下单价格,过滤重复下单
|
388
|
+
self.logger.info(f"{symbol} : {step}. ETF {etf}, {etf_side} 价格={order_price:.{precision}}")
|
389
|
+
|
390
|
+
|
391
|
+
except KeyboardInterrupt:
|
392
|
+
self.logger.info("程序收到中断信号,开始退出...")
|
393
|
+
except Exception as e:
|
394
|
+
error_message = f"程序异常退出: {str(e)}"
|
395
|
+
# 记录错误信息和堆栈跟踪
|
396
|
+
self.logger.error(f"{error_message}\n{traceback.format_exc()}")
|
397
|
+
traceback.print_exc()
|
398
|
+
self.send_feishu_notification(symbol, error_message)
|
399
|
+
finally:
|
400
|
+
self.logger.info("=" * 60 + "\n")
|
401
|
+
|
402
|
+
|
403
|
+
|
404
|
+
|
maker/StrategyMaker.py
ADDED
@@ -0,0 +1,487 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
from functools import lru_cache
|
3
|
+
import pandas as pd
|
4
|
+
from datetime import datetime, timedelta
|
5
|
+
from decimal import Decimal
|
6
|
+
from abc import abstractmethod
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
8
|
+
from core.utils.OPTools import OPTools
|
9
|
+
from core.Exchange import Exchange
|
10
|
+
# 导入SMC相关模块
|
11
|
+
from core.smc import (
|
12
|
+
SMCBase,
|
13
|
+
SMCPDArray,
|
14
|
+
SMCStruct,
|
15
|
+
SMCOrderBlock,
|
16
|
+
SMCFVG,
|
17
|
+
SMCLiquidity
|
18
|
+
)
|
19
|
+
|
20
|
+
class StrategyMaker():
|
21
|
+
BUY_SIDE = 'buy'
|
22
|
+
SELL_SIDE = 'sell'
|
23
|
+
BULLISH_TREND = 'Bullish'
|
24
|
+
BEARISH_TREND = 'Bearish'
|
25
|
+
|
26
|
+
HIGH_COL = SMCBase.SMCBase.HIGH_COL
|
27
|
+
LOW_COL = SMCBase.SMCBase.LOW_COL
|
28
|
+
CLOSE_COL = SMCBase.SMCBase.CLOSE_COL
|
29
|
+
OPEN_COL = SMCBase.SMCBase.OPEN_COL
|
30
|
+
TIMESTAMP_COL = SMCBase.SMCBase.TIMESTAMP_COL
|
31
|
+
VOLUME_COL = SMCBase.SMCBase.VOLUME_COL
|
32
|
+
|
33
|
+
STRUCT_COL = SMCStruct.SMCStruct.STRUCT_COL
|
34
|
+
STRUCT_HIGH_COL = SMCStruct.SMCStruct.STRUCT_HIGH_COL
|
35
|
+
STRUCT_LOW_COL = SMCStruct.SMCStruct.STRUCT_LOW_COL
|
36
|
+
STRUCT_MID_COL = SMCStruct.SMCStruct.STRUCT_MID_COL
|
37
|
+
STRUCT_HIGH_INDEX_COL = SMCStruct.SMCStruct.STRUCT_HIGH_INDEX_COL
|
38
|
+
STRUCT_LOW_INDEX_COL = SMCStruct.SMCStruct.STRUCT_LOW_INDEX_COL
|
39
|
+
STRUCT_DIRECTION_COL = SMCStruct.SMCStruct.STRUCT_DIRECTION_COL
|
40
|
+
HIGH_START_COL = SMCStruct.SMCStruct.HIGH_START_COL
|
41
|
+
LOW_START_COL = SMCStruct.SMCStruct.LOW_START_COL
|
42
|
+
|
43
|
+
OB_HIGH_COL = SMCOrderBlock.SMCOrderBlock.OB_HIGH_COL
|
44
|
+
OB_LOW_COL = SMCOrderBlock.SMCOrderBlock.OB_LOW_COL
|
45
|
+
OB_MID_COL = SMCOrderBlock.SMCOrderBlock.OB_MID_COL
|
46
|
+
OB_VOLUME_COL = SMCOrderBlock.SMCOrderBlock.OB_VOLUME_COL
|
47
|
+
OB_DIRECTION_COL = SMCOrderBlock.SMCOrderBlock.OB_DIRECTION_COL
|
48
|
+
OB_ATR = SMCOrderBlock.SMCOrderBlock.OB_ATR
|
49
|
+
OB_IS_COMBINED = SMCOrderBlock.SMCOrderBlock.OB_IS_COMBINED
|
50
|
+
OB_WAS_CROSSED = SMCOrderBlock.SMCOrderBlock.OB_WAS_CROSSED
|
51
|
+
|
52
|
+
PD_HIGH_COL = SMCPDArray.SMCPDArray.PD_HIGH_COL
|
53
|
+
PD_LOW_COL = SMCPDArray.SMCPDArray.PD_LOW_COL
|
54
|
+
PD_MID_COL = SMCPDArray.SMCPDArray.PD_MID_COL
|
55
|
+
PD_TYPE_COL = SMCPDArray.SMCPDArray.PD_TYPE_COL
|
56
|
+
|
57
|
+
LIQU_HIGH_COL = SMCLiquidity.SMCLiquidity.LIQU_HIGH_COL
|
58
|
+
LIQU_LOW_COL = SMCLiquidity.SMCLiquidity.LIQU_LOW_COL
|
59
|
+
EQUAL_HIGH_COL = SMCLiquidity.SMCLiquidity.EQUAL_HIGH_COL
|
60
|
+
EQUAL_LOW_COL = SMCLiquidity.SMCLiquidity.EQUAL_LOW_COL
|
61
|
+
EQH_INDEX_KEY = SMCLiquidity.SMCLiquidity.EQUAL_HIGH_INDEX_KEY
|
62
|
+
EQL_INDEX_KEY = SMCLiquidity.SMCLiquidity.EQUAL_LOW_INDEX_KEY
|
63
|
+
HAS_EQ_KEY = SMCLiquidity.SMCLiquidity.HAS_EQ_KEY
|
64
|
+
|
65
|
+
def __init__(self, config, platform_config, common_config, feishu_webhook=None, logger=None ,exchangeKey='okx'):
|
66
|
+
"""_summary_
|
67
|
+
初始化
|
68
|
+
Args:
|
69
|
+
config (_type_): _description_
|
70
|
+
platform_config (_type_): _description_
|
71
|
+
common_config (_type_): _description_
|
72
|
+
feishu_webhook (_type_, optional): _description_. Defaults to None.
|
73
|
+
logger (_type_, optional): _description_. Defaults to None.
|
74
|
+
"""
|
75
|
+
self.logger = logger
|
76
|
+
self.g_config = config
|
77
|
+
|
78
|
+
self.common_config = common_config
|
79
|
+
self.feishu_webhook = self.common_config.get('feishu_webhook',"")
|
80
|
+
|
81
|
+
self.strategy_config = self.g_config.get('strategy', {})
|
82
|
+
self.trading_pairs_config = self.g_config.get('tradingPairs', {})
|
83
|
+
|
84
|
+
self.leverage_value = self.strategy_config.get('leverage', 20)
|
85
|
+
self.is_demo_trading = self.common_config.get('is_demo_trading', 1) # live trading: 0, demo trading: 1
|
86
|
+
proxies = {
|
87
|
+
"http": self.common_config.get('proxy', "http://localhost:7890"),
|
88
|
+
"https": self.common_config.get('proxy', "http://localhost:7890")
|
89
|
+
}
|
90
|
+
try:
|
91
|
+
self.exchange = Exchange({
|
92
|
+
'apiKey': platform_config["apiKey"],
|
93
|
+
'secret': platform_config["secret"],
|
94
|
+
'password': platform_config["password"],
|
95
|
+
'timeout': 3000,
|
96
|
+
'rateLimit': 50,
|
97
|
+
'options': {'defaultType': 'future'},
|
98
|
+
'proxies': proxies
|
99
|
+
}, exchangeKey)
|
100
|
+
except Exception as e:
|
101
|
+
self.logger.error(f"连接交易所失败: {e}")
|
102
|
+
raise Exception(f"连接交易所失败: {e}")
|
103
|
+
|
104
|
+
self.smcPDArray = SMCPDArray.SMCPDArray()
|
105
|
+
self.smcStruct = SMCStruct.SMCStruct()
|
106
|
+
self.smcOB = SMCOrderBlock.SMCOrderBlock()
|
107
|
+
self.smcFVG = SMCFVG.SMCFVG()
|
108
|
+
self.smcLiqu = SMCLiquidity.SMCLiquidity()
|
109
|
+
|
110
|
+
self.interval_map = {
|
111
|
+
'1d': 24 * 60 * 60 , # 1天
|
112
|
+
'4h': 4 * 60 * 60 , # 4小时
|
113
|
+
'1h': 60 * 60 , # 1小时
|
114
|
+
'30m': 30 * 60 , # 30分钟
|
115
|
+
'15m': 15 * 60 , # 15分钟
|
116
|
+
'5m': 5 * 60 , # 5分钟
|
117
|
+
}
|
118
|
+
|
119
|
+
self.place_order_prices = {} # 记录每个symbol的挂单价格
|
120
|
+
self.cache_time = {} # 记录缓存时间的字典
|
121
|
+
|
122
|
+
|
123
|
+
def toDecimal(self, price):
|
124
|
+
"""_summary_
|
125
|
+
将价格转换为Decimal类型
|
126
|
+
Args:
|
127
|
+
price (_type_): _description_
|
128
|
+
Returns:
|
129
|
+
_type_: _description_
|
130
|
+
"""
|
131
|
+
return OPTools.toDecimal(price)
|
132
|
+
|
133
|
+
def get_pair_config(self,symbol):
|
134
|
+
# 获取交易对特定配置,如果没有则使用全局策略配置
|
135
|
+
pair_config = self.trading_pairs_config.get(symbol, {})
|
136
|
+
|
137
|
+
# 使用字典推导式合并配置,trading_pairs_config优先级高于strategy_config
|
138
|
+
pair_config = {
|
139
|
+
**self.strategy_config, # 基础配置
|
140
|
+
**pair_config # 交易对特定配置会覆盖基础配置
|
141
|
+
}
|
142
|
+
return pair_config
|
143
|
+
|
144
|
+
|
145
|
+
def send_feishu_notification(self, symbol, message):
|
146
|
+
if self.feishu_webhook:
|
147
|
+
try:
|
148
|
+
OPTools.send_feishu_notification(self.feishu_webhook,message)
|
149
|
+
except Exception as e:
|
150
|
+
self.logger.warning(f"{symbol} 发送飞书消息失败: {e}")
|
151
|
+
|
152
|
+
def get_precision_length(self, symbol):
|
153
|
+
"""_summary_
|
154
|
+
获取价格的精度长度
|
155
|
+
Args:
|
156
|
+
price (_type_): _description_
|
157
|
+
Returns:
|
158
|
+
_type_: _description_
|
159
|
+
"""
|
160
|
+
tick_size = self.exchange.get_tick_size(symbol)
|
161
|
+
return self.smcStruct.get_precision_length(tick_size)
|
162
|
+
|
163
|
+
def get_market_price(self, symbol):
|
164
|
+
"""_summary_
|
165
|
+
获取最新成交价
|
166
|
+
Args:
|
167
|
+
symbol (_type_): _description_
|
168
|
+
Returns:
|
169
|
+
_type_: _description_
|
170
|
+
"""
|
171
|
+
return self.exchange.get_market_price(symbol)
|
172
|
+
|
173
|
+
def place_order(self, symbol, price:Decimal, side, pair_config, leverage:int=0, order_type='limit'):
|
174
|
+
"""_summary_
|
175
|
+
下单
|
176
|
+
Args:
|
177
|
+
symbol (_type_): _description_
|
178
|
+
price (_type_): _description_
|
179
|
+
amount_usdt (_type_): _description_
|
180
|
+
side (_type_): _description_
|
181
|
+
order_type (_type_): _description_
|
182
|
+
"""
|
183
|
+
# 获取做多和做空的下单金额配置
|
184
|
+
long_amount_usdt = pair_config.get('long_amount_usdt', 5)
|
185
|
+
short_amount_usdt = pair_config.get('short_amount_usdt', 5)
|
186
|
+
|
187
|
+
# 设置杠杆倍数
|
188
|
+
leverage = leverage or self.leverage_value
|
189
|
+
|
190
|
+
# 根据交易方向设置下单金额
|
191
|
+
order_amount_usdt = short_amount_usdt if side == self.SELL_SIDE else long_amount_usdt
|
192
|
+
|
193
|
+
# 记录下单日志
|
194
|
+
direction = self.BULLISH_TREND if side == self.BUY_SIDE else self.BEARISH_TREND
|
195
|
+
self.logger.info(f"{symbol} : 触发{direction}下单条件. 下单价格: {price}")
|
196
|
+
|
197
|
+
# 执行下单
|
198
|
+
try :
|
199
|
+
self.exchange.place_order(
|
200
|
+
symbol=symbol,
|
201
|
+
price=price,
|
202
|
+
amount_usdt=order_amount_usdt,
|
203
|
+
side=side,
|
204
|
+
leverage=leverage,
|
205
|
+
order_type=order_type
|
206
|
+
)
|
207
|
+
except Exception as e:
|
208
|
+
error_message = f"{symbol} 下单失败: {e}"
|
209
|
+
self.logger.warning(error_message)
|
210
|
+
self.send_feishu_notification(symbol, error_message)
|
211
|
+
|
212
|
+
def cancel_all_orders(self, symbol):
|
213
|
+
"""_summary_
|
214
|
+
取消所有挂单
|
215
|
+
Args:
|
216
|
+
symbol (_type_): _description_
|
217
|
+
"""
|
218
|
+
try:
|
219
|
+
self.exchange.cancel_all_orders(symbol=symbol)
|
220
|
+
except Exception as e:
|
221
|
+
error_message = f"{symbol} 取消所有挂单失败: {e}"
|
222
|
+
self.logger.warning(error_message)
|
223
|
+
self.send_feishu_notification(symbol, error_message)
|
224
|
+
|
225
|
+
def get_historical_klines(self, symbol, tf='15m'):
|
226
|
+
"""_summary_
|
227
|
+
获取历史K线数据
|
228
|
+
Args:
|
229
|
+
symbol (_type_): _description_
|
230
|
+
bar (_type_, optional): _description_. Defaults to '15m'.
|
231
|
+
Returns:
|
232
|
+
_type_: _description_
|
233
|
+
"""
|
234
|
+
return self.exchange.get_historical_klines(symbol=symbol, bar=tf)
|
235
|
+
|
236
|
+
@lru_cache(maxsize=32) # 缓存最近32个不同的请求
|
237
|
+
def _get_cache_historical_klines_df(self, symbol, tf):
|
238
|
+
"""被缓存的获取K线数据的方法"""
|
239
|
+
return self.get_historical_klines_df(symbol, tf)
|
240
|
+
def clear_cache_historical_klines_df(self, symbol=None):
|
241
|
+
"""
|
242
|
+
清除指定交易对和时间周期的缓存
|
243
|
+
|
244
|
+
参数:
|
245
|
+
symbol (str, optional): 交易对符号,如为None则清除所有缓存
|
246
|
+
tf (str, optional): 时间周期,如为None则清除所有缓存
|
247
|
+
"""
|
248
|
+
if symbol is None:
|
249
|
+
# 清除所有缓存
|
250
|
+
self._get_cache_historical_klines_df.cache_clear()
|
251
|
+
self.cache_time.clear()
|
252
|
+
# print("已清除所有K线数据缓存")
|
253
|
+
else:
|
254
|
+
# 删除所有包含cache_key的缓存
|
255
|
+
keys_to_delete = [k for k in self.cache_time.keys() if symbol in k]
|
256
|
+
if keys_to_delete:
|
257
|
+
for k in keys_to_delete:
|
258
|
+
del self.cache_time[k]
|
259
|
+
# 由于lru_cache无法单独清除特定键,这里只能清除所有缓存
|
260
|
+
self._get_cache_historical_klines_df.cache_clear()
|
261
|
+
|
262
|
+
|
263
|
+
def get_historical_klines_df_by_cache(self, symbol, tf='15m'):
|
264
|
+
"""_summary_
|
265
|
+
获取历史K线数据
|
266
|
+
Args:
|
267
|
+
symbol (_type_): _description_
|
268
|
+
bar (_type_, optional): _description_. Defaults to '15m'.
|
269
|
+
Returns:
|
270
|
+
_type_: _description_
|
271
|
+
"""
|
272
|
+
# cache_key = (symbol, tf)
|
273
|
+
cache_valid_second = self.interval_map.get(tf, 4 * 60 * 60) # 默认缓存时间为60分钟
|
274
|
+
cache_key = (symbol, tf)
|
275
|
+
|
276
|
+
# 检查缓存是否存在且未过期
|
277
|
+
current_time = datetime.now()
|
278
|
+
if cache_key in self.cache_time:
|
279
|
+
# 计算缓存时间与当前时间的差值(秒)
|
280
|
+
cache_age = (current_time - self.cache_time[cache_key]).total_seconds()
|
281
|
+
if cache_age <= cache_valid_second:
|
282
|
+
# 缓存有效,直接返回
|
283
|
+
# print(f"使用缓存数据: {symbol} {tf} (缓存时间: {cache_age:.2f} 分钟前)")
|
284
|
+
return self._get_cache_historical_klines_df(symbol, tf)
|
285
|
+
else:
|
286
|
+
# 缓存过期,清除缓存
|
287
|
+
self.logger.debug(f"{symbol} : 缓存已过期: {symbol} {tf} (缓存时间: {cache_age:.2f} 秒前)")
|
288
|
+
self._get_cache_historical_klines_df.cache_clear()
|
289
|
+
|
290
|
+
# 获取新数据并更新缓存时间
|
291
|
+
self.logger.debug(f"{symbol} : 重新获取新数据: {symbol} {tf}")
|
292
|
+
self.cache_time[cache_key] = current_time
|
293
|
+
return self._get_cache_historical_klines_df(symbol, tf)
|
294
|
+
|
295
|
+
|
296
|
+
def get_historical_klines_df(self, symbol, tf='15m'):
|
297
|
+
"""_summary_
|
298
|
+
获取历史K线数据
|
299
|
+
Args:
|
300
|
+
symbol (_type_): _description_
|
301
|
+
bar (_type_, optional): _description_. Defaults to '15m'.
|
302
|
+
Returns:
|
303
|
+
_type_: _description_
|
304
|
+
"""
|
305
|
+
return self.exchange.get_historical_klines_df(symbol=symbol, bar=tf)
|
306
|
+
def format_klines(self, klines) -> pd.DataFrame:
|
307
|
+
|
308
|
+
"""_summary_
|
309
|
+
格式化K线数据
|
310
|
+
Args:
|
311
|
+
klines (_type_): _description_
|
312
|
+
Returns:
|
313
|
+
_type_: _description_
|
314
|
+
"""
|
315
|
+
|
316
|
+
return self.exchange.format_klines(klines)
|
317
|
+
|
318
|
+
def find_PDArrays(self, symbol, struct, side=None, start_index=-1, pair_config=None) -> pd.DataFrame:
|
319
|
+
"""_summary_
|
320
|
+
寻找PDArray
|
321
|
+
Args:
|
322
|
+
symbol (_type_): _description_
|
323
|
+
data (_type_): _description_
|
324
|
+
side (_type_): _description_
|
325
|
+
start_index (_type_): _description_
|
326
|
+
is_valid (bool, optional): _description_. Defaults to True.
|
327
|
+
pair_config (_type_): _description_
|
328
|
+
Returns:
|
329
|
+
_type_: _description_
|
330
|
+
"""
|
331
|
+
return self.smcPDArray.find_PDArrays(struct=struct, side=side, start_index=start_index)
|
332
|
+
|
333
|
+
def find_OBs(self, symbol, struct, side=None, start_index=-1, is_valid=True, pair_config=None) -> pd.DataFrame:
|
334
|
+
"""_summary_
|
335
|
+
识别OB
|
336
|
+
Args:
|
337
|
+
symbol (_type_): _description_
|
338
|
+
data (_type_): _description_
|
339
|
+
side (_type_): _description_
|
340
|
+
start_index (_type_): _description_
|
341
|
+
is_valid (bool, optional): _description_. Defaults to True.
|
342
|
+
pair_config (_type_): _description_
|
343
|
+
Returns:
|
344
|
+
_type_: _description_
|
345
|
+
"""
|
346
|
+
|
347
|
+
|
348
|
+
return self.smcOB.find_OBs(struct=struct, side=side, start_index=start_index, is_valid=is_valid)
|
349
|
+
|
350
|
+
def get_latest_OB(self, symbol, data, trend, start_index=-1) -> dict:
|
351
|
+
"""_summary_
|
352
|
+
获取最新的Order Block
|
353
|
+
Args:
|
354
|
+
symbol (_type_): _description_
|
355
|
+
data (_type_): _description_
|
356
|
+
trend (_type_): _description_
|
357
|
+
start_index (_type_): _description_
|
358
|
+
Returns:
|
359
|
+
_type_: _description_
|
360
|
+
"""
|
361
|
+
|
362
|
+
return self.smcOB.get_latest_OB(data=data, trend=trend, start_index=start_index)
|
363
|
+
|
364
|
+
|
365
|
+
def find_FVGs(self, symbol, data, side, check_balanced=True, start_index=-1, pair_config=None) -> pd.DataFrame:
|
366
|
+
"""_summary_
|
367
|
+
寻找公允价值缺口
|
368
|
+
Args:
|
369
|
+
symbol (_type_): _description_
|
370
|
+
data (_type_): _description_
|
371
|
+
side (_type_): _description_
|
372
|
+
check_balanced (bool, optional): _description_. Defaults to True.
|
373
|
+
start_index (_type_): _description_
|
374
|
+
pair_config (_type_): _description_
|
375
|
+
Returns:
|
376
|
+
_type_: _description_
|
377
|
+
"""
|
378
|
+
|
379
|
+
|
380
|
+
return self.smcFVG.find_FVGs(data, side, check_balanced, start_index)
|
381
|
+
|
382
|
+
def find_EQH_EQL(self, symbol, data, trend, end_idx=-1, atr_offset=0.1, pair_config=None) -> dict:
|
383
|
+
"""_summary_
|
384
|
+
寻找等值高点和等值低点
|
385
|
+
Args:
|
386
|
+
symbol (_type_): _description_
|
387
|
+
data (_type_): _description_
|
388
|
+
trend (_type_): _description_
|
389
|
+
end_idx (int, optional): _description_. Defaults to -1.
|
390
|
+
atr_offset (float, optional): _description_. Defaults to 0.1.
|
391
|
+
Returns:
|
392
|
+
_type_: _description_
|
393
|
+
"""
|
394
|
+
return self.smcLiqu.find_EQH_EQL(data, trend, end_idx=end_idx, atr_offset=atr_offset)
|
395
|
+
|
396
|
+
def identify_dynamic_trendlines(self, symbol, data, trend, start_idx=-1, end_idx=-1, ratio=0.8) -> bool:
|
397
|
+
"""_summary_
|
398
|
+
识别动态趋势线
|
399
|
+
Args:
|
400
|
+
symbol (_type_): _description_
|
401
|
+
data (_type_): _description_
|
402
|
+
trend (_type_): _description_
|
403
|
+
start_idx (int, optional): _description_. Defaults to -1.
|
404
|
+
end_idx (int, optional): _description_. Defaults to -1.
|
405
|
+
ratio (float, optional): _description_. Defaults to 0.5.
|
406
|
+
Returns:
|
407
|
+
_type_: _description_
|
408
|
+
"""
|
409
|
+
return self.smcLiqu.identify_dynamic_trendlines(data, trend, start_idx, end_idx, ratio)
|
410
|
+
|
411
|
+
def build_struct(self, symbol, data) -> pd.DataFrame:
|
412
|
+
|
413
|
+
"""_summary_
|
414
|
+
构建SMC结构,参考 Tradingview OP@SMC Structures and FVG
|
415
|
+
Args:
|
416
|
+
symbol (_type_): _description_
|
417
|
+
data (_type_): _description_
|
418
|
+
Returns:
|
419
|
+
_type_: _description_
|
420
|
+
"""
|
421
|
+
|
422
|
+
|
423
|
+
return self.smcStruct.build_struct(data)
|
424
|
+
|
425
|
+
def get_latest_struct(self, symbol, data) -> dict:
|
426
|
+
"""_summary_
|
427
|
+
获取最后一个SMC结构
|
428
|
+
Args:
|
429
|
+
symbol (_type_): _description_
|
430
|
+
data (_type_): _description_
|
431
|
+
Returns:
|
432
|
+
_type_: _description_
|
433
|
+
"""
|
434
|
+
return self.smcStruct.get_latest_struct(data)
|
435
|
+
|
436
|
+
def reset_all_cache(self, symbol):
|
437
|
+
"""_summary_
|
438
|
+
重置所有缓存数据
|
439
|
+
"""
|
440
|
+
if symbol in self.place_order_prices:
|
441
|
+
self.place_order_prices.pop(symbol)
|
442
|
+
self.clear_cache_historical_klines_df(symbol)
|
443
|
+
|
444
|
+
def fetch_position(self, symbol) -> bool:
|
445
|
+
"""
|
446
|
+
检查指定交易对是否有持仓,失败时最多重试3次
|
447
|
+
|
448
|
+
Args:
|
449
|
+
symbol: 交易对ID
|
450
|
+
|
451
|
+
Returns:
|
452
|
+
bool: 是否有持仓
|
453
|
+
"""
|
454
|
+
try:
|
455
|
+
position = self.exchange.fetch_position(symbol=symbol)
|
456
|
+
return position['contracts'] > 0
|
457
|
+
except Exception as e:
|
458
|
+
error_message = f"{symbol} 检查持仓失败: {e}"
|
459
|
+
self.logger.error(error_message)
|
460
|
+
self.send_feishu_notification(symbol,error_message)
|
461
|
+
return True
|
462
|
+
|
463
|
+
@abstractmethod
|
464
|
+
def process_pair(self, symbol, pair_config):
|
465
|
+
"""
|
466
|
+
处理单个交易对的策略逻辑
|
467
|
+
|
468
|
+
Args:
|
469
|
+
symbol: 交易对名称
|
470
|
+
pair_config: 交易对配置信息
|
471
|
+
|
472
|
+
Raises:
|
473
|
+
NotImplementedError: 子类必须实现此方法
|
474
|
+
"""
|
475
|
+
raise NotImplementedError("必须在子类中实现process_pair方法")
|
476
|
+
|
477
|
+
def monitor_klines(self):
|
478
|
+
symbols = list(self.trading_pairs_config.keys()) # 获取所有币对的ID
|
479
|
+
batch_size = 10 # 每批处理的数量
|
480
|
+
# while True:
|
481
|
+
|
482
|
+
for i in range(0, len(symbols), batch_size):
|
483
|
+
batch = symbols[i:i + batch_size]
|
484
|
+
with ThreadPoolExecutor(max_workers=batch_size) as executor:
|
485
|
+
futures = [executor.submit(self.process_pair, symbol,self.get_pair_config(symbol)) for symbol in batch]
|
486
|
+
for future in as_completed(futures):
|
487
|
+
future.result() # Raise any exceptions caught during execution
|
maker/ThreeLineStrategyMaker.py
CHANGED
maker/main.py
CHANGED
@@ -1,98 +1,103 @@
|
|
1
1
|
import logging
|
2
2
|
import logging.config
|
3
3
|
import yaml
|
4
|
+
import importlib
|
5
|
+
import importlib.metadata
|
4
6
|
from apscheduler.triggers.interval import IntervalTrigger
|
5
7
|
from apscheduler.schedulers.blocking import BlockingScheduler
|
6
8
|
from datetime import datetime
|
7
9
|
from pyfiglet import Figlet
|
10
|
+
from typing import Dict, Any
|
8
11
|
|
9
|
-
|
10
|
-
|
11
|
-
from maker.BestFVGStrategyMaker import BestFVGStrategyMaker
|
12
|
-
|
13
|
-
|
14
|
-
def read_config_file(file_path):
|
12
|
+
def read_config_file(file_path: str) -> Dict[str, Any]:
|
13
|
+
"""读取并解析YAML配置文件"""
|
15
14
|
try:
|
16
|
-
# 打开 YAML 文件
|
17
15
|
with open(file_path, 'r', encoding='utf-8') as file:
|
18
|
-
|
19
|
-
data = yaml.safe_load(file)
|
20
|
-
return data
|
16
|
+
return yaml.safe_load(file)
|
21
17
|
except FileNotFoundError:
|
22
|
-
raise
|
18
|
+
raise FileNotFoundError(f"文件 {file_path} 未找到。")
|
23
19
|
except yaml.YAMLError as e:
|
24
|
-
raise
|
25
|
-
|
26
|
-
def run_bot(bot, logger):
|
27
|
-
try:
|
20
|
+
raise yaml.YAMLError(f"解析 {file_path} 文件时出错: {e}")
|
28
21
|
|
22
|
+
def run_bot(bot: Any, logger: logging.Logger) -> None:
|
23
|
+
"""执行机器人监控任务"""
|
24
|
+
try:
|
29
25
|
bot.monitor_klines()
|
30
26
|
except Exception as e:
|
31
27
|
logger.error(f"执行任务时发生错误: {str(e)}", exc_info=True)
|
32
28
|
|
33
|
-
def
|
29
|
+
def calculate_next_run_time(current_time: datetime, interval: int) -> datetime:
|
30
|
+
"""计算下一次运行时间"""
|
31
|
+
next_run = current_time.replace(second=58, microsecond=0)
|
32
|
+
current_minute = next_run.minute
|
33
|
+
next_interval = ((current_minute // interval) + 1) * interval - 1
|
34
|
+
|
35
|
+
if next_interval >= 60:
|
36
|
+
next_interval %= 60
|
37
|
+
next_run = next_run.replace(hour=next_run.hour + 1)
|
38
|
+
|
39
|
+
return next_run.replace(minute=next_interval)
|
34
40
|
|
35
|
-
|
36
|
-
|
41
|
+
def setup_scheduler(bot: Any, logger: logging.Logger, interval: int) -> None:
|
42
|
+
"""设置并启动调度器"""
|
43
|
+
scheduler = BlockingScheduler()
|
44
|
+
next_run = calculate_next_run_time(datetime.now(), interval)
|
45
|
+
|
46
|
+
scheduler.add_job(
|
47
|
+
run_bot,
|
48
|
+
IntervalTrigger(minutes=interval),
|
49
|
+
args=[bot, logger],
|
50
|
+
next_run_time=next_run
|
51
|
+
)
|
52
|
+
|
53
|
+
try:
|
54
|
+
logger.info(f"启动定时任务调度器,从 {next_run} 开始每{interval}分钟执行一次...")
|
55
|
+
scheduler.start()
|
56
|
+
except (KeyboardInterrupt, SystemExit):
|
57
|
+
logger.info("程序收到中断信号,正在退出...")
|
58
|
+
scheduler.shutdown()
|
59
|
+
def create_strategy_instance(maker_name: str, configs: Dict[str, Any], logger: logging.Logger, exchangeKey:str):
|
60
|
+
"""创建策略实例"""
|
61
|
+
module = importlib.import_module(f"maker.{maker_name}")
|
62
|
+
strategy_class = getattr(module, maker_name)
|
63
|
+
return strategy_class(
|
64
|
+
configs,
|
65
|
+
configs['platform'][exchangeKey],
|
66
|
+
configs['common'],
|
67
|
+
logger=logger,
|
68
|
+
exchangeKey=exchangeKey
|
69
|
+
)
|
70
|
+
def main():
|
71
|
+
# 获取包信息
|
37
72
|
version = importlib.metadata.version("openfund-maker")
|
73
|
+
package_name = __package__ or "openfund-maker"
|
38
74
|
|
75
|
+
# 读取配置
|
39
76
|
maker_config_path = 'maker_config.yaml'
|
40
77
|
config_data = read_config_file(maker_config_path)
|
41
|
-
|
78
|
+
|
79
|
+
# 设置日志
|
42
80
|
logging.config.dictConfig(config_data["Logger"])
|
43
81
|
logger = logging.getLogger("openfund-maker")
|
44
82
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
common_config = config_data['common']
|
49
|
-
feishu_webhook_url = common_config['feishu_webhook']
|
50
|
-
maker = common_config.get('actived_maker', 'MACDStrategyMaker')
|
51
|
-
logger.info(f" ++ {package_name}.{maker}:{version} is doing...")
|
83
|
+
# 显示启动标题
|
84
|
+
f = Figlet(font="standard")
|
85
|
+
logger.info(f"\n{f.renderText('OpenFund Maker')}")
|
52
86
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
87
|
+
# 获取配置信息
|
88
|
+
common_config = config_data['common']
|
89
|
+
maker_name = common_config.get('actived_maker', 'StrategyMaker')
|
90
|
+
logger.info(f" ++ {package_name}.{maker_name}:{version} is doing...")
|
91
|
+
exchangeKey = common_config.get("exchange_key", "okx")
|
92
|
+
# 创建并运行策略实例
|
93
|
+
bot = create_strategy_instance(maker_name, config_data, logger, exchangeKey)
|
94
|
+
|
95
|
+
# 处理调度
|
60
96
|
schedule_config = common_config.get('schedule', {})
|
61
97
|
if schedule_config.get('enabled', False):
|
62
|
-
|
63
|
-
|
64
|
-
# 设置每5分钟执行一次的任务,从整点开始
|
65
|
-
monitor_interval = int(schedule_config.get('monitor_interval', 4))
|
66
|
-
|
67
|
-
# 计算下一个整点分钟
|
68
|
-
now = datetime.now()
|
69
|
-
# 将当前时间的秒和微秒设置为0
|
70
|
-
next_run = now.replace(second=58, microsecond=0)
|
71
|
-
# 计算下一个周期的开始时间
|
72
|
-
current_minute = next_run.minute
|
73
|
-
# 向上取整到下一个周期时间点, 然后再减去2Units,比如秒就是58秒执行。
|
74
|
-
next_interval = ((current_minute // monitor_interval) + 1) * monitor_interval -1
|
75
|
-
# 如果下一个周期时间点超过60分钟,需要调整为下一个小时的对应分钟数
|
76
|
-
if next_interval >= 60:
|
77
|
-
next_interval = next_interval % 60
|
78
|
-
next_run = next_run.replace(hour=next_run.hour + 1)
|
79
|
-
next_run = next_run.replace(minute=next_interval)
|
80
|
-
|
81
|
-
scheduler.add_job(
|
82
|
-
run_bot,
|
83
|
-
IntervalTrigger(minutes=monitor_interval),
|
84
|
-
args=[bot, logger],
|
85
|
-
next_run_time=next_run # 从下一个周期整点开始
|
86
|
-
)
|
87
|
-
|
88
|
-
try:
|
89
|
-
logger.info(f"启动定时任务调度器,从 {next_run} 开始每{monitor_interval}分钟执行一次...")
|
90
|
-
scheduler.start()
|
91
|
-
except (KeyboardInterrupt, SystemExit):
|
92
|
-
logger.info("程序收到中断信号,正在退出...")
|
93
|
-
scheduler.shutdown()
|
98
|
+
monitor_interval = int(schedule_config.get('monitor_interval', 4))
|
99
|
+
setup_scheduler(bot, logger, monitor_interval)
|
94
100
|
else:
|
95
|
-
# 如果未启用计划,直接运行
|
96
101
|
run_bot(bot, logger)
|
97
102
|
|
98
103
|
if __name__ == "__main__":
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: openfund-maker
|
3
|
-
Version: 2.2
|
3
|
+
Version: 2.3.2
|
4
4
|
Summary: Openfund-maker.
|
5
5
|
Requires-Python: >=3.9,<4.0
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
12
12
|
Requires-Dist: apscheduler (>=3.11.0,<4.0.0)
|
13
13
|
Requires-Dist: ccxt (>=4.4.26,<5.0.0)
|
14
|
+
Requires-Dist: openfund-core (>=1.0.0,<2.0.0)
|
14
15
|
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
15
16
|
Requires-Dist: pyfiglet (>=1.0.2,<2.0.0)
|
16
17
|
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
@@ -1,15 +1,17 @@
|
|
1
1
|
maker/BestFVGStrategyMaker.py,sha256=a9UfClrfzkgX6jXL2FODzANtawrmGeZ_PVeO1-tweDc,12532
|
2
|
+
maker/BestTopDownStrategyMaker.py,sha256=-cyoxGn6ZKFJ4neFKnHHJ84ZvODMR-a_agitHz6VNXI,23735
|
2
3
|
maker/MACDStrategyMaker.py,sha256=WX8wqpF9h5W4WclN1NjZ_Bur7KFi_aMTvacfLyHzEcI,12681
|
3
4
|
maker/SMCStrategyMaker.py,sha256=hkDqymWnuyYDo1gTYY_uyO4H4yOwCw8NBTM9RcfLRPc,28780
|
4
|
-
maker/
|
5
|
+
maker/StrategyMaker.py,sha256=iJa-9MxuUwPDOZot2YJmT-sdYHnPZbgPsdCaKvw3Sis,18821
|
6
|
+
maker/ThreeLineStrategyMaker.py,sha256=K4NZB1rH8IZMVrCFEnzwXctZQbyI9ZdyTMrYObpl-vM,32095
|
5
7
|
maker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
8
|
maker/history_code/WickReversalStrategyMaker.py,sha256=7DqPDVJot4EM0_lSAcFAHrR9rNvkIds9KLMoDOiAHEc,17486
|
7
9
|
maker/history_code/config.py,sha256=YPxghO5i0vgRg9Cja8kGj9O7pgSbbtzOgf3RexqXXwY,1188
|
8
10
|
maker/history_code/okxapi.py,sha256=_9G0U_o0ZC8NxaT6PqpiLgxBm9gPobC9PsFHZE1c5w0,553
|
9
11
|
maker/history_code/zhen.py.bak,sha256=HNkrQbJts8G9umE9chEFsc0cLQApcM9KOVNMYPpkBXM,10918
|
10
12
|
maker/history_code/zhen_2.py,sha256=4IaHVtTCMSlrLGSTZrWpW2q-f7HZsUNRkW_-5QgWv24,10509
|
11
|
-
maker/main.py,sha256=
|
12
|
-
openfund_maker-2.2.
|
13
|
-
openfund_maker-2.2.
|
14
|
-
openfund_maker-2.2.
|
15
|
-
openfund_maker-2.2.
|
13
|
+
maker/main.py,sha256=PRCP2qCUiUFPQyi1YbvnmW9KqeCZcc0zGjy9OBvMWbM,3723
|
14
|
+
openfund_maker-2.3.2.dist-info/METADATA,sha256=V93SlcoCwKl4tYklrw4SMY_t2LqVZyFB9ZsF2OhOSD4,2001
|
15
|
+
openfund_maker-2.3.2.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
16
|
+
openfund_maker-2.3.2.dist-info/entry_points.txt,sha256=gKMytICEKcMRFQDFkHZLnIpID7UQFoTIM_xcpiiV6Ns,50
|
17
|
+
openfund_maker-2.3.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|