openfund-maker 2.2.8__py3-none-any.whl → 2.3.1__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/BestFVGStrategyMaker.py +4 -1
- maker/BestTopDownStrategyMaker.py +324 -0
- maker/StrategyMaker.py +438 -0
- maker/ThreeLineStrategyMaker.py +0 -1
- maker/main.py +71 -66
- {openfund_maker-2.2.8.dist-info → openfund_maker-2.3.1.dist-info}/METADATA +2 -1
- {openfund_maker-2.2.8.dist-info → openfund_maker-2.3.1.dist-info}/RECORD +9 -7
- {openfund_maker-2.2.8.dist-info → openfund_maker-2.3.1.dist-info}/WHEEL +0 -0
- {openfund_maker-2.2.8.dist-info → openfund_maker-2.3.1.dist-info}/entry_points.txt +0 -0
maker/BestFVGStrategyMaker.py
CHANGED
@@ -38,9 +38,12 @@ class BestFVGStrategyMaker(SMCStrategyMaker):
|
|
38
38
|
'''
|
39
39
|
FixMe: bug2.2.5_1 20230502
|
40
40
|
检查价格是否在FVG范围内,bar_index,跳过FVG相关的三根K线,从FVG下一根的K线开始检查
|
41
|
+
并且满足bar_index要从struct_index之后,struct_index和FVG_index 不一定哪个大,取最晚的index。
|
41
42
|
'''
|
43
|
+
start_index = max(struct_index,fvg_index+2) #
|
44
|
+
|
42
45
|
if fvg_index is not None:
|
43
|
-
data = data.iloc[min(len(data),
|
46
|
+
data = data.iloc[min(len(data),start_index):].copy()
|
44
47
|
|
45
48
|
|
46
49
|
# 检查价格是否在FVG范围内,bar_index 从struct_index
|
@@ -0,0 +1,324 @@
|
|
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.check_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_last_struct(symbol=symbol, data=htf_struct)
|
71
|
+
|
72
|
+
htf_trend = self.BULLISH_TREND if htf_latest_struct[self.STRUCT_DIRECTION_COL] == 1 else self.BEARISH_TREND
|
73
|
+
htf_side = self.BUY_SIDE if htf_trend == self.BULLISH_TREND else self.SELL_SIDE
|
74
|
+
# 1.1. Price's Current Trend 市场趋势(HTF)
|
75
|
+
step = "1.1"
|
76
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} Price's Current Trend is {htf_trend}。")
|
77
|
+
# 1.2. Who's In Control 供需控制,Bullish 或者 Bearish | Choch 或者 BOS
|
78
|
+
step = "1.2"
|
79
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} struct is {htf_latest_struct[self.STRUCT_COL]}。")
|
80
|
+
|
81
|
+
# 1.3. HTF Key Support & Resistance Levels 支撑或阻力关键位置(HTF 看上下的供需区位置)
|
82
|
+
step = "1.3"
|
83
|
+
htf_OBs_df = self.find_OBs(symbol=symbol,struct=htf_struct)
|
84
|
+
|
85
|
+
if htf_OBs_df is None or len(htf_OBs_df) == 0:
|
86
|
+
self.logger.debug(f"{symbol} : {step}. HTF {htf} 未找到OB。")
|
87
|
+
return
|
88
|
+
else:
|
89
|
+
self.logger.debug(f"{symbol} : {step}. HTF {htf} 找到OB。")
|
90
|
+
|
91
|
+
htf_support_OB = self.get_lastest_OB(symbol=symbol,data=htf_OBs_df,trend=self.BULLISH_TREND)
|
92
|
+
if htf_support_OB :
|
93
|
+
htf_support_price = htf_support_OB.get(self.OB_MID_COL)
|
94
|
+
else:
|
95
|
+
htf_support_price = htf_struct.at[htf_struct.index[-1], self.STRUCT_LOW_COL]
|
96
|
+
|
97
|
+
htf_resistance_OB = self.get_lastest_OB(symbol=symbol,data=htf_OBs_df,trend=self.BEARISH_TREND)
|
98
|
+
if htf_resistance_OB :
|
99
|
+
htf_resistance_price = htf_resistance_OB.get(self.OB_MID_COL)
|
100
|
+
else:
|
101
|
+
htf_resistance_price = htf_struct.at[htf_struct.index[-1], self.STRUCT_HIGH_COL]
|
102
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf}, Key Support={htf_support_price:.{precision}f} & Key Resistance={htf_resistance_price:.{precision}f} ")
|
103
|
+
#1.4. 检查关键支撑位和阻力位之间是否有利润空间。
|
104
|
+
step = "1.4"
|
105
|
+
# 计算支撑位和阻力位之间的利润空间百分比
|
106
|
+
htf_profit_percent = abs((htf_resistance_price - htf_support_price) / htf_support_price * 100)
|
107
|
+
min_profit_percent = pair_config.get('min_profit_percent', 4) # 默认最小利润空间为0.5%
|
108
|
+
|
109
|
+
if htf_profit_percent < min_profit_percent:
|
110
|
+
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}%,等待...")
|
111
|
+
return
|
112
|
+
else:
|
113
|
+
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}%")
|
114
|
+
|
115
|
+
# 1.5. 检查当前价格是否在关键支撑位和阻力位,支撑位可以做多,阻力位可以做空。
|
116
|
+
step = "1.5"
|
117
|
+
htf_ce_price = (htf_support_price + htf_resistance_price) / 2
|
118
|
+
# 折价区
|
119
|
+
target_trend = self.BULLISH_TREND if (market_price >= htf_support_price and market_price < htf_ce_price) else self.BEARISH_TREND
|
120
|
+
# FIXME target_trend 更新 htf_trend
|
121
|
+
htf_trend = target_trend
|
122
|
+
if target_trend != htf_trend:
|
123
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 目标趋势 {target_trend} 与当前趋势 {htf_trend} 不符,折价区 ce={htf_ce_price:.{precision}f} ,等待...")
|
124
|
+
return
|
125
|
+
else:
|
126
|
+
self.logger.info(f"{symbol} : {step}. HTF {htf} 目标趋势 {target_trend} 与当前趋势 {htf_trend} 一致, ce={htf_ce_price:.{precision}f}")
|
127
|
+
|
128
|
+
"""
|
129
|
+
step 2 : Analysis Time Frames
|
130
|
+
"""
|
131
|
+
# 2. ATF Step
|
132
|
+
# 2.1 Market Condition 市场状况(ATF 看上下的供需区位置)
|
133
|
+
|
134
|
+
atf_side, atf_struct, atf_trend = None, None, None
|
135
|
+
atf_df = self.get_historical_klines_df(symbol=symbol, tf=atf)
|
136
|
+
atf_struct =self.build_struct(symbol=symbol, data=atf_df)
|
137
|
+
atf_latest_struct = self.get_last_struct(symbol=symbol, data=atf_struct)
|
138
|
+
atf_trend = atf_latest_struct[self.STRUCT_DIRECTION_COL]
|
139
|
+
atf_side = self.BUY_SIDE if atf_trend == self.BULLISH_TREND else self.SELL_SIDE
|
140
|
+
# 2.1. Price's Current Trend 市场趋势(HTF )
|
141
|
+
step = "2.1"
|
142
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} Price's Current Trend is {atf_trend}。")
|
143
|
+
# 2.2. Who's In Control 供需控制,Bullish 或者 Bearish | Choch 或者 BOS
|
144
|
+
step = "2.2"
|
145
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} struct is {atf_latest_struct[self.STRUCT_COL]}。")
|
146
|
+
# TODO 2.3. 检查关键支撑位和阻力位之间是否有利润空间。
|
147
|
+
|
148
|
+
# 2.4. ATF 方向要和 HTF方向一致
|
149
|
+
step = "2.4"
|
150
|
+
|
151
|
+
if htf_trend != atf_trend:
|
152
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} is {atf_trend} 与 HTF {htf} is {htf_trend} 不一致,等待...")
|
153
|
+
return
|
154
|
+
else:
|
155
|
+
self.logger.info(f"{symbol} : {step}. ATF {atf} is {atf_trend} 与 HTF {htf} is {htf_trend} 一致。")
|
156
|
+
#TODO 2.5. check Liquidity Areas ,检查当前结构是否是流动性摄取。
|
157
|
+
|
158
|
+
|
159
|
+
|
160
|
+
# 2.6. 在HTF供需区范围,找ATF的PDArray,FVG和OB,供需区,计算监测下单区域范围。
|
161
|
+
setp = "2.6"
|
162
|
+
atf_pdArrays_df = self.find_PDArrays(symbol=symbol,struct=atf_struct,side=atf_side)
|
163
|
+
|
164
|
+
# 不同的结构,不同位置,如果是Choch则等待价格进入PDArray,如果是BOS则等待价格进入折价区
|
165
|
+
# 划分 折价(discount)区和溢价(premium)区
|
166
|
+
atf_struct_high = atf_latest_struct[self.STRUCT_HIGH_COL]
|
167
|
+
atf_struct_low = atf_latest_struct[self.STRUCT_LOW_COL]
|
168
|
+
atf_struct_mid = atf_latest_struct[self.STRUCT_MID_COL]
|
169
|
+
|
170
|
+
if "CHOCH" in atf_struct[self.STRUCT_COL]:
|
171
|
+
# 找PDArray,Bullish 则PDArray的mid要小于 atf_struct_mid,Bearish 则PDArray的mid要大于 atf_struct_mid
|
172
|
+
# atf_discount_mid = (atf_struct_mid + atf_struct_high) / 2 if atf_trend == self.BEARISH_TREND else (atf_struct_mid + atf_struct_low) / 2
|
173
|
+
mask = atf_pdArrays_df[self.PD_MID_COL] >= atf_struct_mid if atf_trend == self.BEARISH_TREND else atf_pdArrays_df[self.PD_MID_COL] <= atf_struct_mid
|
174
|
+
atf_pdArrays_df = atf_pdArrays_df[mask]
|
175
|
+
if len(atf_pdArrays_df) == 0:
|
176
|
+
self.logger.info(f"{symbol} : {setp}.1. ATF {atf} 未找到PDArray,不下单")
|
177
|
+
return
|
178
|
+
else:
|
179
|
+
# 找到最新的PDArray
|
180
|
+
atf_vaild_pdArray = atf_pdArrays_df.iloc[-1]
|
181
|
+
self.logger.info(f"{symbol} : {setp}.1. ATF {atf} 找到PDArray\n"
|
182
|
+
f"{atf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}。")
|
183
|
+
|
184
|
+
|
185
|
+
#SMS
|
186
|
+
elif "SMS" in atf_struct[self.STRUCT_COL]:
|
187
|
+
mask = atf_pdArrays_df[self.PD_MID_COL] >= atf_struct_mid if atf_trend == self.BEARISH_TREND else atf_pdArrays_df[self.PD_MID_COL] <= atf_struct_mid
|
188
|
+
atf_pdArrays_df = atf_pdArrays_df[mask]
|
189
|
+
if len(atf_pdArrays_df) == 0:
|
190
|
+
self.logger.info(f"{symbol} : {setp}.1. ATF {atf} 在{atf_struct_mid:.{precision}f}未找到PDArray,不下单")
|
191
|
+
return
|
192
|
+
else:
|
193
|
+
# 找到最新的PDArray
|
194
|
+
atf_vaild_pdArray = atf_pdArrays_df.iloc[-1]
|
195
|
+
self.logger.info(f"{symbol} : {setp}.1. ATF {atf} 找到PDArray\n"
|
196
|
+
f"{atf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}。")
|
197
|
+
|
198
|
+
|
199
|
+
#BMS
|
200
|
+
else:
|
201
|
+
atf_premium_mid = (atf_struct_mid + atf_struct_low) / 2 if atf_trend == self.BEARISH_TREND else (atf_struct_mid + atf_struct_high) / 2
|
202
|
+
mask = atf_pdArrays_df[self.PD_HIGH_COL] >= atf_premium_mid if atf_trend == self.BEARISH_TREND else atf_pdArrays_df[self.PD_LOW_COL] <= atf_premium_mid
|
203
|
+
atf_pdArrays_df = atf_pdArrays_df[mask]
|
204
|
+
if len(atf_pdArrays_df) == 0:
|
205
|
+
self.logger.info(f"{symbol} : {setp}.1. ATF {atf} ,在{atf_premium_mid:.{precision}f}未找到PDArray,不下单")
|
206
|
+
return
|
207
|
+
else:
|
208
|
+
# 找到最新的PDArray
|
209
|
+
atf_vaild_pdArray = atf_pdArrays_df.iloc[-1]
|
210
|
+
self.logger.info(f"{symbol} : {setp}.1. ATF {atf} 找到PDArray\n"
|
211
|
+
f"{atf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}")
|
212
|
+
|
213
|
+
|
214
|
+
|
215
|
+
setp = "2.7"
|
216
|
+
|
217
|
+
# 2.7. 等待价格进入 PDArray
|
218
|
+
|
219
|
+
if not (market_price <= atf_vaild_pdArray[self.PD_HIGH_COL] and market_price >= atf_vaild_pdArray[self.PD_LOW_COL]):
|
220
|
+
self.logger.info(f"{symbol} : {setp}. ATF {atf} market_price={market_price:.{precision}f} 未达到PDArray范围。"
|
221
|
+
f"PD_HIGH={atf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f} "
|
222
|
+
f"PD_LOW={atf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} ")
|
223
|
+
|
224
|
+
return
|
225
|
+
else:
|
226
|
+
self.logger.info(f"{symbol} : {setp}. ATF {atf} market_price={market_price:.{precision}f} 已到达PDArray范围。"
|
227
|
+
f"PD_HIGH={atf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f} "
|
228
|
+
f"PD_LOW={atf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} ")
|
229
|
+
|
230
|
+
|
231
|
+
# 3. ETF Step
|
232
|
+
|
233
|
+
etf_side, etf_struct, etf_trend = None, None, None
|
234
|
+
etf_df = self.get_historical_klines_df(symbol=symbol, tf=etf)
|
235
|
+
etf_struct =self.build_struct(symbol=symbol, data=etf_df)
|
236
|
+
etf_latest_struct = self.get_last_struct(symbol=symbol, data=etf_struct)
|
237
|
+
|
238
|
+
# 初始化HTF趋势相关变量
|
239
|
+
|
240
|
+
etf_trend = etf_latest_struct[self.STRUCT_DIRECTION_COL]
|
241
|
+
#FIXME etf_trend
|
242
|
+
etf_trend = atf_trend
|
243
|
+
|
244
|
+
etf_side = self.BUY_SIDE if etf_trend == self.BULLISH_TREND else self.SELL_SIDE
|
245
|
+
|
246
|
+
# 3.1. Price's Current Trend 市场趋势(ETF )
|
247
|
+
setp = "3.1"
|
248
|
+
self.logger.info(f"{symbol} : {setp}. ETF {etf} Price's Current Trend is {etf_trend}。")
|
249
|
+
# 3.2. Who's In Control 供需控制,Bullish 或者 Bearish | Choch 或者 BOS
|
250
|
+
setp = "3.2"
|
251
|
+
self.logger.info(f"{symbol} : {setp}. ETF {etf} struct is {etf_latest_struct[self.STRUCT_COL]}。")
|
252
|
+
|
253
|
+
|
254
|
+
# 3.3 Reversal Signs 反转信号
|
255
|
+
setp = "3.3"
|
256
|
+
|
257
|
+
if atf_trend != etf_trend:
|
258
|
+
|
259
|
+
self.logger.info(f"{symbol} : {setp}. ETF {etf} 市场结构{etf_latest_struct[self.STRUCT_COL]}未反转,等待...")
|
260
|
+
return
|
261
|
+
else:
|
262
|
+
self.logger.info(f"{symbol} : {setp}. ETF {etf} 市场结构{etf_latest_struct[self.STRUCT_COL]}已反转。")
|
263
|
+
|
264
|
+
# TODO "CHOCH"|"BOS" 的PDArray 入场位置不一样
|
265
|
+
|
266
|
+
# 3.4 找 PD Arrays 价格区间(ETF 看上下的供需区位置)
|
267
|
+
setp = "3.4"
|
268
|
+
etf_pdArrays_df = self.find_PDArrays(symbol=symbol,struct=etf_struct,side=etf_side)
|
269
|
+
# 划分 折价(discount)区和溢价(premium)区
|
270
|
+
etf_struct_high = etf_latest_struct[self.STRUCT_HIGH_COL]
|
271
|
+
etf_struct_low = etf_latest_struct[self.STRUCT_LOW_COL]
|
272
|
+
etf_struct_mid = etf_latest_struct[self.STRUCT_MID_COL]
|
273
|
+
mask = etf_pdArrays_df[self.PD_MID_COL] >= etf_struct_mid if etf_trend == self.BEARISH_TREND else etf_pdArrays_df[self.PD_MID_COL] <= etf_struct_mid
|
274
|
+
etf_pdArrays_df = etf_pdArrays_df[mask]
|
275
|
+
if len(etf_pdArrays_df) == 0:
|
276
|
+
self.logger.info(f"{symbol} : {setp}.1. ETF {etf} 未找到PDArray,不下单")
|
277
|
+
return
|
278
|
+
else:
|
279
|
+
# 找到最新的PDArray
|
280
|
+
etf_vaild_pdArray = etf_pdArrays_df.iloc[-1]
|
281
|
+
self.logger.info(f"{symbol} : {setp}.1. ETF {etf} 找到PDArray.\n"
|
282
|
+
f"{etf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}。")
|
283
|
+
|
284
|
+
|
285
|
+
if not (market_price <= etf_vaild_pdArray[self.PD_HIGH_COL] and market_price >= etf_vaild_pdArray[self.PD_LOW_COL]):
|
286
|
+
self.logger.info(f"{symbol} : {setp}.2. ETF {etf} market_price={market_price:.{precision}f} 未达到PDArray范围。"
|
287
|
+
f"PD_HIGH={etf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} "
|
288
|
+
f"PD_LOW={etf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f}")
|
289
|
+
|
290
|
+
return
|
291
|
+
else:
|
292
|
+
self.logger.info(f"{symbol} : {setp}.2. ETF {etf} market_price={market_price:.{precision}f} 已到达PDArray范围。"
|
293
|
+
f"PD_HIGH={etf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} "
|
294
|
+
f"PD_LOW={etf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f}")
|
295
|
+
|
296
|
+
# 3.5 Place Order 下单
|
297
|
+
setp = "3.5"
|
298
|
+
# 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] )
|
299
|
+
order_price = self.toDecimal(etf_vaild_pdArray[self.PD_MID_COL])
|
300
|
+
|
301
|
+
latest_order_price = self.toDecimal(self.place_order_prices.get(symbol,0))
|
302
|
+
if order_price == latest_order_price:
|
303
|
+
self.logger.info(f"{symbol} : {setp}. ETF {etf}, 下单价格 {order_price:.{precision}} 未变化,不进行下单。")
|
304
|
+
return
|
305
|
+
|
306
|
+
self.cancel_all_orders(symbol=symbol)
|
307
|
+
self.place_order(symbol=symbol, price=order_price, side=etf_side, pair_config=pair_config)
|
308
|
+
self.place_order_prices[symbol] = order_price # 记录下单价格,过滤重复下单
|
309
|
+
self.logger.info(f"{symbol} : {setp}. ETF {etf}, {etf_side} 价格={order_price:.{precision}}")
|
310
|
+
|
311
|
+
|
312
|
+
except KeyboardInterrupt:
|
313
|
+
self.logger.info("程序收到中断信号,开始退出...")
|
314
|
+
except Exception as e:
|
315
|
+
error_message = f"程序异常退出: {str(e)}"
|
316
|
+
self.logger.error(error_message,exc_info=True)
|
317
|
+
traceback.print_exc()
|
318
|
+
self.send_feishu_notification(symbol, error_message)
|
319
|
+
finally:
|
320
|
+
self.logger.info("=" * 60 + "\n")
|
321
|
+
|
322
|
+
|
323
|
+
|
324
|
+
|
maker/StrategyMaker.py
ADDED
@@ -0,0 +1,438 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
from functools import lru_cache
|
3
|
+
import traceback
|
4
|
+
import pandas as pd
|
5
|
+
from datetime import datetime, timedelta
|
6
|
+
from decimal import Decimal
|
7
|
+
from abc import abstractmethod
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
9
|
+
from core.utils.OPTools import OPTools
|
10
|
+
from core.Exchange import Exchange
|
11
|
+
# 导入SMC相关模块
|
12
|
+
from core.smc import (
|
13
|
+
SMCBase,
|
14
|
+
SMCPDArray,
|
15
|
+
SMCStruct,
|
16
|
+
SMCOrderBlock,
|
17
|
+
SMCFVG
|
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
|
+
def __init__(self, config, platform_config, common_config, feishu_webhook=None, logger=None ,exchangeKey='okx'):
|
58
|
+
"""_summary_
|
59
|
+
初始化
|
60
|
+
Args:
|
61
|
+
config (_type_): _description_
|
62
|
+
platform_config (_type_): _description_
|
63
|
+
common_config (_type_): _description_
|
64
|
+
feishu_webhook (_type_, optional): _description_. Defaults to None.
|
65
|
+
logger (_type_, optional): _description_. Defaults to None.
|
66
|
+
"""
|
67
|
+
self.logger = logger
|
68
|
+
self.g_config = config
|
69
|
+
|
70
|
+
self.common_config = common_config
|
71
|
+
self.feishu_webhook = self.common_config.get('feishu_webhook',"")
|
72
|
+
|
73
|
+
self.strategy_config = self.g_config.get('strategy', {})
|
74
|
+
self.trading_pairs_config = self.g_config.get('tradingPairs', {})
|
75
|
+
|
76
|
+
self.leverage_value = self.strategy_config.get('leverage', 20)
|
77
|
+
self.is_demo_trading = self.common_config.get('is_demo_trading', 1) # live trading: 0, demo trading: 1
|
78
|
+
proxies = {
|
79
|
+
"http": self.common_config.get('proxy', "http://localhost:7890"),
|
80
|
+
"https": self.common_config.get('proxy', "http://localhost:7890")
|
81
|
+
}
|
82
|
+
try:
|
83
|
+
self.exchange = Exchange({
|
84
|
+
'apiKey': platform_config["apiKey"],
|
85
|
+
'secret': platform_config["secret"],
|
86
|
+
'password': platform_config["password"],
|
87
|
+
'timeout': 3000,
|
88
|
+
'rateLimit': 50,
|
89
|
+
'options': {'defaultType': 'future'},
|
90
|
+
'proxies': proxies
|
91
|
+
}, exchangeKey)
|
92
|
+
except Exception as e:
|
93
|
+
self.logger.error(f"连接交易所失败: {e}")
|
94
|
+
raise Exception(f"连接交易所失败: {e}")
|
95
|
+
|
96
|
+
self.smcPDArray = SMCPDArray.SMCPDArray()
|
97
|
+
self.smcStruct = SMCStruct.SMCStruct()
|
98
|
+
self.smcOB = SMCOrderBlock.SMCOrderBlock()
|
99
|
+
self.smcFVG = SMCFVG.SMCFVG()
|
100
|
+
|
101
|
+
self.interval_map = {
|
102
|
+
'1d': 24 * 60 * 60 , # 1天
|
103
|
+
'4h': 4 * 60 * 60 , # 4小时
|
104
|
+
'1h': 60 * 60 , # 1小时
|
105
|
+
'30m': 30 * 60 , # 30分钟
|
106
|
+
'15m': 15 * 60 , # 15分钟
|
107
|
+
'5m': 5 * 60 , # 5分钟
|
108
|
+
}
|
109
|
+
|
110
|
+
self.place_order_prices = {} # 记录每个symbol的挂单价格
|
111
|
+
self.cache_time = {} # 记录缓存时间的字典
|
112
|
+
|
113
|
+
|
114
|
+
def toDecimal(self, price):
|
115
|
+
"""_summary_
|
116
|
+
将价格转换为Decimal类型
|
117
|
+
Args:
|
118
|
+
price (_type_): _description_
|
119
|
+
Returns:
|
120
|
+
_type_: _description_
|
121
|
+
"""
|
122
|
+
return OPTools.toDecimal(price)
|
123
|
+
|
124
|
+
def get_pair_config(self,symbol):
|
125
|
+
# 获取交易对特定配置,如果没有则使用全局策略配置
|
126
|
+
pair_config = self.trading_pairs_config.get(symbol, {})
|
127
|
+
|
128
|
+
# 使用字典推导式合并配置,trading_pairs_config优先级高于strategy_config
|
129
|
+
pair_config = {
|
130
|
+
**self.strategy_config, # 基础配置
|
131
|
+
**pair_config # 交易对特定配置会覆盖基础配置
|
132
|
+
}
|
133
|
+
return pair_config
|
134
|
+
|
135
|
+
|
136
|
+
def send_feishu_notification(self, symbol, message):
|
137
|
+
if self.feishu_webhook:
|
138
|
+
try:
|
139
|
+
OPTools.send_feishu_notification(self.feishu_webhook,message)
|
140
|
+
except Exception as e:
|
141
|
+
self.logger.warning(f"{symbol} 发送飞书消息失败: {e}")
|
142
|
+
|
143
|
+
def get_precision_length(self, symbol):
|
144
|
+
"""_summary_
|
145
|
+
获取价格的精度长度
|
146
|
+
Args:
|
147
|
+
price (_type_): _description_
|
148
|
+
Returns:
|
149
|
+
_type_: _description_
|
150
|
+
"""
|
151
|
+
tick_size = self.exchange.get_tick_size(symbol)
|
152
|
+
return self.smcStruct.get_precision_length(tick_size)
|
153
|
+
|
154
|
+
def get_market_price(self, symbol):
|
155
|
+
"""_summary_
|
156
|
+
获取最新成交价
|
157
|
+
Args:
|
158
|
+
symbol (_type_): _description_
|
159
|
+
Returns:
|
160
|
+
_type_: _description_
|
161
|
+
"""
|
162
|
+
return self.exchange.get_market_price(symbol)
|
163
|
+
|
164
|
+
def place_order(self, symbol, price:Decimal, side, pair_config, leverage:int=0, order_type='limit'):
|
165
|
+
"""_summary_
|
166
|
+
下单
|
167
|
+
Args:
|
168
|
+
symbol (_type_): _description_
|
169
|
+
price (_type_): _description_
|
170
|
+
amount_usdt (_type_): _description_
|
171
|
+
side (_type_): _description_
|
172
|
+
order_type (_type_): _description_
|
173
|
+
"""
|
174
|
+
# 获取做多和做空的下单金额配置
|
175
|
+
long_amount_usdt = pair_config.get('long_amount_usdt', 5)
|
176
|
+
short_amount_usdt = pair_config.get('short_amount_usdt', 5)
|
177
|
+
|
178
|
+
# 设置杠杆倍数
|
179
|
+
leverage = leverage or self.leverage_value
|
180
|
+
|
181
|
+
# 根据交易方向设置下单金额
|
182
|
+
order_amount_usdt = short_amount_usdt if side == self.SELL_SIDE else long_amount_usdt
|
183
|
+
|
184
|
+
# 记录下单日志
|
185
|
+
direction = self.BULLISH_TREND if side == self.BUY_SIDE else self.BEARISH_TREND
|
186
|
+
self.logger.debug(f"{symbol} : 触发{direction}下单条件。")
|
187
|
+
|
188
|
+
# 执行下单
|
189
|
+
self.exchange.place_order(
|
190
|
+
symbol=symbol,
|
191
|
+
price=price,
|
192
|
+
amount_usdt=order_amount_usdt,
|
193
|
+
side=side,
|
194
|
+
leverage=leverage,
|
195
|
+
order_type=order_type
|
196
|
+
)
|
197
|
+
|
198
|
+
def cancel_all_orders(self, symbol):
|
199
|
+
"""_summary_
|
200
|
+
取消所有挂单
|
201
|
+
Args:
|
202
|
+
symbol (_type_): _description_
|
203
|
+
"""
|
204
|
+
self.exchange.cancel_all_orders(symbol=symbol)
|
205
|
+
def get_historical_klines(self, symbol, tf='15m'):
|
206
|
+
"""_summary_
|
207
|
+
获取历史K线数据
|
208
|
+
Args:
|
209
|
+
symbol (_type_): _description_
|
210
|
+
bar (_type_, optional): _description_. Defaults to '15m'.
|
211
|
+
Returns:
|
212
|
+
_type_: _description_
|
213
|
+
"""
|
214
|
+
return self.exchange.get_historical_klines(symbol=symbol, bar=tf)
|
215
|
+
|
216
|
+
@lru_cache(maxsize=32) # 缓存最近32个不同的请求
|
217
|
+
def _get_cache_historical_klines_df(self, symbol, tf):
|
218
|
+
"""被缓存的获取K线数据的方法"""
|
219
|
+
return self.get_historical_klines_df(symbol, tf)
|
220
|
+
def clear_cache_historical_klines_df(self, symbol=None):
|
221
|
+
"""
|
222
|
+
清除指定交易对和时间周期的缓存
|
223
|
+
|
224
|
+
参数:
|
225
|
+
symbol (str, optional): 交易对符号,如为None则清除所有缓存
|
226
|
+
tf (str, optional): 时间周期,如为None则清除所有缓存
|
227
|
+
"""
|
228
|
+
if symbol is None:
|
229
|
+
# 清除所有缓存
|
230
|
+
self._get_cache_historical_klines_df.cache_clear()
|
231
|
+
self.cache_time.clear()
|
232
|
+
# print("已清除所有K线数据缓存")
|
233
|
+
else:
|
234
|
+
# 删除所有包含cache_key的缓存
|
235
|
+
keys_to_delete = [k for k in self.cache_time.keys() if symbol in k]
|
236
|
+
if keys_to_delete:
|
237
|
+
for k in keys_to_delete:
|
238
|
+
del self.cache_time[k]
|
239
|
+
# 由于lru_cache无法单独清除特定键,这里只能清除所有缓存
|
240
|
+
self._get_cache_historical_klines_df.cache_clear()
|
241
|
+
|
242
|
+
|
243
|
+
def get_historical_klines_df_by_cache(self, symbol, tf='15m'):
|
244
|
+
"""_summary_
|
245
|
+
获取历史K线数据
|
246
|
+
Args:
|
247
|
+
symbol (_type_): _description_
|
248
|
+
bar (_type_, optional): _description_. Defaults to '15m'.
|
249
|
+
Returns:
|
250
|
+
_type_: _description_
|
251
|
+
"""
|
252
|
+
# cache_key = (symbol, tf)
|
253
|
+
cache_valid_second = self.interval_map.get(tf, 4 * 60 * 60) # 默认缓存时间为60分钟
|
254
|
+
cache_key = (symbol, tf)
|
255
|
+
|
256
|
+
# 检查缓存是否存在且未过期
|
257
|
+
current_time = datetime.now()
|
258
|
+
if cache_key in self.cache_time:
|
259
|
+
# 计算缓存时间与当前时间的差值(秒)
|
260
|
+
cache_age = (current_time - self.cache_time[cache_key]).total_seconds()
|
261
|
+
if cache_age <= cache_valid_second:
|
262
|
+
# 缓存有效,直接返回
|
263
|
+
# print(f"使用缓存数据: {symbol} {tf} (缓存时间: {cache_age:.2f} 分钟前)")
|
264
|
+
return self._get_cache_historical_klines_df(symbol, tf)
|
265
|
+
else:
|
266
|
+
# 缓存过期,清除缓存
|
267
|
+
self.logger.debug(f"{symbol} : 缓存已过期: {symbol} {tf} (缓存时间: {cache_age:.2f} 秒前)")
|
268
|
+
self._get_cache_historical_klines_df.cache_clear()
|
269
|
+
|
270
|
+
# 获取新数据并更新缓存时间
|
271
|
+
self.logger.debug(f"{symbol} : 重新获取新数据: {symbol} {tf}")
|
272
|
+
self.cache_time[cache_key] = current_time
|
273
|
+
return self._get_cache_historical_klines_df(symbol, tf)
|
274
|
+
|
275
|
+
|
276
|
+
def get_historical_klines_df(self, symbol, tf='15m'):
|
277
|
+
"""_summary_
|
278
|
+
获取历史K线数据
|
279
|
+
Args:
|
280
|
+
symbol (_type_): _description_
|
281
|
+
bar (_type_, optional): _description_. Defaults to '15m'.
|
282
|
+
Returns:
|
283
|
+
_type_: _description_
|
284
|
+
"""
|
285
|
+
return self.exchange.get_historical_klines_df(symbol=symbol, bar=tf)
|
286
|
+
def format_klines(self, klines) -> pd.DataFrame:
|
287
|
+
|
288
|
+
"""_summary_
|
289
|
+
格式化K线数据
|
290
|
+
Args:
|
291
|
+
klines (_type_): _description_
|
292
|
+
Returns:
|
293
|
+
_type_: _description_
|
294
|
+
"""
|
295
|
+
|
296
|
+
return self.exchange.format_klines(klines)
|
297
|
+
|
298
|
+
def find_PDArrays(self, symbol, struct, side=None, start_index=-1, pair_config=None) -> pd.DataFrame:
|
299
|
+
"""_summary_
|
300
|
+
寻找PDArray
|
301
|
+
Args:
|
302
|
+
symbol (_type_): _description_
|
303
|
+
data (_type_): _description_
|
304
|
+
side (_type_): _description_
|
305
|
+
start_index (_type_): _description_
|
306
|
+
is_valid (bool, optional): _description_. Defaults to True.
|
307
|
+
pair_config (_type_): _description_
|
308
|
+
Returns:
|
309
|
+
_type_: _description_
|
310
|
+
"""
|
311
|
+
return self.smcPDArray.find_PDArrays(struct=struct, side=side, start_index=start_index)
|
312
|
+
|
313
|
+
def find_OBs(self, symbol, struct, side=None, start_index=-1, is_valid=True, pair_config=None) -> pd.DataFrame:
|
314
|
+
"""_summary_
|
315
|
+
识别OB
|
316
|
+
Args:
|
317
|
+
symbol (_type_): _description_
|
318
|
+
data (_type_): _description_
|
319
|
+
side (_type_): _description_
|
320
|
+
start_index (_type_): _description_
|
321
|
+
is_valid (bool, optional): _description_. Defaults to True.
|
322
|
+
pair_config (_type_): _description_
|
323
|
+
Returns:
|
324
|
+
_type_: _description_
|
325
|
+
"""
|
326
|
+
|
327
|
+
|
328
|
+
return self.smcOB.find_OBs(struct=struct, side=side, start_index=start_index, is_valid=is_valid)
|
329
|
+
|
330
|
+
def get_lastest_OB(self, symbol, data, trend, start_index=-1) -> dict:
|
331
|
+
"""_summary_
|
332
|
+
获取最新的Order Block
|
333
|
+
Args:
|
334
|
+
symbol (_type_): _description_
|
335
|
+
data (_type_): _description_
|
336
|
+
trend (_type_): _description_
|
337
|
+
start_index (_type_): _description_
|
338
|
+
Returns:
|
339
|
+
_type_: _description_
|
340
|
+
"""
|
341
|
+
|
342
|
+
return self.smcOB.get_lastest_OB(data=data, trend=trend, start_index=start_index)
|
343
|
+
|
344
|
+
|
345
|
+
def find_FVGs(self, symbol, data, side, check_balanced=True, start_index=-1, pair_config=None) -> pd.DataFrame:
|
346
|
+
"""_summary_
|
347
|
+
寻找公允价值缺口
|
348
|
+
Args:
|
349
|
+
symbol (_type_): _description_
|
350
|
+
data (_type_): _description_
|
351
|
+
side (_type_): _description_
|
352
|
+
check_balanced (bool, optional): _description_. Defaults to True.
|
353
|
+
start_index (_type_): _description_
|
354
|
+
pair_config (_type_): _description_
|
355
|
+
Returns:
|
356
|
+
_type_: _description_
|
357
|
+
"""
|
358
|
+
|
359
|
+
|
360
|
+
return self.smcFVG.find_FVGs(data, side, check_balanced, start_index)
|
361
|
+
|
362
|
+
def build_struct(self, symbol, data) -> pd.DataFrame:
|
363
|
+
|
364
|
+
"""_summary_
|
365
|
+
构建SMC结构,参考 Tradingview OP@SMC Structures and FVG
|
366
|
+
Args:
|
367
|
+
symbol (_type_): _description_
|
368
|
+
data (_type_): _description_
|
369
|
+
Returns:
|
370
|
+
_type_: _description_
|
371
|
+
"""
|
372
|
+
|
373
|
+
|
374
|
+
return self.smcStruct.build_struct(data)
|
375
|
+
|
376
|
+
def get_last_struct(self, symbol, data) -> dict:
|
377
|
+
"""_summary_
|
378
|
+
获取最后一个SMC结构
|
379
|
+
Args:
|
380
|
+
symbol (_type_): _description_
|
381
|
+
data (_type_): _description_
|
382
|
+
Returns:
|
383
|
+
_type_: _description_
|
384
|
+
"""
|
385
|
+
return self.smcStruct.get_last_struct(data)
|
386
|
+
|
387
|
+
def reset_all_cache(self, symbol):
|
388
|
+
"""_summary_
|
389
|
+
重置所有缓存数据
|
390
|
+
"""
|
391
|
+
if symbol in self.place_order_prices:
|
392
|
+
self.place_order_prices.pop(symbol)
|
393
|
+
self.clear_cache_historical_klines_df(symbol)
|
394
|
+
|
395
|
+
def check_position(self, symbol) -> bool:
|
396
|
+
"""
|
397
|
+
检查指定交易对是否有持仓,失败时最多重试3次
|
398
|
+
|
399
|
+
Args:
|
400
|
+
symbol: 交易对ID
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
bool: 是否有持仓
|
404
|
+
"""
|
405
|
+
try:
|
406
|
+
position = self.exchange.fetch_position(symbol=symbol)
|
407
|
+
return position['contracts'] > 0
|
408
|
+
except Exception as e:
|
409
|
+
error_message = f"{symbol} 检查持仓失败: {e}"
|
410
|
+
self.logger.error(error_message)
|
411
|
+
self.send_feishu_notification(symbol,error_message)
|
412
|
+
return False
|
413
|
+
|
414
|
+
@abstractmethod
|
415
|
+
def process_pair(self, symbol, pair_config):
|
416
|
+
"""
|
417
|
+
处理单个交易对的策略逻辑
|
418
|
+
|
419
|
+
Args:
|
420
|
+
symbol: 交易对名称
|
421
|
+
pair_config: 交易对配置信息
|
422
|
+
|
423
|
+
Raises:
|
424
|
+
NotImplementedError: 子类必须实现此方法
|
425
|
+
"""
|
426
|
+
raise NotImplementedError("必须在子类中实现process_pair方法")
|
427
|
+
|
428
|
+
def monitor_klines(self):
|
429
|
+
symbols = list(self.trading_pairs_config.keys()) # 获取所有币对的ID
|
430
|
+
batch_size = 10 # 每批处理的数量
|
431
|
+
# while True:
|
432
|
+
|
433
|
+
for i in range(0, len(symbols), batch_size):
|
434
|
+
batch = symbols[i:i + batch_size]
|
435
|
+
with ThreadPoolExecutor(max_workers=batch_size) as executor:
|
436
|
+
futures = [executor.submit(self.process_pair, symbol,self.get_pair_config(symbol)) for symbol in batch]
|
437
|
+
for future in as_completed(futures):
|
438
|
+
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.
|
3
|
+
Version: 2.3.1
|
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
|
-
maker/BestFVGStrategyMaker.py,sha256=
|
1
|
+
maker/BestFVGStrategyMaker.py,sha256=a9UfClrfzkgX6jXL2FODzANtawrmGeZ_PVeO1-tweDc,12532
|
2
|
+
maker/BestTopDownStrategyMaker.py,sha256=w3g9U6sQO5ybY8dx7F6ojsWjXbQ06WANa9M_z1045jA,18009
|
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=2nF5TPqHUIAVo3qK2AyZvfsWCrhDNtwULPJ2bIrOb-g,16550
|
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.
|
13
|
-
openfund_maker-2.
|
14
|
-
openfund_maker-2.
|
15
|
-
openfund_maker-2.
|
13
|
+
maker/main.py,sha256=PRCP2qCUiUFPQyi1YbvnmW9KqeCZcc0zGjy9OBvMWbM,3723
|
14
|
+
openfund_maker-2.3.1.dist-info/METADATA,sha256=hIo1FIfLMrxBxds9aEpfrW1yI3iuNLZZAkBVXll3kvc,2001
|
15
|
+
openfund_maker-2.3.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
16
|
+
openfund_maker-2.3.1.dist-info/entry_points.txt,sha256=gKMytICEKcMRFQDFkHZLnIpID7UQFoTIM_xcpiiV6Ns,50
|
17
|
+
openfund_maker-2.3.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|