openfund-maker 1.0.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/ThreeLineOrderBot.py +546 -0
- maker/WickReversalOrderBot.py +396 -0
- maker/__init__.py +0 -0
- maker/config.py +45 -0
- maker/main.py +47 -0
- maker/main_m.py +378 -0
- maker/okxapi.py +21 -0
- maker/zhen.py.bak +268 -0
- maker/zhen_2.py +261 -0
- openfund_maker-1.0.1.dist-info/METADATA +48 -0
- openfund_maker-1.0.1.dist-info/RECORD +13 -0
- openfund_maker-1.0.1.dist-info/WHEEL +4 -0
- openfund_maker-1.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,546 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
import time
|
3
|
+
import ccxt
|
4
|
+
import traceback
|
5
|
+
import requests
|
6
|
+
import pandas as pd
|
7
|
+
|
8
|
+
from logging.handlers import TimedRotatingFileHandler
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
10
|
+
|
11
|
+
|
12
|
+
class ThreeLineOrdergBot:
|
13
|
+
def __init__(self, config, platform_config, feishu_webhook=None,logger=None):
|
14
|
+
|
15
|
+
self.g_config = config
|
16
|
+
self.feishu_webhook = feishu_webhook
|
17
|
+
self.monitor_interval = self.g_config.get("monitor_interval", 4) # 默认值为60秒 # 监控循环时间是分仓监控的3倍
|
18
|
+
self.trading_pairs_config = self.g_config.get('tradingPairs', {})
|
19
|
+
self.highest_total_profit = 0 # 记录最高总盈利
|
20
|
+
self.leverage_value = self.g_config.get('leverage', 2)
|
21
|
+
self.is_demo_trading = self.g_config.get('is_demo_trading', 1) # live trading: 0, demo trading: 1
|
22
|
+
# self.instrument_info_dict = {}
|
23
|
+
self.cross_directions = {} # 持仓期间,存储每个交易对的交叉方向
|
24
|
+
|
25
|
+
# 配置交易所
|
26
|
+
self.exchange = ccxt.okx({
|
27
|
+
'apiKey': platform_config["apiKey"],
|
28
|
+
'secret': platform_config["secret"],
|
29
|
+
'password': platform_config["password"],
|
30
|
+
'timeout': 3000,
|
31
|
+
'rateLimit': 50,
|
32
|
+
'options': {'defaultType': 'future'},
|
33
|
+
'proxies': {'http': 'http://127.0.0.1:7890', 'https': 'http://127.0.0.1:7890'},
|
34
|
+
})
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
self.logger = logger
|
39
|
+
self.position_mode = self.get_position_mode() # 获取持仓模式
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
def get_position_mode(self):
|
44
|
+
try:
|
45
|
+
# 假设获取账户持仓模式的 API
|
46
|
+
response = self.exchange.private_get_account_config()
|
47
|
+
data = response.get('data', [])
|
48
|
+
if data and isinstance(data, list):
|
49
|
+
# 取列表的第一个元素(假设它是一个字典),然后获取 'posMode'
|
50
|
+
position_mode = data[0].get('posMode', 'single') # 默认值为单向
|
51
|
+
self.logger.info(f"当前持仓模式: {position_mode}")
|
52
|
+
return position_mode
|
53
|
+
else:
|
54
|
+
self.logger.error("无法检测持仓模式: 'data' 字段为空或格式不正确")
|
55
|
+
return 'single' # 返回默认值
|
56
|
+
except Exception as e:
|
57
|
+
self.logger.error(f"无法检测持仓模式: {e}")
|
58
|
+
return None
|
59
|
+
|
60
|
+
|
61
|
+
def fetch_and_store_all_instruments(self,instType='SWAP'):
|
62
|
+
try:
|
63
|
+
self.logger.info(f"Fetching all instruments for type: {instType}")
|
64
|
+
# 获取当前交易对
|
65
|
+
instruments = self.exchange.fetch_markets_by_type(type=instType)
|
66
|
+
if instruments:
|
67
|
+
# self.instrument_info_dict.clear()
|
68
|
+
for instrument in instruments:
|
69
|
+
# instId = instrument['info']['instId']
|
70
|
+
symbol = instrument['symbol']
|
71
|
+
# self.instrument_info_dict[symbol] = instrument['info']
|
72
|
+
except Exception as e:
|
73
|
+
self.logger.error(f"Error fetching instruments: {e}")
|
74
|
+
raise
|
75
|
+
|
76
|
+
def send_feishu_notification(self,message):
|
77
|
+
if self.feishu_webhook:
|
78
|
+
headers = {'Content-Type': 'application/json'}
|
79
|
+
data = {"msg_type": "text", "content": {"text": message}}
|
80
|
+
response = requests.post(self.feishu_webhook, headers=headers, json=data)
|
81
|
+
if response.status_code == 200:
|
82
|
+
self.logger.debug("飞书通知发送成功")
|
83
|
+
else:
|
84
|
+
self.logger.error(f"飞书通知发送失败: {response.text}")
|
85
|
+
# 获取K线收盘价格
|
86
|
+
def get_close_price(self,symbol):
|
87
|
+
'''
|
88
|
+
bar =
|
89
|
+
时间粒度,默认值1m
|
90
|
+
如 [1m/3m/5m/15m/30m/1H/2H/4H]
|
91
|
+
香港时间开盘价k线:[6H/12H/1D/2D/3D/1W/1M/3M]
|
92
|
+
UTC时间开盘价k线:[/6Hutc/12Hutc/1Dutc/2Dutc/3Dutc/1Wutc/1Mutc/3Mutc]
|
93
|
+
'''
|
94
|
+
# response = market_api.get_candlesticks(instId=instId,bar='1m')
|
95
|
+
klines = self.exchange.fetch_ohlcv(symbol, timeframe='1m',limit=3)
|
96
|
+
if klines:
|
97
|
+
# close_price = response['data'][0][4]
|
98
|
+
# 获取前一个K线 close price
|
99
|
+
close_price = klines[-1][4]
|
100
|
+
return float(close_price)
|
101
|
+
else:
|
102
|
+
raise ValueError("Unexpected response structure or missing 'c' value")
|
103
|
+
|
104
|
+
|
105
|
+
def get_mark_price(self,symbol):
|
106
|
+
# response = market_api.get_ticker(instId)
|
107
|
+
ticker = self.exchange.fetch_ticker(symbol)
|
108
|
+
# if 'data' in response and len(response['data']) > 0:
|
109
|
+
if ticker :
|
110
|
+
# last_price = response['data'][0]['last']
|
111
|
+
last_price = ticker['last']
|
112
|
+
return float(last_price)
|
113
|
+
else:
|
114
|
+
raise ValueError("Unexpected response structure or missing 'last' key")
|
115
|
+
|
116
|
+
def round_price_to_tick(self,price, tick_size):
|
117
|
+
# 计算 tick_size 的小数位数
|
118
|
+
tick_decimals = len(f"{tick_size:.10f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.10f}" else 0
|
119
|
+
|
120
|
+
# 调整价格为 tick_size 的整数倍
|
121
|
+
adjusted_price = round(price / tick_size) * tick_size
|
122
|
+
return f"{adjusted_price:.{tick_decimals}f}"
|
123
|
+
|
124
|
+
def get_historical_klines(self,symbol, bar='1m', limit=241):
|
125
|
+
# response = market_api.get_candlesticks(instId, bar=bar, limit=limit)
|
126
|
+
params = {
|
127
|
+
# 'instId': instId,
|
128
|
+
}
|
129
|
+
klines = self.exchange.fetch_ohlcv(symbol, timeframe=bar,limit=limit,params=params)
|
130
|
+
# if 'data' in response and len(response['data']) > 0:
|
131
|
+
if klines :
|
132
|
+
# return response['data']
|
133
|
+
return klines
|
134
|
+
else:
|
135
|
+
raise ValueError("Unexpected response structure or missing candlestick data")
|
136
|
+
|
137
|
+
def calculate_atr(self,klines, period=60):
|
138
|
+
trs = []
|
139
|
+
for i in range(1, len(klines)):
|
140
|
+
high = float(klines[i][2])
|
141
|
+
low = float(klines[i][3])
|
142
|
+
prev_close = float(klines[i-1][4])
|
143
|
+
tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
|
144
|
+
trs.append(tr)
|
145
|
+
atr = sum(trs[-period:]) / period
|
146
|
+
return atr
|
147
|
+
|
148
|
+
def calculate_sma_pandas(self,kLines,period):
|
149
|
+
"""
|
150
|
+
使用 pandas 计算 SMA
|
151
|
+
:param KLines K线
|
152
|
+
:param period: SMA 周期
|
153
|
+
:return: SMA 值
|
154
|
+
"""
|
155
|
+
df = pd.DataFrame(kLines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
156
|
+
sma = df['close'].rolling(window=period).mean()
|
157
|
+
return sma
|
158
|
+
|
159
|
+
|
160
|
+
def calculate_ema_pandas(self,kLines, period):
|
161
|
+
"""
|
162
|
+
使用 pandas 计算 EMA
|
163
|
+
:param KLines K线
|
164
|
+
:param period: EMA 周期
|
165
|
+
:return: EMA 值
|
166
|
+
"""
|
167
|
+
|
168
|
+
df = pd.DataFrame(kLines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
169
|
+
# 计算EMA
|
170
|
+
ema = df['close'].ewm(span=period, adjust=False).mean()
|
171
|
+
return ema
|
172
|
+
|
173
|
+
|
174
|
+
def calculate_average_amplitude(self,klines, period=60):
|
175
|
+
amplitudes = []
|
176
|
+
for i in range(len(klines) - period, len(klines)):
|
177
|
+
high = float(klines[i][2])
|
178
|
+
low = float(klines[i][3])
|
179
|
+
close = float(klines[i][4])
|
180
|
+
amplitude = ((high - low) / close) * 100
|
181
|
+
amplitudes.append(amplitude)
|
182
|
+
average_amplitude = sum(amplitudes) / len(amplitudes)
|
183
|
+
return average_amplitude
|
184
|
+
|
185
|
+
def cancel_all_orders(self,symbol):
|
186
|
+
try:
|
187
|
+
# 获取所有未完成订单
|
188
|
+
params = {
|
189
|
+
# 'instId': instId
|
190
|
+
}
|
191
|
+
open_orders = self.exchange.fetch_open_orders(symbol=symbol,params=params)
|
192
|
+
|
193
|
+
# 取消每个订单
|
194
|
+
for order in open_orders:
|
195
|
+
self.exchange.cancel_order(order['id'], symbol,params=params)
|
196
|
+
|
197
|
+
self.logger.info(f"{symbol} 挂单取消成功.")
|
198
|
+
except Exception as e:
|
199
|
+
self.logger.error(f"{symbol} 取消订单失败: {str(e)}")
|
200
|
+
|
201
|
+
def set_leverage(self,symbol, leverage, mgnMode='isolated',posSide=None):
|
202
|
+
try:
|
203
|
+
# 设置杠杆
|
204
|
+
params = {
|
205
|
+
# 'instId': instId,
|
206
|
+
'leverage': leverage,
|
207
|
+
'marginMode': mgnMode
|
208
|
+
}
|
209
|
+
if posSide:
|
210
|
+
params['side'] = posSide
|
211
|
+
|
212
|
+
self.exchange.set_leverage(leverage, symbol=symbol, params=params)
|
213
|
+
self.logger.debug(f"{symbol} Successfully set leverage to {leverage}x")
|
214
|
+
except Exception as e:
|
215
|
+
self.logger.error(f"{symbol} Error setting leverage: {e}")
|
216
|
+
#
|
217
|
+
def check_position(self,symbol) -> bool:
|
218
|
+
"""
|
219
|
+
检查指定交易对是否有持仓
|
220
|
+
|
221
|
+
Args:
|
222
|
+
symbol: 交易对ID
|
223
|
+
|
224
|
+
Returns:
|
225
|
+
bool: 是否有持仓
|
226
|
+
"""
|
227
|
+
try:
|
228
|
+
position = self.exchange.fetch_position(symbol=symbol)
|
229
|
+
if position and position['contracts']> 0:
|
230
|
+
self.logger.debug(f"{symbol} 有持仓合约数: {position['contracts']}")
|
231
|
+
return True
|
232
|
+
return False
|
233
|
+
except Exception as e:
|
234
|
+
self.logger.error(f"{symbol} 检查持仓失败: {str(e)}")
|
235
|
+
return False
|
236
|
+
|
237
|
+
|
238
|
+
def place_order(self,symbol, price, amount_usdt, side):
|
239
|
+
if self.check_position(symbol=symbol) :
|
240
|
+
self.logger.info(f"{symbol} 有持仓合约,不进行下单。")
|
241
|
+
return
|
242
|
+
|
243
|
+
markets = self.exchange.load_markets()
|
244
|
+
if symbol not in markets:
|
245
|
+
self.logger.error(f"{symbol}: Instrument {symbol} not found in markets")
|
246
|
+
return
|
247
|
+
market = markets[symbol]
|
248
|
+
# 获取价格精度
|
249
|
+
price_precision = market['precision']['price']
|
250
|
+
adjusted_price = self.round_price_to_tick(price, price_precision)
|
251
|
+
|
252
|
+
# okx api
|
253
|
+
# if instId not in self.instrument_info_dict:
|
254
|
+
# tick_size = float(self.instrument_info_dict[instId]['tickSz'])
|
255
|
+
# adjusted_price = self.round_price_to_tick(price, tick_size)
|
256
|
+
|
257
|
+
# response = public_api.convert_contract_coin(type='1', instId=instId, sz=str(amount_usdt), px=str(adjusted_price), unit='usdt', opType='open')
|
258
|
+
|
259
|
+
# 使用ccxt进行单位换算:将USDT金额转换为合约张数
|
260
|
+
# contract_amount = self.exchange.amount_to_precision(symbol, amount_usdt / float(adjusted_price))
|
261
|
+
# if float(contract_amount) > 0:
|
262
|
+
if amount_usdt > 0:
|
263
|
+
if side == 'buy':
|
264
|
+
pos_side = 'long'
|
265
|
+
else:
|
266
|
+
pos_side = 'short'
|
267
|
+
# 设置杠杆
|
268
|
+
self.set_leverage(symbol=symbol, leverage=self.leverage_value, mgnMode='isolated',posSide=pos_side)
|
269
|
+
params = {
|
270
|
+
|
271
|
+
"tdMode": 'isolated',
|
272
|
+
"side": side,
|
273
|
+
"ordType": 'limit',
|
274
|
+
# "sz": amount_usdt,
|
275
|
+
"px": str(adjusted_price)
|
276
|
+
}
|
277
|
+
|
278
|
+
# 模拟盘(demo_trading)需要 posSide
|
279
|
+
if self.is_demo_trading == 1 :
|
280
|
+
params["posSide"] = pos_side
|
281
|
+
|
282
|
+
# self.logger.debug(f"---- Order placed params: {params}")
|
283
|
+
try:
|
284
|
+
order = {
|
285
|
+
'symbol': symbol,
|
286
|
+
'side': side,
|
287
|
+
'type': 'limit',
|
288
|
+
'amount': amount_usdt,
|
289
|
+
'price': float(adjusted_price),
|
290
|
+
'params': params
|
291
|
+
}
|
292
|
+
# 使用ccxt创建订单
|
293
|
+
# self.logger.debug(f"Pre Order placed: {order} ")
|
294
|
+
order_result = self.exchange.create_order(
|
295
|
+
**order
|
296
|
+
# symbol=symbol,
|
297
|
+
# type='limit',
|
298
|
+
# side=side,
|
299
|
+
# amount=amount_usdt,
|
300
|
+
# price=float(adjusted_price),
|
301
|
+
# params=params
|
302
|
+
)
|
303
|
+
self.logger.debug(f"{symbol} ++ Order placed rs : {order_result}")
|
304
|
+
except Exception as e:
|
305
|
+
self.logger.error(f"{symbol} Failed to place order: {e}")
|
306
|
+
self.logger.info(f"--------- ++ {symbol} Order placed done! --------")
|
307
|
+
|
308
|
+
# 定义根据均线斜率判断 K 线方向的函数: 0 空 1 多 -1 平
|
309
|
+
def judge_k_line_direction(self,symbol, pair_config, ema) -> int:
|
310
|
+
tick_size = float(self.exchange.market(symbol)['precision']['price'])
|
311
|
+
|
312
|
+
ema_range_period = int(pair_config.get('ema_range_period', 3))
|
313
|
+
ema_range_limit = int(pair_config.get('ema_range_limit', 2))
|
314
|
+
# 20250210 判断EMA均线是否走平,用EMA周期内的极差计算,极差在阈值范围内为平
|
315
|
+
ema_ranges = ema.rolling(window=ema_range_period).max() - ema.rolling(window=ema_range_period).min()
|
316
|
+
ema_slope = ema.diff()
|
317
|
+
latest_ema_slope = ema_slope.iloc[-1]
|
318
|
+
latest_ema_range = ema_ranges.abs().iloc[-1]
|
319
|
+
|
320
|
+
direction = -1
|
321
|
+
threshold = ema_range_limit * tick_size # 极差阈值,用tick_size计算
|
322
|
+
if latest_ema_range <= threshold: # 判断EMA极差范围
|
323
|
+
direction = -1
|
324
|
+
elif latest_ema_slope > 0: # 判断斜率情况
|
325
|
+
direction = 1
|
326
|
+
elif latest_ema_slope < 0: # 判断斜率情况
|
327
|
+
direction = 0
|
328
|
+
|
329
|
+
|
330
|
+
self.logger.debug(f"{symbol}: 极差={latest_ema_range} 斜率={latest_ema_slope}, K线方向 {direction}")
|
331
|
+
|
332
|
+
return direction
|
333
|
+
|
334
|
+
def judge_cross_direction(self,fastklines,slowklines) :
|
335
|
+
|
336
|
+
# 将输入转换为 pandas 的 Series 类型
|
337
|
+
fastk = pd.Series(fastklines)
|
338
|
+
slowk = pd.Series(slowklines)
|
339
|
+
|
340
|
+
# 判断金叉:当前 fastk 大于 slowk 且前一个 fastk 小于 slowk
|
341
|
+
golden_cross = (fastk > slowk) & (fastk.shift(1) < slowk.shift(1))
|
342
|
+
# 判断死叉:当前 fastk 小于 slowk 且前一个 fastk 大于 slowk
|
343
|
+
death_cross = (fastk < slowk) & (fastk.shift(1) > slowk.shift(1))
|
344
|
+
|
345
|
+
# 从后往前查找第一个金叉或死叉
|
346
|
+
for i in range(len(fastk) - 1, -1, -1):
|
347
|
+
|
348
|
+
if golden_cross[i]:
|
349
|
+
return {
|
350
|
+
'cross': 1, # 金叉
|
351
|
+
'index': i
|
352
|
+
}
|
353
|
+
elif death_cross[i]:
|
354
|
+
return {
|
355
|
+
'cross': 0, # 死叉
|
356
|
+
'index': i
|
357
|
+
}
|
358
|
+
|
359
|
+
return {
|
360
|
+
'cross': -1, # 无交叉
|
361
|
+
'index': None
|
362
|
+
}
|
363
|
+
|
364
|
+
|
365
|
+
def calculate_price_diff(self,prices:pd.Series) -> float:
|
366
|
+
"""
|
367
|
+
计算价格列表中最后一个价格与第一个价格的差值。
|
368
|
+
Args:
|
369
|
+
prices: 价格列表。
|
370
|
+
Returns:
|
371
|
+
diff: 计算最高价列的最大值与最小值的差值
|
372
|
+
。
|
373
|
+
"""
|
374
|
+
if prices.empty:
|
375
|
+
return None
|
376
|
+
# 将价格列表转换为pandas Series格式
|
377
|
+
|
378
|
+
diff = prices.max() - prices.min()
|
379
|
+
|
380
|
+
return diff
|
381
|
+
|
382
|
+
def calculate_place_order_price(self, symbol,side,base_price, amplitude_limit, offset=1) -> float:
|
383
|
+
"""
|
384
|
+
计算开仓价格
|
385
|
+
Args:
|
386
|
+
symbol: 交易对
|
387
|
+
side: 开仓方向
|
388
|
+
base_price: 开盘价格
|
389
|
+
amplitude_limit: 振幅限制
|
390
|
+
offset: 偏移量
|
391
|
+
Returns:
|
392
|
+
place_order_price: 开仓价格
|
393
|
+
"""
|
394
|
+
tick_size = float(self.exchange.market(symbol)['precision']['price'])
|
395
|
+
place_order_price = None
|
396
|
+
# 计算止盈价格,用市场价格(取持仓期间历史最高)减去开仓价格的利润,再乘以不同阶段的止盈百分比。
|
397
|
+
|
398
|
+
if side == 'long':
|
399
|
+
place_order_price = base_price * (1- amplitude_limit/100) - offset * tick_size
|
400
|
+
else:
|
401
|
+
place_order_price = base_price * (1 + amplitude_limit/100) + offset * tick_size
|
402
|
+
self.logger.debug(f"++++ {symbol} 下单价格: {place_order_price:.9f} 方向 {side} 基准价格{base_price} 振幅限制 {amplitude_limit} ")
|
403
|
+
return float(self.round_price_to_tick(place_order_price,tick_size))
|
404
|
+
|
405
|
+
def process_pair(self,symbol,pair_config):
|
406
|
+
|
407
|
+
try:
|
408
|
+
klines = self.get_historical_klines(symbol=symbol)
|
409
|
+
# 提取收盘价数据用于计算 EMA
|
410
|
+
# 从K线数据中提取收盘价,按时间顺序排列(新数据在后)
|
411
|
+
# close_prices = [float(kline[4]) for kline in klines]
|
412
|
+
is_bullish_trend = False
|
413
|
+
is_bearish_trend = False
|
414
|
+
|
415
|
+
# 计算 快线EMA & 慢线SMA
|
416
|
+
ema_length = pair_config.get('ema', 15)
|
417
|
+
sma_length = pair_config.get('sma', 50)
|
418
|
+
|
419
|
+
# 增加 金叉死叉 方向确认的 20250209
|
420
|
+
fastk = self.calculate_ema_pandas(klines, period=ema_length)
|
421
|
+
slowk = self.calculate_sma_pandas(klines, period=sma_length)
|
422
|
+
|
423
|
+
cross_direction = self.judge_cross_direction(fastklines=fastk,slowklines=slowk)
|
424
|
+
# 更新交叉状态
|
425
|
+
if cross_direction['cross'] != -1 : #本次不一定有交叉
|
426
|
+
self.cross_directions[symbol] = cross_direction
|
427
|
+
|
428
|
+
# 最新交叉方向
|
429
|
+
last_cross_direction = self.exchange.safe_dict(self.cross_directions,symbol,None)
|
430
|
+
|
431
|
+
|
432
|
+
# 判断趋势:多头趋势或空头趋势
|
433
|
+
direction = self.judge_k_line_direction(symbol=symbol, pair_config=pair_config,ema=fastk)
|
434
|
+
if direction == 1:
|
435
|
+
is_bullish_trend = True
|
436
|
+
elif direction == 0:
|
437
|
+
is_bearish_trend = True
|
438
|
+
else:
|
439
|
+
self.logger.info(f"{symbol} 当前是震荡(平)趋势,不执行挂单!!")
|
440
|
+
return
|
441
|
+
|
442
|
+
if last_cross_direction and last_cross_direction['cross'] == 1 : # 金叉
|
443
|
+
self.logger.debug(f"{symbol} 金叉:{last_cross_direction},清理空单,挂多单!!")
|
444
|
+
is_bearish_trend = False
|
445
|
+
elif last_cross_direction and last_cross_direction['cross'] == 0 : # 死叉
|
446
|
+
self.logger.debug(f"{symbol} 死叉:{last_cross_direction},清理多单,挂空单!!")
|
447
|
+
is_bullish_trend = False
|
448
|
+
else:
|
449
|
+
self.logger.debug(f"{symbol} 当前没有金叉死叉,以快线趋势为准。!")
|
450
|
+
return
|
451
|
+
|
452
|
+
'''
|
453
|
+
取当前K线的前三根K线中最高/低的值作为止盈位。
|
454
|
+
20250210 增加开单价格约束,下单时,三线如果价格振幅小(如0.32%内),那去找到0.32%外的那根。 振幅 amplitude_limit
|
455
|
+
'''
|
456
|
+
# 获取 K 线数据
|
457
|
+
# ohlcv = self.exchange.fetch_ohlcv(symbol, '1m')
|
458
|
+
|
459
|
+
# 取当前 K 线的前三根 K 线
|
460
|
+
# previous_three_candles = ohlcv[-4:-1]
|
461
|
+
# 提取每根 K 线的最高价
|
462
|
+
|
463
|
+
# high_prices = [candle[2] for candle in previous_three_candles]
|
464
|
+
# # 提取每根 K 线的最低价
|
465
|
+
# low_prices = [candle[3] for candle in previous_three_candles]
|
466
|
+
# # 找出最大值
|
467
|
+
# max_high = max(high_prices)
|
468
|
+
# # 找出最小值
|
469
|
+
# min_low = min(low_prices)
|
470
|
+
|
471
|
+
# 取当前 K 线的前三根 K 线
|
472
|
+
|
473
|
+
df_3 = pd.DataFrame(klines[-4:-1], columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
474
|
+
low_prices = df_3['low']
|
475
|
+
high_prices = df_3['high']
|
476
|
+
max_high = high_prices.max()
|
477
|
+
min_low = low_prices.min()
|
478
|
+
|
479
|
+
# 计算当前 振幅是否超过amplitude_limit
|
480
|
+
|
481
|
+
amplitude_limit = pair_config.get('amplitude_limit', 0.32)
|
482
|
+
|
483
|
+
self.logger.debug(f"{symbol} 当前K线的前三根K线 最高价: {max_high}, 最低价: {min_low}")
|
484
|
+
|
485
|
+
self.cancel_all_orders(symbol=symbol)
|
486
|
+
|
487
|
+
long_amount_usdt = pair_config.get('long_amount_usdt', 5)
|
488
|
+
short_amount_usdt = pair_config.get('short_amount_usdt', 5)
|
489
|
+
|
490
|
+
'''
|
491
|
+
挂单线都是三线中最高/低,如果打到下单线说明趋势反转,所以应该挂和反方向的单,
|
492
|
+
|
493
|
+
'''
|
494
|
+
# 取最新K线的收盘价格
|
495
|
+
close_price = klines[-1][4]
|
496
|
+
self.logger.debug(f"-- {symbol} 最新K线 {klines[-1]}")
|
497
|
+
if is_bullish_trend:
|
498
|
+
diff = self.calculate_price_diff(prices=low_prices)
|
499
|
+
cur_amplitude_limit = diff / close_price * 100
|
500
|
+
self.logger.info(f"{symbol} 当前为上升(多)趋势,允许挂多单,振幅{cur_amplitude_limit:.3f} hight/low {max(low_prices)}/{min(low_prices)} ++")
|
501
|
+
# 振幅大于限制,直接下单,否则,根据振幅计算下单价格
|
502
|
+
if cur_amplitude_limit >= amplitude_limit:
|
503
|
+
self.place_order(symbol, min_low, long_amount_usdt, 'buy')
|
504
|
+
else:
|
505
|
+
entry_price = self.calculate_place_order_price(symbol,side='buy',base_price=min_low, amplitude_limit=amplitude_limit,offset=0)
|
506
|
+
self.place_order(symbol, entry_price ,long_amount_usdt, 'buy')
|
507
|
+
|
508
|
+
|
509
|
+
if is_bearish_trend:
|
510
|
+
diff = self.calculate_price_diff(prices=high_prices)
|
511
|
+
cur_amplitude_limit = diff / close_price * 100
|
512
|
+
self.logger.info(f"{symbol} 当前为下降(空)趋势,允许挂空单,振幅{cur_amplitude_limit:.3f} hight/low {max(high_prices)}/{min(high_prices)}--")
|
513
|
+
if cur_amplitude_limit >= amplitude_limit:
|
514
|
+
self.place_order(symbol, max_high, short_amount_usdt, 'sell')
|
515
|
+
else:
|
516
|
+
entry_price = self.calculate_place_order_price(symbol,side='sell',base_price=max_high, amplitude_limit=amplitude_limit,offset=0)
|
517
|
+
self.place_order(symbol, entry_price ,long_amount_usdt, 'sell')
|
518
|
+
|
519
|
+
|
520
|
+
except KeyboardInterrupt:
|
521
|
+
self.logger.info("程序收到中断信号,开始退出...")
|
522
|
+
except Exception as e:
|
523
|
+
error_message = f"程序异常退出: {str(e)}"
|
524
|
+
self.logger.error(error_message,exc_info=True)
|
525
|
+
traceback.print_exc()
|
526
|
+
self.send_feishu_notification(error_message)
|
527
|
+
|
528
|
+
|
529
|
+
|
530
|
+
def monitor_klines(self):
|
531
|
+
# self.fetch_and_store_all_instruments()
|
532
|
+
import importlib.metadata
|
533
|
+
|
534
|
+
version = importlib.metadata.version("openfund-wick-reversal")
|
535
|
+
self.logger.info(f" ++ openfund-wick-reversal:{version} is doing...")
|
536
|
+
symbols = list(self.trading_pairs_config.keys()) # 获取所有币对的ID
|
537
|
+
batch_size = 5 # 每批处理的数量
|
538
|
+
while True:
|
539
|
+
for i in range(0, len(symbols), batch_size):
|
540
|
+
batch = symbols[i:i + batch_size]
|
541
|
+
with ThreadPoolExecutor(max_workers=batch_size) as executor:
|
542
|
+
futures = [executor.submit(self.process_pair, symbol,self.trading_pairs_config[symbol]) for symbol in batch]
|
543
|
+
for future in as_completed(futures):
|
544
|
+
future.result() # Raise any exceptions caught during execution
|
545
|
+
|
546
|
+
time.sleep(self.monitor_interval)
|