openfund-maker 1.3.17__tar.gz → 2.0.1__tar.gz
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.
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/PKG-INFO +1 -1
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/pyproject.toml +1 -1
- openfund_maker-2.0.1/src/maker/SMCStrategyMaker.py +555 -0
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/ThreeLineStrategyMaker.py +24 -21
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/main.py +2 -1
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/README.md +0 -0
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/MACDStrategyMaker.py +0 -0
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/WickReversalStrategyMaker.py +0 -0
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/__init__.py +0 -0
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/config.py +0 -0
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/main_m.py +0 -0
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/okxapi.py +0 -0
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/zhen.py.bak +0 -0
- {openfund_maker-1.3.17 → openfund_maker-2.0.1}/src/maker/zhen_2.py +0 -0
@@ -0,0 +1,555 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
import traceback
|
3
|
+
import pandas as pd
|
4
|
+
import talib as ta
|
5
|
+
|
6
|
+
from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
|
7
|
+
|
8
|
+
|
9
|
+
class SMCStrategyMaker(ThreeLineStrategyMaker):
|
10
|
+
def __init__(self, config, platform_config, feishu_webhook=None,logger=None):
|
11
|
+
super().__init__(config, platform_config, feishu_webhook, logger)
|
12
|
+
self.place_order_prices = {} # 记录每个symbol的挂单价格
|
13
|
+
|
14
|
+
def place_order(self,symbol, price, side,pair_config):
|
15
|
+
"""_summary_
|
16
|
+
下单
|
17
|
+
Args:
|
18
|
+
symbol (_type_): _description_
|
19
|
+
price (_type_): _description_
|
20
|
+
amount_usdt (_type_): _description_
|
21
|
+
side (_type_): _description_
|
22
|
+
order_type (_type_): _description_
|
23
|
+
"""
|
24
|
+
long_amount_usdt = pair_config.get('long_amount_usdt', 5)
|
25
|
+
short_amount_usdt = pair_config.get('short_amount_usdt', 5)
|
26
|
+
order_amount_usdt = 5
|
27
|
+
# order_type='optimal_limit_ioc'
|
28
|
+
|
29
|
+
if side == 'sell' :
|
30
|
+
self.logger.debug(f"{symbol} : 触发做空下单条件。")
|
31
|
+
order_amount_usdt = short_amount_usdt
|
32
|
+
elif side == 'buy' :
|
33
|
+
self.logger.debug(f"{symbol} : 触发做多下单条件。")
|
34
|
+
order_amount_usdt = long_amount_usdt
|
35
|
+
super().place_order(symbol=symbol, price=price, amount_usdt=order_amount_usdt, side=side)
|
36
|
+
|
37
|
+
def format_klines(self,klines) -> pd.DataFrame:
|
38
|
+
|
39
|
+
"""_summary_
|
40
|
+
格式化K线数据
|
41
|
+
Args:
|
42
|
+
klines (_type_): _description_
|
43
|
+
"""
|
44
|
+
klines_df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
45
|
+
# 转换时间戳为日期时间
|
46
|
+
klines_df['timestamp'] = pd.to_datetime(klines_df['timestamp'], unit='ms')
|
47
|
+
klines_df['timestamp'] = klines_df['timestamp'].dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
|
48
|
+
|
49
|
+
return klines_df
|
50
|
+
|
51
|
+
def find_OB_boxes(self, data, side, threshold, pivot_index, symbol=None, pair_config=None) -> list:
|
52
|
+
"""_summary_
|
53
|
+
识别OB
|
54
|
+
Args:
|
55
|
+
data (_type_): _description_
|
56
|
+
symbol (_type_): _description_
|
57
|
+
|
58
|
+
"""
|
59
|
+
df = data.copy().iloc[pivot_index:]
|
60
|
+
# 首先计算实体的高点和低点,即开盘价和收盘价中的较大值和较小值
|
61
|
+
df['body_high'] = df[['open', 'close']].max(axis=1)
|
62
|
+
df['body_low'] = df[['open', 'close']].min(axis=1)
|
63
|
+
|
64
|
+
# 初始化OB的高点和低点列为空
|
65
|
+
df['OB_high'] = None
|
66
|
+
df['OB_low'] = None
|
67
|
+
|
68
|
+
# 使用布尔索引一次性更新OB_high
|
69
|
+
# df.loc[df['iUp'] == df.index, 'OB_high'] = df.loc[df['iUp'] == df.index, 'high']
|
70
|
+
# df.loc[df['iUp'] == df.index, 'OB_low'] = df['body_low'].shift(1).fillna(df['body_low'])
|
71
|
+
|
72
|
+
# df.loc[df['iDn'] == df.index, 'OB_low'] = df.loc[df['iDn'] == df.index, 'low']
|
73
|
+
# df.loc[df['iDn'] == df.index, 'OB_high'] = df['body_high'].shift(1).fillna(df['body_high'])
|
74
|
+
|
75
|
+
# print(df[['timestamp', 'pattern','high','low','Dn','iDn','Up','iUp','body_high','body_low']])
|
76
|
+
|
77
|
+
OB_boxes = []
|
78
|
+
# 根据交易方向构建OB盒子,OB区规则孤立高点+实体低点 孤立低点+实体高点
|
79
|
+
if side == 'buy':
|
80
|
+
# 买入方向的OB盒子构建
|
81
|
+
OB_boxes = [
|
82
|
+
{
|
83
|
+
'index': idx,
|
84
|
+
'top': float(df.loc[idx, 'low']), # OB低点为当前K线的最低点
|
85
|
+
'bot': float(df.loc[idx - 1 if idx > df.index[0] else idx, 'body_high']) # OB高点为前一根K线的实体高点
|
86
|
+
}
|
87
|
+
for idx in df.index
|
88
|
+
# 判断条件:是第一根K线(极值点)或当前下降趋势大于前一个,且前一根K线实体高点小于阈值
|
89
|
+
if (idx == df.index[0] or (df.loc[idx, 'Dn'] > df.loc[idx - 1, 'Dn']))
|
90
|
+
and df.loc[idx - 1 if idx > df.index[0] else idx, 'body_high'] <= threshold
|
91
|
+
]
|
92
|
+
else:
|
93
|
+
# 卖出方向的OB盒子构建
|
94
|
+
OB_boxes = [
|
95
|
+
{
|
96
|
+
'index': idx,
|
97
|
+
'top': float(df.loc[idx, 'high']), # OB高点为当前K线的最高点
|
98
|
+
'bot': float(df.loc[idx - 1 if idx > df.index[0] else idx, 'body_low']) # OB低点为前一根K线的实体低点
|
99
|
+
}
|
100
|
+
for idx in df.index
|
101
|
+
# 判断条件:是第一根K线(极值点)或当前上升趋势小于前一个,且前一根K线实体低点大于阈值
|
102
|
+
if (idx == df.index[0] or (df.loc[idx, 'Up'] < df.loc[idx - 1, 'Up']))
|
103
|
+
and df.loc[idx - 1 if idx > df.index[0] else idx, 'body_low'] >= threshold
|
104
|
+
]
|
105
|
+
|
106
|
+
return OB_boxes
|
107
|
+
|
108
|
+
|
109
|
+
def find_fvg_boxes(self, data, side, threshold, pivot_index=0, symbol=None, pair_config=None) -> list:
|
110
|
+
"""_summary_
|
111
|
+
寻找公允价值缺口
|
112
|
+
Args:
|
113
|
+
df (_type_): _description_
|
114
|
+
side (_type_): _description_
|
115
|
+
threshold (_type_): _description_
|
116
|
+
"""
|
117
|
+
|
118
|
+
df = data.copy().iloc[pivot_index:]
|
119
|
+
|
120
|
+
fvg_boxes = []
|
121
|
+
if side == 'buy' :
|
122
|
+
|
123
|
+
# 处理看涨公允价值缺口
|
124
|
+
df.loc[:, 'is_bullish_fvg'] = df['high'].shift(3) < df['low'].shift(1)
|
125
|
+
bullish_df = df[df['is_bullish_fvg']].copy()
|
126
|
+
valid_indices = bullish_df.index[
|
127
|
+
(bullish_df.index - 1).isin(df.index) &
|
128
|
+
(bullish_df.index - 2).isin(df.index) &
|
129
|
+
(bullish_df.index - 3).isin(df.index)
|
130
|
+
]
|
131
|
+
|
132
|
+
fvg_boxes = [
|
133
|
+
{
|
134
|
+
'index': idx - 2, # FVG的索引
|
135
|
+
'top': float(df.loc[idx - 1, 'low']), # FVG高点为右1K线的最低点
|
136
|
+
'bot': float(df.loc[idx - 3, 'high']) # FVG低点为左1K线的最高点
|
137
|
+
}
|
138
|
+
# [df.loc[idx - 1, 'low'], df.loc[idx - 3, 'high'], idx - 2]
|
139
|
+
for idx in valid_indices
|
140
|
+
if df.loc[idx - 3, 'high'] <= threshold and
|
141
|
+
all((df.loc[idx:, 'low'] >= df.loc[idx - 3, 'high'])) # 检查FVG是否被平衡过
|
142
|
+
]
|
143
|
+
|
144
|
+
|
145
|
+
else :
|
146
|
+
# 处理看跌公允价值缺口
|
147
|
+
df.loc[:, 'is_bearish_fvg'] = df['low'].shift(3) > df['high'].shift(1)
|
148
|
+
|
149
|
+
bearish_df = df[df['is_bearish_fvg']].copy()
|
150
|
+
valid_indices = bearish_df.index[
|
151
|
+
(bearish_df.index - 1).isin(df.index) &
|
152
|
+
(bearish_df.index - 2).isin(df.index) &
|
153
|
+
(bearish_df.index - 3).isin(df.index)
|
154
|
+
]
|
155
|
+
|
156
|
+
|
157
|
+
fvg_boxes = [
|
158
|
+
{
|
159
|
+
'index': idx - 2, # FVG的索引
|
160
|
+
'top': float(df.loc[idx - 1, 'high']), # FVG高点为右1K线的最高点
|
161
|
+
'bot': float(df.loc[idx - 3, 'low']) # FVG低点为左1K线的最低点
|
162
|
+
}
|
163
|
+
|
164
|
+
for idx in valid_indices
|
165
|
+
if df.loc[idx - 3, 'low'] >= threshold and
|
166
|
+
all((df.loc[idx:, 'high'] >= df.loc[idx - 3, 'low'])) # 检查FVG是否被平衡过
|
167
|
+
]
|
168
|
+
|
169
|
+
|
170
|
+
return fvg_boxes
|
171
|
+
|
172
|
+
|
173
|
+
def detect_struct(self, data, prd=10, s1=True, resp=7) -> dict:
|
174
|
+
"""_summary_
|
175
|
+
识别智能资金结构
|
176
|
+
|
177
|
+
:param data: 包含 'high' 和 'low' 列的 DataFrame
|
178
|
+
:param prd: 结构周期
|
179
|
+
:param s1: 结构响应布尔值
|
180
|
+
:param resp: 响应周期
|
181
|
+
:return: 包含结构识别结果的 DataFrame
|
182
|
+
"""
|
183
|
+
|
184
|
+
|
185
|
+
# data = data.copy()
|
186
|
+
data['Up'] = None
|
187
|
+
data['Dn'] = None
|
188
|
+
data['iUp'] = None
|
189
|
+
data['iDn'] = None
|
190
|
+
data['pos'] = 0
|
191
|
+
data['pattern'] = None
|
192
|
+
|
193
|
+
# 初始化 Up 和 Dn 的第一个值
|
194
|
+
data.at[0, 'Up'] = data.at[0, 'high']
|
195
|
+
data.at[0, 'Dn'] = data.at[0, 'low']
|
196
|
+
|
197
|
+
|
198
|
+
for index in range(1, len(data)):
|
199
|
+
|
200
|
+
data.at[index, 'Up'] = max(data.at[index - 1, 'Up'], data.at[index, 'high'])
|
201
|
+
data.at[index, 'Dn'] = min(data.at[index - 1, 'Dn'], data.at[index, 'low'])
|
202
|
+
data.at[index, 'pos'] = data.at[index - 1, 'pos']
|
203
|
+
data.at[index, 'iUp'] = data.at[max(0,index - 1), 'iUp'] if data.at[max(0,index - 1), 'iUp'] is not None else index
|
204
|
+
data.at[index, 'iDn'] = data.at[max(0,index - 1), 'iDn'] if data.at[max(0,index - 1), 'iDn'] is not None else index
|
205
|
+
|
206
|
+
# 寻找枢轴高点和低点
|
207
|
+
pvtHi = self.is_pivot_high(data, index, prd)
|
208
|
+
pvtLo = self.is_pivot_low(data, index, prd)
|
209
|
+
|
210
|
+
if pvtHi:
|
211
|
+
data.at[index, 'Up'] = data.at[index, 'high']
|
212
|
+
data.at[index, 'iUp'] = index
|
213
|
+
if pvtLo:
|
214
|
+
data.at[index, 'Dn'] = data.at[index, 'low']
|
215
|
+
data.at[index, 'iDn'] = index
|
216
|
+
# 寻找Bullish结构
|
217
|
+
if data.at[index, 'Up'] > data.at[index - 1, 'Up']:
|
218
|
+
|
219
|
+
if data.at[index - 1, 'pos'] <= 0:
|
220
|
+
# data.at[index, 'pattern'] = 'CHoCH (Bullish)'
|
221
|
+
data.at[index, 'pattern'] = 'Bullish_CHoCH'
|
222
|
+
data.at[index, 'pos'] = 1
|
223
|
+
elif data.at[index - 1, 'pos'] == 1 \
|
224
|
+
and data.at[index - 1, 'Up'] == data.at[max(0,index - (resp if s1 else prd)), 'Up']:
|
225
|
+
data.at[index, 'pattern'] = 'Bullish_SMS'
|
226
|
+
data.at[index, 'pos'] = 2
|
227
|
+
|
228
|
+
elif data.at[index - 1, 'pos'] > 1 \
|
229
|
+
and data.at[index - 1, 'Up'] == data.at[max(0,index - (resp if s1 else prd)), 'Up']:
|
230
|
+
data.at[index, 'pattern'] = 'Bullish_BMS'
|
231
|
+
data.at[index, 'pos'] = data.at[index - 1, 'pos'] + 1
|
232
|
+
|
233
|
+
elif data.at[index, 'Up'] < data.at[index - 1, 'Up']:
|
234
|
+
data.at[index, 'iUp'] = data.at[index - 1, 'iUp']
|
235
|
+
|
236
|
+
# # 寻找Bearish结构
|
237
|
+
if data.at[index, 'Dn'] < data.at[index - 1, 'Dn']:
|
238
|
+
|
239
|
+
if data.at[index - 1, 'pos'] >= 0:
|
240
|
+
|
241
|
+
data.at[index, 'pattern'] = 'Bearish_CHoCH'
|
242
|
+
data.at[index, 'pos'] = -1
|
243
|
+
elif data.at[index - 1, 'pos'] == -1 \
|
244
|
+
and data.at[index - 1, 'Dn'] == data.at[max(0,index - (resp if s1 else prd)), 'Dn']:
|
245
|
+
data.at[index, 'pattern'] = 'Bearish_SMS'
|
246
|
+
data.at[index, 'pos'] = -2
|
247
|
+
elif data.at[index - 1, 'pos'] < -1 \
|
248
|
+
and data.at[index - 1, 'Dn'] == data.at[max(0,index - (resp if s1 else prd)), 'Dn']:
|
249
|
+
data.at[index, 'pattern'] = 'Bearish_BMS'
|
250
|
+
data.at[index, 'pos'] = data.at[index - 1, 'pos'] - 1
|
251
|
+
|
252
|
+
elif data.at[index, 'Dn'] > data.at[index - 1, 'Dn']:
|
253
|
+
data.at[index, 'iDn'] = data.at[index - 1, 'iDn']
|
254
|
+
|
255
|
+
# 获取最后一个结构和位置
|
256
|
+
last_struct = {
|
257
|
+
"struct": None,
|
258
|
+
"index": -1,
|
259
|
+
"pivot_high": None,
|
260
|
+
"pivot_high_index": -1,
|
261
|
+
"pivot_low": None,
|
262
|
+
"pivot_low_index": -1,
|
263
|
+
"side": None
|
264
|
+
|
265
|
+
}
|
266
|
+
|
267
|
+
|
268
|
+
for i in range(len(data)-1, -1, -1):
|
269
|
+
if data.at[i, 'pattern'] is not None:
|
270
|
+
last_struct["struct"] = data.at[i, 'pattern']
|
271
|
+
last_struct["index"] = i
|
272
|
+
|
273
|
+
break
|
274
|
+
|
275
|
+
if last_struct['struct'] is not None :
|
276
|
+
# 找到最后一个结构的枢轴高点和低点,如果当前是孤立点,则取前一个孤立点
|
277
|
+
# 判断交易方向
|
278
|
+
if 'Bearish' in last_struct["struct"]:
|
279
|
+
last_struct["side"] = 'sell'
|
280
|
+
else :
|
281
|
+
last_struct["side"] = 'buy'
|
282
|
+
|
283
|
+
last_struct["pivot_high_index"] = int(data["iUp"].iloc[-1])
|
284
|
+
last_struct["pivot_low_index"] = int(data["iDn"].iloc[-1])
|
285
|
+
|
286
|
+
last_struct["pivot_high"] = float(data.loc[last_struct["pivot_high_index"], 'high'])
|
287
|
+
last_struct["pivot_low"] = float(data.loc[last_struct["pivot_low_index"], 'low'])
|
288
|
+
|
289
|
+
|
290
|
+
|
291
|
+
# last_struct["pivot_high_index"] = int(data.loc[data.index > last_struct["index"], 'high'].idxmax())
|
292
|
+
# last_struct["pivot_high"] = float(data.loc[data.index > last_struct["index"], 'high'].max())
|
293
|
+
# last_struct["pivot_low_index"] = int(data.loc[data.index > last_struct["index"], 'low'].idxmin())
|
294
|
+
# last_struct["pivot_low"] = float(data.loc[data.index > last_struct["index"], 'low'].min())
|
295
|
+
|
296
|
+
return last_struct
|
297
|
+
|
298
|
+
|
299
|
+
|
300
|
+
|
301
|
+
def is_pivot_high(self,data, index, period, check_bounds=False):
|
302
|
+
"""
|
303
|
+
判断当前索引处是否为枢轴高点
|
304
|
+
:param data: 包含 'high' 列的 DataFrame
|
305
|
+
:param index: 当前索引
|
306
|
+
:param period: 前后比较的周期数
|
307
|
+
:return: 是否为枢轴高点
|
308
|
+
"""
|
309
|
+
if check_bounds and (index < period or index >= len(data) - period):
|
310
|
+
return False
|
311
|
+
current_high = data.at[index, 'high']
|
312
|
+
prev_highs = data['high'].iloc[max(0,index - period):index]
|
313
|
+
next_highs = data['high'].iloc[index :min(len(data),index + period + 1)]
|
314
|
+
return all(current_high >= prev_highs) and all(current_high >= next_highs)
|
315
|
+
|
316
|
+
|
317
|
+
def is_pivot_low(self,data, index, period, check_bounds=False):
|
318
|
+
"""
|
319
|
+
判断当前索引处是否为枢轴低点
|
320
|
+
:param data: 包含 'low' 列的 DataFrame
|
321
|
+
:param index: 当前索引
|
322
|
+
:param period: 前后比较的周期数
|
323
|
+
:return: 是否为枢轴低点
|
324
|
+
"""
|
325
|
+
if check_bounds and (index < period or index >= len(data) - period):
|
326
|
+
return False
|
327
|
+
current_low = data.at[index, 'low']
|
328
|
+
prev_lows = data['low'].iloc[max(0,index - period):index]
|
329
|
+
next_lows = data['low'].iloc[index :min(len(data),index + period + 1)]
|
330
|
+
return all(current_low <= prev_lows) and all(current_low <= next_lows)
|
331
|
+
|
332
|
+
def round_price(self,symbol, price):
|
333
|
+
tick_size = self.get_tick_size(symbol)
|
334
|
+
return super().round_price_to_tick(price, tick_size)
|
335
|
+
|
336
|
+
def calculate_ce(self,symbol,pivot_high , pivot_low) -> float:
|
337
|
+
ce = (pivot_high + pivot_low) / 2
|
338
|
+
return float(self.round_price(symbol, ce))
|
339
|
+
|
340
|
+
|
341
|
+
def process_pair(self,symbol,pair_config):
|
342
|
+
self.logger.info("=" * 60)
|
343
|
+
"""_summary_
|
344
|
+
1. HTF 判断struct趋势(SMS和BMS)
|
345
|
+
2. HTF 获取最新的两个极值点,设置折价区和溢价区
|
346
|
+
3. CTF 在折价区获取FVG和OB的位置
|
347
|
+
4. CTF 下单
|
348
|
+
5.
|
349
|
+
"""
|
350
|
+
try:
|
351
|
+
# 检查是否有持仓,有持仓不进行下单
|
352
|
+
if self.check_position(symbol=symbol) :
|
353
|
+
self.logger.info(f"{symbol} : 有持仓合约,不进行下单。")
|
354
|
+
if symbol in self.place_order_prices:
|
355
|
+
self.place_order_prices[symbol] = {}
|
356
|
+
return
|
357
|
+
|
358
|
+
|
359
|
+
smc_strategy = pair_config.get('smc_strategy',{})
|
360
|
+
|
361
|
+
# 获取历史K线,HTF和CTF
|
362
|
+
htf = str(smc_strategy.get('HTF','15m'))
|
363
|
+
htf_Klines = self.get_historical_klines(symbol=symbol, bar=htf)
|
364
|
+
htf_df = self.format_klines(htf_Klines)
|
365
|
+
|
366
|
+
ctf = str(pair_config.get('CHF', '5m'))
|
367
|
+
ctf_kLines = self.get_historical_klines(symbol=symbol, bar=ctf)
|
368
|
+
ctf_df = self.format_klines(ctf_kLines)
|
369
|
+
|
370
|
+
enable_FVG = smc_strategy.get('enable_FVG',True) # 是否启用FVG
|
371
|
+
enable_OB = smc_strategy.get('enable_OB',True) # 是否启用OB
|
372
|
+
self.logger.debug(f"{symbol} : SMC策略 {ctf}\{htf} enable_FVG={enable_FVG} enable_OB={enable_OB} ...")
|
373
|
+
|
374
|
+
side = 'none'
|
375
|
+
# 1. HTF 判断struct趋势(CHoCH\SMS\BMS) ,HTF struct 看趋势,CTF 看FVG和OB的位置
|
376
|
+
swing_points_length = smc_strategy.get('swing_points_length',10)
|
377
|
+
htf_last_struct = self.detect_struct(htf_df,prd=swing_points_length)
|
378
|
+
htf_last_struct_label = htf_last_struct["struct"]
|
379
|
+
|
380
|
+
|
381
|
+
if htf_last_struct_label is None:
|
382
|
+
self.logger.debug(f"{symbol} : {htf} 未形成 struct,不下单。{htf_last_struct}。")
|
383
|
+
return
|
384
|
+
|
385
|
+
# ctf_last_struct = self.detect_struct(ctf_df)
|
386
|
+
# ctf_last_struct_label = ctf_last_struct["struct"]
|
387
|
+
|
388
|
+
# if ctf_last_struct_label is None:
|
389
|
+
# self.logger.debug(f"{symbol} :{ctf} 未形成 struct,不下单。{ctf_last_struct}。")
|
390
|
+
# return
|
391
|
+
|
392
|
+
side = htf_last_struct["side"]
|
393
|
+
# self.logger.debug(f"{symbol} : {htf} 趋势={htf_last_struct_label}-{side}: \n{htf_last_struct}")
|
394
|
+
|
395
|
+
|
396
|
+
# 2. HTF 获取最新的两个极值点,设置折价(discount)区和溢价(premium)区
|
397
|
+
pivot_high = htf_last_struct["pivot_high"]
|
398
|
+
pivot_low = htf_last_struct["pivot_low"]
|
399
|
+
mid_line = self.calculate_ce(symbol,pivot_high,pivot_low)
|
400
|
+
|
401
|
+
# 计算溢价和折价区
|
402
|
+
premium_box = {
|
403
|
+
'top': pivot_high,
|
404
|
+
'bot': mid_line,
|
405
|
+
'ce': self.calculate_ce(symbol,pivot_high,mid_line)
|
406
|
+
}
|
407
|
+
discount_box = {
|
408
|
+
'top': mid_line,
|
409
|
+
'bot': pivot_low,
|
410
|
+
'ce': self.calculate_ce(symbol,mid_line,pivot_low)
|
411
|
+
}
|
412
|
+
|
413
|
+
self.logger.debug(f"{symbol} : {htf} 趋势={htf_last_struct_label}: \npivot_high={pivot_high} pivot_low={pivot_low} mid_line={mid_line}\n溢价区={premium_box}\n折价区={discount_box}")
|
414
|
+
|
415
|
+
# 3. 根据HTF结构来分析下单位置和止盈位置
|
416
|
+
threshold = 0.0
|
417
|
+
order_side = side
|
418
|
+
# 获取当前市场价格
|
419
|
+
market_price = ctf_df['close'].iloc[-1]
|
420
|
+
|
421
|
+
if 'CHoCH' in htf_last_struct_label:
|
422
|
+
"""
|
423
|
+
ChoCh 结构。
|
424
|
+
Bearish趋势 如果价格,
|
425
|
+
1.在溢价区上半区,可以考虑顺当前趋势,做空。
|
426
|
+
2.在折价区下半区,则考虑回收流动性,做多。
|
427
|
+
3.溢价区下半区和折价区上半区,不做单。
|
428
|
+
|
429
|
+
Bullish趋势 如果价格,
|
430
|
+
1.在折价区下半区,可以考虑顺当前趋势,做多。
|
431
|
+
2.在溢价区上半区,则考虑回收流动性的,做空。
|
432
|
+
3.溢价区下半区和折价区上半区,不做单。
|
433
|
+
|
434
|
+
"""
|
435
|
+
# 溢价区上半区做空
|
436
|
+
if market_price >= premium_box['ce'] and side == 'sell':
|
437
|
+
threshold = premium_box['ce']
|
438
|
+
# 折价区下半区做多
|
439
|
+
elif market_price <= discount_box['ce'] and side == 'buy':
|
440
|
+
threshold = discount_box['ce']
|
441
|
+
# 折价区下半区回收流动性做空 # TODO 要考虑是否有孤立点
|
442
|
+
# elif market_price <= discount_box['ce'] and side == 'sell':
|
443
|
+
# threshold = discount_box['ce']
|
444
|
+
# order_side = 'buy'
|
445
|
+
# # 溢价区上半区回收流动性做多
|
446
|
+
# elif market_price >= premium_box['ce'] and side == 'buy':
|
447
|
+
# threshold = premium_box['ce']
|
448
|
+
# order_side = 'sell'
|
449
|
+
|
450
|
+
|
451
|
+
elif 'SMS' in htf_last_struct_label or 'BMS' in htf_last_struct_label:
|
452
|
+
"""
|
453
|
+
SMS/BMS 结构。
|
454
|
+
Bullish趋势 如果价格,
|
455
|
+
1.在折价区可以下单,不区分上下半区
|
456
|
+
|
457
|
+
Bearish趋势 如果价格,
|
458
|
+
1.在溢价区可以下单,不区分上下半区
|
459
|
+
|
460
|
+
"""
|
461
|
+
# Bearish趋势 如果价格在溢价区可以下单
|
462
|
+
# if market_price >= mid_line and side == 'sell':
|
463
|
+
# threshold = mid_line
|
464
|
+
# # Bullish趋势 如果价格在折价区可以下单
|
465
|
+
# elif market_price <= mid_line and side == 'buy':
|
466
|
+
# threshold = mid_line
|
467
|
+
threshold = mid_line
|
468
|
+
|
469
|
+
|
470
|
+
|
471
|
+
if threshold == 0.0:
|
472
|
+
self.logger.debug(f"{symbol} : 价格{market_price}不在目标区域,不下单。")
|
473
|
+
# 取消所有未成交订单
|
474
|
+
self.cancel_all_orders(symbol=symbol)
|
475
|
+
return
|
476
|
+
|
477
|
+
|
478
|
+
# 4. 在CTF折价区获取FVG的位置
|
479
|
+
order_price = 0.0
|
480
|
+
|
481
|
+
if enable_FVG and order_price == 0.0:
|
482
|
+
|
483
|
+
all_tf = ['1m', '3m', '5m', '15m', '30m', '1H', '2H', '4H']
|
484
|
+
# 获取当前时间周期之前的所有时间周期
|
485
|
+
ctf_index = all_tf.index(ctf)
|
486
|
+
ltf_tfs = all_tf[:ctf_index + 1]
|
487
|
+
|
488
|
+
# 遍历所有LTF时间周期,获取FVG
|
489
|
+
for tf in ltf_tfs[::-1]:
|
490
|
+
tf_Klines = self.get_historical_klines(symbol=symbol, bar=tf)
|
491
|
+
tf_df = self.format_klines(tf_Klines)
|
492
|
+
|
493
|
+
fvg_boxes = self.find_fvg_boxes(tf_df,side=order_side,threshold=threshold)
|
494
|
+
if len(fvg_boxes) > 0:
|
495
|
+
self.logger.debug(f"{symbol} : 方向={order_side}, {tf} FVG={fvg_boxes}")
|
496
|
+
break
|
497
|
+
else:
|
498
|
+
self.logger.debug(f"{symbol} : 方向={order_side}, {tf} 未找到 FVG")
|
499
|
+
|
500
|
+
|
501
|
+
|
502
|
+
if len(fvg_boxes) != 0 and order_price == 0.0:
|
503
|
+
last_fvg_box = fvg_boxes[-1]
|
504
|
+
ce_price = self.calculate_ce(symbol,last_fvg_box['top'],last_fvg_box['bot'])
|
505
|
+
self.logger.info(f"{symbol} : 方向={order_side}, FVG_ce={ce_price} FVG={last_fvg_box} ")
|
506
|
+
order_price = ce_price
|
507
|
+
|
508
|
+
# 4. 找OB位置,OB规则孤立高点+实体低点 孤立低点+实体高点
|
509
|
+
|
510
|
+
if enable_OB and order_price == 0.0: # OB 优先级低于 FVG, order_price有价格时,不再计算OB
|
511
|
+
|
512
|
+
ctf_last_struct = self.detect_struct(ctf_df,prd=swing_points_length)
|
513
|
+
# 找到最近的一个极值点的位置
|
514
|
+
if order_side == 'buy':
|
515
|
+
pivot_index = ctf_last_struct["pivot_low_index"]
|
516
|
+
else:
|
517
|
+
pivot_index = ctf_last_struct["pivot_high_index"]
|
518
|
+
# TODO 不同级别的pivot_index 需要优化计算
|
519
|
+
OB_boxes = self.find_OB_boxes(ctf_df,side=side,threshold=threshold,pivot_index=pivot_index)
|
520
|
+
|
521
|
+
if len(OB_boxes) != 0 :
|
522
|
+
last_OB_box = OB_boxes[-1]
|
523
|
+
ce_price = self.calculate_ce(symbol,last_OB_box['top'],last_OB_box['bot'])
|
524
|
+
self.logger.info(f"{symbol} : 方向={order_side}, OB_ce={ce_price} , OB={last_OB_box} ")
|
525
|
+
order_price = ce_price
|
526
|
+
|
527
|
+
if order_price == 0.0:
|
528
|
+
self.logger.warning(f"!!!{symbol} : 未找到 FVG和OB")
|
529
|
+
self.cancel_all_orders(symbol=symbol)
|
530
|
+
return
|
531
|
+
|
532
|
+
latest_order_price = self.place_order_prices.get(symbol,0.0)
|
533
|
+
if order_price == latest_order_price:
|
534
|
+
self.logger.debug(f"{symbol} : 下单价格 {order_price} 未变化,不进行下单。")
|
535
|
+
return
|
536
|
+
|
537
|
+
|
538
|
+
# 下单
|
539
|
+
self.cancel_all_orders(symbol=symbol)
|
540
|
+
self.place_order(symbol=symbol, price=order_price, side=order_side, pair_config=pair_config)
|
541
|
+
self.place_order_prices[symbol] = order_price # 记录下单价格,过滤重复下单
|
542
|
+
self.logger.debug(f"{symbol} : {side}, 下单价格 {order_price}")
|
543
|
+
|
544
|
+
|
545
|
+
except KeyboardInterrupt:
|
546
|
+
self.logger.info("程序收到中断信号,开始退出...")
|
547
|
+
except Exception as e:
|
548
|
+
error_message = f"程序异常退出: {str(e)}"
|
549
|
+
self.logger.error(error_message,exc_info=True)
|
550
|
+
traceback.print_exc()
|
551
|
+
self.send_feishu_notification(error_message)
|
552
|
+
finally:
|
553
|
+
self.logger.info("-" * 60)
|
554
|
+
|
555
|
+
|
@@ -42,7 +42,9 @@ class ThreeLineStrategyMaker:
|
|
42
42
|
return self.exchange.market(symbol)
|
43
43
|
|
44
44
|
def get_tick_size(self,symbol):
|
45
|
-
|
45
|
+
market = self.getMarket(symbol)
|
46
|
+
marketPrecision = self.exchange.safe_dict(market, 'precision')
|
47
|
+
return self.exchange.safe_float(marketPrecision, 'price')
|
46
48
|
|
47
49
|
def convert_contract(self, symbol, amount, price:float, direction='cost_to_contract'):
|
48
50
|
"""
|
@@ -123,7 +125,7 @@ class ThreeLineStrategyMaker:
|
|
123
125
|
else:
|
124
126
|
raise ValueError("Unexpected response structure or missing 'last' key")
|
125
127
|
|
126
|
-
def round_price_to_tick(self,price, tick_size):
|
128
|
+
def round_price_to_tick(self, price, tick_size):
|
127
129
|
# 计算 tick_size 的小数位数
|
128
130
|
tick_decimals = len(f"{tick_size:.10f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.10f}" else 0
|
129
131
|
|
@@ -424,14 +426,15 @@ class ThreeLineStrategyMaker:
|
|
424
426
|
def place_order(self,symbol, price, amount_usdt, side,order_type='limit'):
|
425
427
|
|
426
428
|
|
427
|
-
markets = self.exchange.load_markets()
|
428
|
-
if symbol not in markets:
|
429
|
-
|
430
|
-
|
431
|
-
market = markets[symbol]
|
432
|
-
# 获取价格精度
|
433
|
-
price_precision = market['precision']['price']
|
434
|
-
|
429
|
+
# markets = self.exchange.load_markets()
|
430
|
+
# if symbol not in markets:
|
431
|
+
# self.logger.error(f"{symbol}: Instrument {symbol} not found in markets")
|
432
|
+
# return
|
433
|
+
# market = markets[symbol]
|
434
|
+
# # 获取价格精度
|
435
|
+
# price_precision = market['precision']['price']
|
436
|
+
tick_size = self.get_tick_size(symbol)
|
437
|
+
adjusted_price = self.round_price_to_tick(price, tick_size)
|
435
438
|
|
436
439
|
if amount_usdt > 0:
|
437
440
|
if side == 'buy':
|
@@ -501,15 +504,15 @@ class ThreeLineStrategyMaker:
|
|
501
504
|
|
502
505
|
self.logger.info(f"{symbol}: {order_ids} 挂单取消成功.")
|
503
506
|
else:
|
504
|
-
self.logger.info(f"{symbol}:
|
507
|
+
self.logger.info(f"{symbol}: 无挂单.")
|
505
508
|
return True
|
506
509
|
|
507
510
|
except Exception as e:
|
508
511
|
retry_count += 1
|
509
512
|
if retry_count == max_retries:
|
510
|
-
self.logger.warning(f"{symbol}
|
513
|
+
self.logger.warning(f"{symbol} 取消挂单失败(重试{retry_count}次): {str(e)}")
|
511
514
|
return False
|
512
|
-
self.logger.warning(f"{symbol}
|
515
|
+
self.logger.warning(f"{symbol} 取消挂单失败,正在进行第{retry_count}次重试: {str(e)}")
|
513
516
|
time.sleep(0.1) # 重试前等待0.1秒
|
514
517
|
|
515
518
|
def process_pair(self,symbol,pair_config):
|
@@ -666,13 +669,13 @@ class ThreeLineStrategyMaker:
|
|
666
669
|
def monitor_klines(self):
|
667
670
|
symbols = list(self.trading_pairs_config.keys()) # 获取所有币对的ID
|
668
671
|
batch_size = 5 # 每批处理的数量
|
669
|
-
while True:
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
672
|
+
# while True:
|
673
|
+
|
674
|
+
for i in range(0, len(symbols), batch_size):
|
675
|
+
batch = symbols[i:i + batch_size]
|
676
|
+
with ThreadPoolExecutor(max_workers=batch_size) as executor:
|
677
|
+
futures = [executor.submit(self.process_pair, symbol,self.trading_pairs_config[symbol]) for symbol in batch]
|
678
|
+
for future in as_completed(futures):
|
679
|
+
future.result() # Raise any exceptions caught during execution
|
677
680
|
|
678
681
|
# time.sleep(self.monitor_interval)
|
@@ -8,6 +8,7 @@ from datetime import datetime
|
|
8
8
|
from maker.WickReversalStrategyMaker import WickReversalStrategyMaker
|
9
9
|
from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
|
10
10
|
from maker.MACDStrategyMaker import MACDStrategyMaker
|
11
|
+
from maker.SMCStrategyMaker import SMCStrategyMaker
|
11
12
|
|
12
13
|
def build_logger(log_config) -> logging.Logger:
|
13
14
|
# 配置日志
|
@@ -60,7 +61,7 @@ def main():
|
|
60
61
|
package_name = __package__ or "maker"
|
61
62
|
logger.info(f" ++ {package_name}:{version} is doing...")
|
62
63
|
logger.info("开始执行交易任务...")
|
63
|
-
bot =
|
64
|
+
bot = SMCStrategyMaker(config_data, platform_config, feishu_webhook=feishu_webhook_url, logger=logger)
|
64
65
|
|
65
66
|
# 获取计划配置
|
66
67
|
schedule_config = config_data.get('schedule', {})
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|