openfund-taker 2.0.3__tar.gz → 2.0.5__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_taker-2.0.3 → openfund_taker-2.0.5}/PKG-INFO +1 -1
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/pyproject.toml +2 -2
- openfund_taker-2.0.5/src/taker/SMCSLAndTPTaker.py +470 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/TrailingSLAndTPTaker.py +4 -5
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/TrailingSLTaker.py +62 -49
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/main.py +1 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/README.md +0 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/MultiAssetOldTradingBot.py +0 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/ThreeLineTradingTaker.py +0 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/__init__.py +0 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/chua_bitget.py +0 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/chua_bn.py +0 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/chua_ok.py +0 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/chua_ok_all.py +0 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/chua_ok_bot.py +0 -0
- {openfund_taker-2.0.3 → openfund_taker-2.0.5}/src/taker/config.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "openfund-taker"
|
3
|
-
version = "2.0.
|
3
|
+
version = "2.0.5"
|
4
4
|
description = "Openfund-taker"
|
5
5
|
authors = []
|
6
6
|
readme = "README.md"
|
@@ -20,7 +20,7 @@ pyyaml = "^6.0.2"
|
|
20
20
|
pytest = "^7.4.4"
|
21
21
|
pytest-mock = "^3.14.0"
|
22
22
|
pytest-asyncio = "^0.23.3"
|
23
|
-
|
23
|
+
pytest-cov = "^6.1.1"
|
24
24
|
|
25
25
|
|
26
26
|
[[tool.poetry.source]]
|
@@ -0,0 +1,470 @@
|
|
1
|
+
from typing import override
|
2
|
+
import pandas as pd
|
3
|
+
|
4
|
+
from taker.TrailingSLTaker import TrailingSLTaker
|
5
|
+
|
6
|
+
class SMCSLAndTPTaker(TrailingSLTaker):
|
7
|
+
def __init__(self,g_config, platform_config, feishu_webhook=None, monitor_interval=4,logger=None):
|
8
|
+
super().__init__(g_config, platform_config, feishu_webhook, monitor_interval,logger)
|
9
|
+
self.global_symbol_take_profit_flag = {} # 记录每个symbol是否设置全局止盈标志
|
10
|
+
self.global_symbol_take_profit_price = {} # 记录每个symbol的止盈价格
|
11
|
+
self.htf_liquidities = {}
|
12
|
+
|
13
|
+
@override
|
14
|
+
def check_reverse_position(self,symbol,position,pair_config):
|
15
|
+
"""
|
16
|
+
检查是否有反向持仓
|
17
|
+
"""
|
18
|
+
self.logger.debug(f"{symbol}: 检查LTF-Struceture是否市价清仓。")
|
19
|
+
# 根据LTF的Struceture识别
|
20
|
+
|
21
|
+
def format_klines(self,klines) -> pd.DataFrame:
|
22
|
+
|
23
|
+
"""_summary_
|
24
|
+
格式化K线数据
|
25
|
+
Args:
|
26
|
+
klines (_type_): _description_
|
27
|
+
"""
|
28
|
+
klines_df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
29
|
+
# 转换时间戳为日期时间
|
30
|
+
klines_df['timestamp'] = pd.to_datetime(klines_df['timestamp'], unit='ms').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
|
31
|
+
|
32
|
+
return klines_df
|
33
|
+
|
34
|
+
def build_struct(self, df, prd=20, check_bounds=True, global_extremum=False) :
|
35
|
+
|
36
|
+
"""_summary_
|
37
|
+
构建SMC结构,参考 Tradingview Smart Money Concepts Probability (Expo)@Openfund
|
38
|
+
"""
|
39
|
+
data = df.copy()
|
40
|
+
data['Up'] = None
|
41
|
+
data['Dn'] = None
|
42
|
+
data['iUp'] = None
|
43
|
+
data['iDn'] = None
|
44
|
+
data['pos'] = 0
|
45
|
+
data['pattern'] = None
|
46
|
+
|
47
|
+
# 初始化 Up 和 Dn 的第一个值
|
48
|
+
data.at[0, 'Up'] = data.at[0, 'high']
|
49
|
+
data.at[0, 'Dn'] = data.at[0, 'low']
|
50
|
+
|
51
|
+
|
52
|
+
for index in range(1, len(data)):
|
53
|
+
|
54
|
+
data.at[index, 'Up'] = max(data.at[index - 1, 'Up'], data.at[index, 'high'])
|
55
|
+
data.at[index, 'Dn'] = min(data.at[index - 1, 'Dn'], data.at[index, 'low'])
|
56
|
+
data.at[index, 'pos'] = data.at[index - 1, 'pos']
|
57
|
+
data.at[index, 'iUp'] = data.at[max(0,index - 1), 'iUp'] if data.at[max(0,index - 1), 'iUp'] is not None else index
|
58
|
+
data.at[index, 'iDn'] = data.at[max(0,index - 1), 'iDn'] if data.at[max(0,index - 1), 'iDn'] is not None else index
|
59
|
+
|
60
|
+
# 寻找枢轴高点和低点
|
61
|
+
pvtHi = self.is_pivot_high(data, index, prd, check_bounds)
|
62
|
+
pvtLo = self.is_pivot_low(data, index, prd, check_bounds)
|
63
|
+
|
64
|
+
if pvtHi:
|
65
|
+
data.at[index, 'Up'] = data.at[index, 'high']
|
66
|
+
data.at[index, 'iUp'] = index
|
67
|
+
if pvtLo:
|
68
|
+
data.at[index, 'Dn'] = data.at[index, 'low']
|
69
|
+
data.at[index, 'iDn'] = index
|
70
|
+
# 寻找Bullish结构
|
71
|
+
if data.at[index, 'Up'] > data.at[index - 1, 'Up']:
|
72
|
+
data.at[index, 'iUp'] = index # TODO
|
73
|
+
if data.at[index - 1, 'pos'] <= 0:
|
74
|
+
# data.at[index, 'pattern'] = 'CHoCH (Bullish)'
|
75
|
+
data.at[index, 'pattern'] = 'Bullish_CHoCH'
|
76
|
+
data.at[index, 'pos'] = 1
|
77
|
+
elif data.at[index - 1, 'pos'] == 1 \
|
78
|
+
and data.at[index - 1, 'Up'] == data.at[max(0,index - prd), 'Up']:
|
79
|
+
data.at[index, 'pattern'] = 'Bullish_SMS'
|
80
|
+
data.at[index, 'pos'] = 2
|
81
|
+
|
82
|
+
elif data.at[index - 1, 'pos'] > 1 \
|
83
|
+
and data.at[index - 1, 'Up'] == data.at[max(0,index - prd), 'Up']:
|
84
|
+
data.at[index, 'pattern'] = 'Bullish_BMS'
|
85
|
+
data.at[index, 'pos'] = data.at[index - 1, 'pos'] + 1
|
86
|
+
|
87
|
+
elif global_extremum and data.at[index, 'Up'] < data.at[index - 1, 'Up']:
|
88
|
+
data.at[index, 'iUp'] = data.at[index - 1, 'iUp']
|
89
|
+
|
90
|
+
# # 寻找Bearish结构
|
91
|
+
if data.at[index, 'Dn'] < data.at[index - 1, 'Dn']:
|
92
|
+
data.at[index, 'iDn'] = index # TODO
|
93
|
+
if data.at[index - 1, 'pos'] >= 0:
|
94
|
+
|
95
|
+
data.at[index, 'pattern'] = 'Bearish_CHoCH'
|
96
|
+
data.at[index, 'pos'] = -1
|
97
|
+
elif data.at[index - 1, 'pos'] == -1 \
|
98
|
+
and data.at[index - 1, 'Dn'] == data.at[max(0,index - prd), 'Dn']:
|
99
|
+
data.at[index, 'pattern'] = 'Bearish_SMS'
|
100
|
+
data.at[index, 'pos'] = -2
|
101
|
+
elif data.at[index - 1, 'pos'] < -1 \
|
102
|
+
and data.at[index - 1, 'Dn'] == data.at[max(0,index - prd), 'Dn']:
|
103
|
+
data.at[index, 'pattern'] = 'Bearish_BMS'
|
104
|
+
data.at[index, 'pos'] = data.at[index - 1, 'pos'] - 1
|
105
|
+
|
106
|
+
elif global_extremum and data.at[index, 'Dn'] > data.at[index - 1, 'Dn']:
|
107
|
+
data.at[index, 'iDn'] = data.at[index - 1, 'iDn']
|
108
|
+
|
109
|
+
return data
|
110
|
+
|
111
|
+
def detect_struct(self, data, prd=20, check_valid_range=True, struct_key=None, check_bounds=True, global_extremum=False) -> dict:
|
112
|
+
"""_summary_
|
113
|
+
识别智能资金结构
|
114
|
+
|
115
|
+
:param data: 包含 'high' 和 'low' 列的 DataFrame
|
116
|
+
:param prd: 结构周期
|
117
|
+
:param s1: 结构响应布尔值
|
118
|
+
:param resp: 响应周期
|
119
|
+
:return: 包含结构识别结果的 DataFrame
|
120
|
+
"""
|
121
|
+
|
122
|
+
data = self.build_struct(data, prd, check_bounds, global_extremum)
|
123
|
+
|
124
|
+
|
125
|
+
# 获取最后一个结构和位置
|
126
|
+
last_struct = {
|
127
|
+
"struct": None,
|
128
|
+
"index": -1,
|
129
|
+
"pivot_high": None,
|
130
|
+
"pivot_high_index": -1,
|
131
|
+
"pivot_low": None,
|
132
|
+
"pivot_low_index": -1,
|
133
|
+
"side": None
|
134
|
+
|
135
|
+
}
|
136
|
+
|
137
|
+
pivot_high_index = last_struct["pivot_high_index"] = int(data["iUp"].iloc[-1])
|
138
|
+
pivot_low_index = last_struct["pivot_low_index"] = int(data["iDn"].iloc[-1])
|
139
|
+
|
140
|
+
last_struct["pivot_high"] = float(data.loc[last_struct["pivot_high_index"], 'high'])
|
141
|
+
last_struct["pivot_low"] = float(data.loc[last_struct["pivot_low_index"], 'low'])
|
142
|
+
|
143
|
+
for i in range(len(data)-1, -1, -1):
|
144
|
+
if check_valid_range:
|
145
|
+
# 检查是否在pivot_high_index和pivot_low_index之间的有效范围内
|
146
|
+
if data.at[i, 'iUp'] != -1 and data.at[i, 'iDn'] != -1:
|
147
|
+
# pivot_high_index = data.at[i, 'iUp']
|
148
|
+
# pivot_low_index = data.at[i, 'iDn']
|
149
|
+
if i < min(pivot_high_index, pivot_low_index) or i > max(pivot_high_index, pivot_low_index):
|
150
|
+
continue
|
151
|
+
|
152
|
+
if data.at[i, 'pattern'] is not None:
|
153
|
+
if struct_key is not None and struct_key not in data.at[i, 'pattern']:
|
154
|
+
continue
|
155
|
+
last_struct["struct"] = data.at[i, 'pattern']
|
156
|
+
last_struct["index"] = i
|
157
|
+
|
158
|
+
break
|
159
|
+
|
160
|
+
if last_struct['struct'] is not None :
|
161
|
+
# 找到最后一个结构的枢轴高点和低点,如果当前是孤立点,则取前一个孤立点
|
162
|
+
# 判断交易方向
|
163
|
+
if 'Bearish' in last_struct["struct"]:
|
164
|
+
last_struct["side"] = 'sell'
|
165
|
+
else :
|
166
|
+
last_struct["side"] = 'buy'
|
167
|
+
else:
|
168
|
+
last_struct['struct'] = 'None'
|
169
|
+
last_struct["index"] = -1
|
170
|
+
|
171
|
+
|
172
|
+
return last_struct
|
173
|
+
|
174
|
+
def is_pivot_high(self, data, index, period, check_bounds=False):
|
175
|
+
"""
|
176
|
+
判断当前索引处是否为枢轴高点
|
177
|
+
:param data: 包含 'high' 列的 DataFrame
|
178
|
+
:param index: 当前索引
|
179
|
+
:param period: 前后比较的周期数
|
180
|
+
:return: 是否为枢轴高点
|
181
|
+
"""
|
182
|
+
if check_bounds and (index < period or index >= len(data) - period):
|
183
|
+
return False
|
184
|
+
current_high = data.at[index, 'high']
|
185
|
+
prev_highs = data['high'].iloc[max(0,index - period):index]
|
186
|
+
next_highs = data['high'].iloc[index :min(len(data),index + period + 1)]
|
187
|
+
return all(current_high >= prev_highs) and all(current_high >= next_highs)
|
188
|
+
|
189
|
+
def is_pivot_low(self, data, index, period, check_bounds=False):
|
190
|
+
"""
|
191
|
+
判断当前索引处是否为枢轴低点
|
192
|
+
:param data: 包含 'low' 列的 DataFrame
|
193
|
+
:param index: 当前索引
|
194
|
+
:param period: 前后比较的周期数
|
195
|
+
:return: 是否为枢轴低点
|
196
|
+
"""
|
197
|
+
if check_bounds and (index < period or index >= len(data) - period):
|
198
|
+
return False
|
199
|
+
current_low = data.at[index, 'low']
|
200
|
+
prev_lows = data['low'].iloc[max(0,index - period):index]
|
201
|
+
next_lows = data['low'].iloc[index :min(len(data),index + period + 1)]
|
202
|
+
return all(current_low <= prev_lows) and all(current_low <= next_lows)
|
203
|
+
@override
|
204
|
+
def set_stop_loss_take_profit(self, symbol, position, stop_loss_price=None,take_profit_price=None) -> bool:
|
205
|
+
if not stop_loss_price :
|
206
|
+
self.logger.warning(f"{symbol}: No stop loss price or take profit price provided for {symbol}")
|
207
|
+
return False
|
208
|
+
if not position:
|
209
|
+
self.logger.warning(f"{symbol}: No position found for {symbol}")
|
210
|
+
return False
|
211
|
+
|
212
|
+
return self.set_stop_loss(symbol=symbol, position=position, stop_loss_price=stop_loss_price)
|
213
|
+
|
214
|
+
|
215
|
+
def set_sl_by_profit(self, symbol, position, profit, pair_config, kLines=None):
|
216
|
+
|
217
|
+
"""
|
218
|
+
根据利润设置止损价
|
219
|
+
Args:
|
220
|
+
symbol: 交易对
|
221
|
+
position: 仓位信息
|
222
|
+
profit: 利润
|
223
|
+
pair_config: 交易对配置
|
224
|
+
"""
|
225
|
+
|
226
|
+
total_profit = profit
|
227
|
+
|
228
|
+
current_tier = '无'
|
229
|
+
# 各档止盈逻辑
|
230
|
+
# 确定当前盈利档位
|
231
|
+
if total_profit >= self.second_trail_profit_threshold:
|
232
|
+
current_tier = "高档"
|
233
|
+
|
234
|
+
elif total_profit>= self.first_trail_profit_threshold:
|
235
|
+
current_tier = "中档"
|
236
|
+
|
237
|
+
elif total_profit >= self.low_trail_profit_threshold:
|
238
|
+
current_tier = "低档"
|
239
|
+
|
240
|
+
# 根据不同档位设置止损价格,没有单独为交易对设置,用全局参数代替
|
241
|
+
tier_config = {
|
242
|
+
"低档": {
|
243
|
+
"stop_loss_pct": float(pair_config.get('low_trail_stop_loss_pct',self.low_trail_stop_loss_pct)),
|
244
|
+
|
245
|
+
},
|
246
|
+
"中档": {
|
247
|
+
"stop_loss_pct": float(pair_config.get('trail_stop_loss_pct',self.trail_stop_loss_pct)),
|
248
|
+
|
249
|
+
},
|
250
|
+
"高档": {
|
251
|
+
"stop_loss_pct": float(pair_config.get('higher_trail_stop_loss_pct',self.higher_trail_stop_loss_pct)),
|
252
|
+
|
253
|
+
}
|
254
|
+
}
|
255
|
+
|
256
|
+
if current_tier in tier_config:
|
257
|
+
config = tier_config[current_tier]
|
258
|
+
|
259
|
+
|
260
|
+
# 计算回撤止损价格
|
261
|
+
sl_price = self.calculate_stop_loss_price(
|
262
|
+
symbol=symbol,
|
263
|
+
position=position,
|
264
|
+
stop_loss_pct=config['stop_loss_pct']
|
265
|
+
)
|
266
|
+
|
267
|
+
# 检查价格是否变化
|
268
|
+
latest_sl_price = self.exchange.safe_float(self.global_symbol_stop_loss_price, symbol, 0.0)
|
269
|
+
if sl_price == latest_sl_price:
|
270
|
+
self.logger.debug(f"{symbol}: 回撤止损价格{latest_sl_price}未变化,不设置")
|
271
|
+
return
|
272
|
+
|
273
|
+
# 移动止损保护
|
274
|
+
if_success = self.set_stop_loss(symbol=symbol, position=position, stop_loss_price=sl_price)
|
275
|
+
|
276
|
+
if if_success:
|
277
|
+
# 更新回撤止损价格
|
278
|
+
|
279
|
+
self.global_symbol_stop_loss_price[symbol] = sl_price
|
280
|
+
self.global_symbol_stop_loss_flag[symbol] = True
|
281
|
+
|
282
|
+
|
283
|
+
# 发送通知
|
284
|
+
msg = (f"{symbol}: 盈利达到【{current_tier}】阈值,最高总盈利: {cur_highest_total_profit:.2f}%,"
|
285
|
+
f"当前盈利回撤到: {total_profit:.2f}%,市场价格:{position['markPrice']},"
|
286
|
+
f"设置回撤止损位: {sl_price:.9f}")
|
287
|
+
self.logger.info(msg)
|
288
|
+
self.send_feishu_notification(msg)
|
289
|
+
|
290
|
+
else:
|
291
|
+
|
292
|
+
# 默认全局止损
|
293
|
+
self.set_global_stop_loss(symbol=symbol, position=position, pair_config=pair_config ,kLines = kLines)
|
294
|
+
|
295
|
+
def find_liquidity(self, symbol, data, side) -> pd.DataFrame:
|
296
|
+
"""
|
297
|
+
寻找流动性,根据side判断是做多还是做空,做多则寻找iUp,做空则寻找iDn
|
298
|
+
Args:
|
299
|
+
symbol (str): 交易对
|
300
|
+
data (pd.DataFrame): 数据
|
301
|
+
side (str): 交易方向,'long' 或'short'
|
302
|
+
"""
|
303
|
+
df = data.copy()
|
304
|
+
|
305
|
+
is_buy = side == 'long'
|
306
|
+
|
307
|
+
col_prefix = 'iUp' if is_buy else 'iDn'
|
308
|
+
price_col = 'Up' if is_buy else 'Dn'
|
309
|
+
|
310
|
+
return df[df.index == df[col_prefix]].sort_index(ascending=False)
|
311
|
+
|
312
|
+
def detect_liquidity(self, symbol, data, side , market_price) -> pd.DataFrame:
|
313
|
+
"""
|
314
|
+
校对流动性,用市场价格校验流动性是否有效,做多则流动性在市场价格之上,做空流动性要在市场价格之下。
|
315
|
+
Args:
|
316
|
+
symbol (str): 交易对
|
317
|
+
side (str): 交易方向,'long' 或 'short'
|
318
|
+
df_liquidities (pd.DataFrame): 流动性数据
|
319
|
+
market_price (float): 当前市场价格
|
320
|
+
|
321
|
+
"""
|
322
|
+
df_liquidities = self.find_liquidity(symbol, data, side)
|
323
|
+
|
324
|
+
df_valid_liquidities = df_liquidities.copy()
|
325
|
+
is_buy = side == 'long'
|
326
|
+
col_prefix = 'iUp' if is_buy else 'iDn'
|
327
|
+
price_col = 'Up' if is_buy else 'Dn'
|
328
|
+
|
329
|
+
result_indices = []
|
330
|
+
current_price = float('-inf') if is_buy else float('inf')
|
331
|
+
current_i = float('inf')
|
332
|
+
|
333
|
+
# 遍历并筛选符合条件的记录
|
334
|
+
for idx, row in df_valid_liquidities.iterrows():
|
335
|
+
if is_buy:
|
336
|
+
if row[price_col] > current_price and row[price_col] > market_price and row[col_prefix] < current_i:
|
337
|
+
result_indices.append(idx)
|
338
|
+
current_price = row[price_col]
|
339
|
+
current_i = row[col_prefix]
|
340
|
+
else:
|
341
|
+
if row[price_col] < current_price and row[price_col] < market_price and row[col_prefix] < current_i:
|
342
|
+
result_indices.append(idx)
|
343
|
+
current_price = row[price_col]
|
344
|
+
current_i = row[col_prefix]
|
345
|
+
|
346
|
+
return df_valid_liquidities.loc[result_indices].sort_index(ascending=False)
|
347
|
+
|
348
|
+
def calculate_tp_price_by_liquidity(self, symbol, position, df_liquidities, offset=1) -> float:
|
349
|
+
"""_summary_
|
350
|
+
计算止盈价格,根据流动性,做多则止盈价格在流动性之上,做空则止盈价格在流动性之下。
|
351
|
+
Args:
|
352
|
+
symbol (_type_): _description_
|
353
|
+
position (_type_): _description_
|
354
|
+
df_liquidities (_type_): _description_
|
355
|
+
|
356
|
+
Returns:
|
357
|
+
float: _description_
|
358
|
+
"""
|
359
|
+
tp_price = 0.0
|
360
|
+
market_price = float(position['markPrice'])
|
361
|
+
|
362
|
+
is_buy = position['side'] == 'long'
|
363
|
+
price_col = 'Up' if is_buy else 'Dn'
|
364
|
+
|
365
|
+
sl_price = self.global_symbol_stop_loss_price.get(symbol, 0.0)
|
366
|
+
entry_price = float(position['entryPrice'])
|
367
|
+
threshold = market_price
|
368
|
+
if is_buy and sl_price > 0 :
|
369
|
+
threshold = (entry_price - sl_price ) * 1.5 + entry_price
|
370
|
+
elif not is_buy and sl_price > 0:
|
371
|
+
threshold = entry_price - (sl_price - entry_price ) * 1.5
|
372
|
+
|
373
|
+
# 过滤有效的流动性价格
|
374
|
+
valid_mask = df_liquidities[price_col] > threshold if is_buy else df_liquidities[price_col] < threshold
|
375
|
+
df_valid_liquidities = df_liquidities[valid_mask]
|
376
|
+
|
377
|
+
# 获取止盈价格
|
378
|
+
tp_price = df_valid_liquidities.iloc[0][price_col] if len(df_valid_liquidities) > 0 else 0.0
|
379
|
+
tick_size = self.get_tick_size(symbol)
|
380
|
+
|
381
|
+
# 计算止盈价格 , 做多则止盈价格在流动性之下tick_size,做空则止盈价格在流动性之上tick_size。
|
382
|
+
if is_buy:
|
383
|
+
tp_price = tp_price - offset * tick_size
|
384
|
+
else:
|
385
|
+
tp_price = tp_price + offset * tick_size
|
386
|
+
|
387
|
+
|
388
|
+
return tp_price
|
389
|
+
|
390
|
+
@override
|
391
|
+
def close_all_cache(self):
|
392
|
+
super().close_all_cache()
|
393
|
+
self.htf_liquidities.clear()
|
394
|
+
self.global_symbol_take_profit_flag.clear()
|
395
|
+
self.global_symbol_take_profit_price.clear()
|
396
|
+
|
397
|
+
@override
|
398
|
+
def reset_all_cache(self, symbol):
|
399
|
+
super().reset_all_cache(symbol)
|
400
|
+
self.htf_liquidities[symbol] = None
|
401
|
+
self.global_symbol_take_profit_flag[symbol] = False
|
402
|
+
self.global_symbol_take_profit_price[symbol] = None
|
403
|
+
|
404
|
+
def set_tp_by_structure(self, symbol, position, pair_config, htf_Klines=None):
|
405
|
+
"""
|
406
|
+
根据结构设置止盈
|
407
|
+
"""
|
408
|
+
# 如果已经触发过全局止盈,则跳过
|
409
|
+
if self.global_symbol_take_profit_flag.get(symbol, False):
|
410
|
+
self.logger.info(f"{symbol}: 已经设置过全局止盈 tp_price={self.global_symbol_take_profit_price[symbol]}")
|
411
|
+
return
|
412
|
+
|
413
|
+
smc_strategy = pair_config.get('smc_strategy',{})
|
414
|
+
htf = str(smc_strategy.get('HTF','15m'))
|
415
|
+
htf_prd = int(smc_strategy.get('HTF_swing_points_length',3))
|
416
|
+
|
417
|
+
# 寻找HTF的流动性,作为止盈位置
|
418
|
+
if htf_Klines is None:
|
419
|
+
htf_Klines = self.get_historical_klines(symbol=symbol, bar=htf)
|
420
|
+
htf_df = self.format_klines(htf_Klines)
|
421
|
+
htf_df_with_struct = self.build_struct(df=htf_df,prd=htf_prd)
|
422
|
+
|
423
|
+
# 寻找流动性
|
424
|
+
htf_liquidity = self.htf_liquidities.get(symbol,None)
|
425
|
+
if htf_liquidity is None:
|
426
|
+
htf_liquidity = self.detect_liquidity(symbol, htf_df_with_struct, position['side'], position['markPrice'])
|
427
|
+
self.htf_liquidities[symbol] = htf_liquidity
|
428
|
+
|
429
|
+
if len(htf_liquidity) <= 0:
|
430
|
+
self.logger.info(f"{symbol} : 没有找到流动性,不设置止盈")
|
431
|
+
return
|
432
|
+
|
433
|
+
tp_price = self.calculate_tp_price_by_liquidity(symbol, position, htf_liquidity)
|
434
|
+
|
435
|
+
if self.set_take_profit(symbol, position, tp_price):
|
436
|
+
self.global_symbol_take_profit_flag[symbol] = True
|
437
|
+
self.global_symbol_take_profit_price[symbol] = tp_price
|
438
|
+
self.logger.info(f"{symbol} : [{position['side']}] 设置全局止盈价={tp_price}")
|
439
|
+
|
440
|
+
|
441
|
+
def check_total_profit(self, symbol, position):
|
442
|
+
"""
|
443
|
+
检查当前总盈利
|
444
|
+
"""
|
445
|
+
pair_config = self.get_pair_config(symbol)
|
446
|
+
total_profit = self.calculate_average_profit(symbol, position)
|
447
|
+
cur_highest_total_profit = self.highest_total_profit.get(symbol, 0.0)
|
448
|
+
if total_profit > cur_highest_total_profit:
|
449
|
+
self.highest_total_profit[symbol] = total_profit
|
450
|
+
|
451
|
+
msg = f"{symbol} : 盈利={total_profit:.2f}% 方向={position['side']} 开仓={position['entryPrice']:.6f} 市价={position['markPrice']:.6f}"
|
452
|
+
self.logger.info(msg)
|
453
|
+
self.send_feishu_notification(msg)
|
454
|
+
|
455
|
+
# self.cancel_all_algo_orders(symbol=symbol)
|
456
|
+
|
457
|
+
smc_strategy = pair_config.get('smc_strategy',{})
|
458
|
+
htf = str(smc_strategy.get('HTF','15m'))
|
459
|
+
htf_Klines = self.get_historical_klines(symbol=symbol, bar=htf)
|
460
|
+
|
461
|
+
|
462
|
+
# 1. 根据总盈利设置止损
|
463
|
+
self.set_sl_by_profit(symbol=symbol, position=position, profit=total_profit, pair_config=pair_config, kLines=htf_Klines)
|
464
|
+
|
465
|
+
# 2. 根据结构设置止盈
|
466
|
+
self.set_tp_by_structure(symbol=symbol, position=position, pair_config=pair_config, htf_Klines=htf_Klines)
|
467
|
+
|
468
|
+
|
469
|
+
return
|
470
|
+
|
@@ -6,17 +6,16 @@ from taker.TrailingSLTaker import TrailingSLTaker
|
|
6
6
|
class TrailingSLAndTPTaker(TrailingSLTaker):
|
7
7
|
def __init__(self,g_config, platform_config, feishu_webhook=None, monitor_interval=4,logger=None):
|
8
8
|
super().__init__(g_config, platform_config, feishu_webhook, monitor_interval,logger)
|
9
|
-
self.
|
10
|
-
self.all_take_profit_pct = self.stop_loss_pct * self.
|
9
|
+
self.all_TP_SL_ratio = float(platform_config.get("all_TP_SL_ratio",1.5)) #The profit-loss ratio 盈亏比
|
10
|
+
self.all_take_profit_pct = self.stop_loss_pct * self.all_TP_SL_ratio
|
11
11
|
|
12
12
|
def set_stop_loss_take_profit(self, symbol, position, stop_loss_price=None, take_profit_price=None) -> bool:
|
13
13
|
is_successful = super().set_stop_loss_take_profit(symbol, position, stop_loss_price, take_profit_price)
|
14
|
-
|
14
|
+
|
15
15
|
order_take_profit_price = take_profit_price
|
16
16
|
if take_profit_price is None:
|
17
17
|
order_take_profit_price = self.calculate_take_profile_price(symbol, position, self.all_take_profit_pct)
|
18
|
-
is_successful = self.set_take_profit(symbol, position, order_take_profit_price)
|
19
|
-
|
18
|
+
is_successful = self.set_take_profit(symbol, position, order_take_profit_price)
|
20
19
|
|
21
20
|
return is_successful
|
22
21
|
|
@@ -122,18 +122,25 @@ class TrailingSLTaker:
|
|
122
122
|
|
123
123
|
# 获取历史K线,不包含最新的一条
|
124
124
|
def get_historical_klines_except_last(self,symbol, bar='1m', limit=241):
|
125
|
-
|
125
|
+
|
126
|
+
klines = self.get_historical_klines(symbol, bar=bar, limit=limit)
|
127
|
+
return klines[:-1]
|
128
|
+
|
129
|
+
|
130
|
+
def get_historical_klines(self,symbol, bar='1m', limit=241):
|
131
|
+
|
126
132
|
params = {
|
127
133
|
# 'instId': instId,
|
128
134
|
}
|
129
135
|
klines = self.exchange.fetch_ohlcv(symbol, timeframe=bar,limit=limit,params=params)
|
130
|
-
|
136
|
+
|
131
137
|
if klines :
|
132
|
-
|
133
|
-
return klines
|
138
|
+
|
139
|
+
return klines
|
134
140
|
else:
|
135
141
|
raise ValueError("Unexpected response structure or missing candlestick data")
|
136
142
|
|
143
|
+
|
137
144
|
def is_pivot_high(self, data, index, period, check_bounds=False):
|
138
145
|
"""
|
139
146
|
判断当前索引处是否为枢轴高点
|
@@ -323,8 +330,8 @@ class TrailingSLTaker:
|
|
323
330
|
|
324
331
|
# 根据情况 重新修正止损
|
325
332
|
self.global_symbol_stop_loss_flag[symbol] = False
|
326
|
-
|
327
|
-
self.set_global_stop_loss(symbol=symbol,position=position,stop_loss_pct=order_stop_loss_pct
|
333
|
+
|
334
|
+
self.set_global_stop_loss(symbol=symbol,position=position, pair_config=pair_config, stop_loss_pct=order_stop_loss_pct)
|
328
335
|
|
329
336
|
|
330
337
|
except Exception as e:
|
@@ -456,6 +463,9 @@ class TrailingSLTaker:
|
|
456
463
|
self.logger.warning(f"{symbol}: No position found for {symbol}")
|
457
464
|
return False
|
458
465
|
|
466
|
+
# 取消所有策略订单
|
467
|
+
self.cancel_all_algo_orders(symbol=symbol)
|
468
|
+
|
459
469
|
if_stop_loss_success = True
|
460
470
|
if_take_profit_success = True
|
461
471
|
|
@@ -560,10 +570,6 @@ class TrailingSLTaker:
|
|
560
570
|
self.logger.warning(f"{symbol}: amount is 0 for {symbol}")
|
561
571
|
return
|
562
572
|
|
563
|
-
# 取消所有策略订单
|
564
|
-
self.cancel_all_algo_orders(symbol=symbol)
|
565
|
-
|
566
|
-
|
567
573
|
# 止损单逻辑
|
568
574
|
adjusted_price = self.round_price_to_tick(symbol, stop_loss_price)
|
569
575
|
|
@@ -638,17 +644,17 @@ class TrailingSLTaker:
|
|
638
644
|
return True
|
639
645
|
|
640
646
|
|
641
|
-
def set_global_stop_loss(self, symbol, position, stop_loss_pct=None,
|
647
|
+
def set_global_stop_loss(self, symbol, position, pair_config , stop_loss_pct=None, kLines=None):
|
642
648
|
"""设置全局止损
|
643
649
|
|
644
650
|
Args:
|
645
651
|
symbol: 交易对
|
646
652
|
position: 持仓信息
|
647
|
-
|
648
|
-
|
653
|
+
pair_config: 交易对配置
|
654
|
+
|
649
655
|
"""
|
650
656
|
# 如果已经触发过全局止损并且有止损单,则跳过
|
651
|
-
self.logger.debug(f"{symbol}
|
657
|
+
self.logger.debug(f"{symbol} : 是否设置过全局止损 {self.global_symbol_stop_loss_flag.get(symbol, False)} ")
|
652
658
|
if self.global_symbol_stop_loss_flag.get(symbol, False):
|
653
659
|
return
|
654
660
|
|
@@ -660,17 +666,17 @@ class TrailingSLTaker:
|
|
660
666
|
side = position['side']
|
661
667
|
mark_price = position['markPrice']
|
662
668
|
if side == 'long':
|
663
|
-
tp_price = position['entryPrice'] * (1 + stop_loss_pct*2/100)
|
669
|
+
# tp_price = position['entryPrice'] * (1 + stop_loss_pct*2/100)
|
664
670
|
sl_price = position['entryPrice'] * (1 - stop_loss_pct/100)
|
665
671
|
|
666
672
|
|
667
673
|
elif side == 'short':
|
668
|
-
tp_price = position['entryPrice'] * (1 - stop_loss_pct*2/100)
|
674
|
+
# tp_price = position['entryPrice'] * (1 - stop_loss_pct*2/100)
|
669
675
|
sl_price = position['entryPrice'] * (1 + stop_loss_pct/100)
|
670
676
|
|
671
677
|
|
672
678
|
sl_order_price = float(self.round_price_to_tick(symbol, sl_price))
|
673
|
-
tp_order_price = float(self.round_price_to_tick(symbol, tp_price))
|
679
|
+
# tp_order_price = float(self.round_price_to_tick(symbol, tp_price))
|
674
680
|
|
675
681
|
# 验证止损价格是否合理20250312
|
676
682
|
mark_price = float(position['markPrice'])
|
@@ -686,32 +692,33 @@ class TrailingSLTaker:
|
|
686
692
|
return
|
687
693
|
|
688
694
|
# 250228 没有指定止损回撤阈值stop_loss_pct...
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
695
|
+
open_swing_point = pair_config.get('open_swing_point', False)
|
696
|
+
if open_swing_point :
|
697
|
+
if kLines is None :
|
698
|
+
klines_period = str(pair_config.get('klines_period', '1m'))
|
699
|
+
kLines = self.get_historical_klines(symbol=symbol,bar=klines_period)
|
700
|
+
|
693
701
|
swing_points_length = pair_config.get('swing_points_length', 20)
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
self.logger.debug(f"{symbol}: - {side} ,止损价={sl_order_price} 孤立点=\n{last_isolated_point} ")
|
702
|
+
isolated_points = self.get_last_solated_point(symbol ,position , kLines,prd=swing_points_length)
|
703
|
+
|
704
|
+
if len(isolated_points) > 0 :
|
705
|
+
last_isolated_point = isolated_points[0]
|
706
|
+
|
707
|
+
if side == 'long':
|
708
|
+
|
709
|
+
sl_order_price = min(last_isolated_point['low'],sl_order_price)
|
710
|
+
else:
|
711
|
+
|
712
|
+
sl_order_price = max(last_isolated_point['high'],sl_order_price)
|
713
|
+
|
714
|
+
self.logger.debug(f"{symbol} : {side} 止损价={sl_order_price} 孤立点=\n{last_isolated_point} ")
|
708
715
|
|
709
716
|
|
710
717
|
|
711
718
|
last_sl_price= self.global_symbol_stop_loss_price.get(symbol,None)
|
712
719
|
if last_sl_price is not None and last_sl_price == sl_order_price:
|
713
720
|
self.global_symbol_stop_loss_flag[symbol] = True
|
714
|
-
self.logger.debug(f"{symbol}:
|
721
|
+
self.logger.debug(f"{symbol} : [{side}] 全局止损价没变化, {last_sl_price} = {sl_order_price}")
|
715
722
|
return
|
716
723
|
|
717
724
|
try:
|
@@ -719,23 +726,23 @@ class TrailingSLTaker:
|
|
719
726
|
if_success = self.set_stop_loss_take_profit(
|
720
727
|
symbol=symbol,
|
721
728
|
position=position,
|
722
|
-
stop_loss_price=sl_order_price
|
723
|
-
take_profit_price=tp_order_price
|
729
|
+
stop_loss_price=sl_order_price
|
730
|
+
# take_profit_price=tp_order_price
|
724
731
|
)
|
725
732
|
if if_success:
|
726
733
|
# 设置全局止损标志
|
727
|
-
self.logger.
|
734
|
+
self.logger.info(f"{symbol} : [{side}] 设置全局止损价={sl_order_price}")
|
728
735
|
self.global_symbol_stop_loss_flag[symbol] = True
|
729
736
|
self.global_symbol_stop_loss_price[symbol] = sl_order_price
|
730
737
|
|
731
738
|
except Exception as e:
|
732
|
-
error_msg = f"{symbol}
|
739
|
+
error_msg = f"{symbol} : 设置止损时发生错误, {str(e)}"
|
733
740
|
self.logger.error(error_msg)
|
734
741
|
self.send_feishu_notification(error_msg)
|
735
742
|
|
736
743
|
def calculate_take_profile_price(self, symbol, position, take_profile_pct, offset=1) -> float:
|
737
|
-
tick_size =
|
738
|
-
|
744
|
+
tick_size = self.get_tick_size(symbol)
|
745
|
+
|
739
746
|
entry_price = position['entryPrice']
|
740
747
|
side = position['side']
|
741
748
|
|
@@ -966,11 +973,20 @@ class TrailingSLTaker:
|
|
966
973
|
else:
|
967
974
|
|
968
975
|
# 默认全局止损
|
969
|
-
|
970
|
-
self.set_global_stop_loss(symbol, position,
|
976
|
+
|
977
|
+
self.set_global_stop_loss(symbol, position, pair_config)
|
971
978
|
self.logger.info(f"{symbol}: 全局止损阈值: {self.stop_loss_pct:.2f}%")
|
972
979
|
|
973
980
|
return
|
981
|
+
|
982
|
+
def close_all_cache(self):
|
983
|
+
self.reset_highest_profit_and_tier()
|
984
|
+
self.reset_take_profie()
|
985
|
+
self.positions_entry_price= {}
|
986
|
+
|
987
|
+
def reset_all_cache(self, symbol):
|
988
|
+
self.reset_highest_profit_and_tier(symbol)
|
989
|
+
self.reset_take_profie(symbol)
|
974
990
|
|
975
991
|
def monitor_total_profit(self):
|
976
992
|
self.logger.info("启动主循环,开始监控总盈利...")
|
@@ -983,9 +999,7 @@ class TrailingSLTaker:
|
|
983
999
|
# 检查是否有仓位
|
984
1000
|
if not positions:
|
985
1001
|
# self.logger.debug("没有持仓,等待下一次检查...")
|
986
|
-
self.
|
987
|
-
self.reset_take_profie()
|
988
|
-
self.positions_entry_price= {}
|
1002
|
+
self.close_all_cache()
|
989
1003
|
time.sleep(1)
|
990
1004
|
continue
|
991
1005
|
|
@@ -1006,8 +1020,7 @@ class TrailingSLTaker:
|
|
1006
1020
|
# 检查仓位是否有变化
|
1007
1021
|
if symbol in self.positions_entry_price and cur_entry_price != self.positions_entry_price[symbol]:
|
1008
1022
|
# 新开仓
|
1009
|
-
self.
|
1010
|
-
self.reset_take_profie(symbol)
|
1023
|
+
self.reset_all_cache(symbol)
|
1011
1024
|
self.positions_entry_price[symbol] = cur_entry_price
|
1012
1025
|
|
1013
1026
|
self.check_total_profit(symbol, position)
|
@@ -5,6 +5,7 @@ from logging.handlers import TimedRotatingFileHandler
|
|
5
5
|
from taker.TrailingSLTaker import TrailingSLTaker
|
6
6
|
from taker.TrailingSLAndTPTaker import TrailingSLAndTPTaker
|
7
7
|
from taker.ThreeLineTradingTaker import ThreeLineTradingTaker
|
8
|
+
from taker.SMCSLAndTPTaker import SMCSLAndTPTaker
|
8
9
|
|
9
10
|
def build_logger(log_config) -> logging.Logger:
|
10
11
|
# 配置日志
|
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
|
File without changes
|