openfund-core 0.0.4__tar.gz → 1.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_core-1.0.5/PKG-INFO +48 -0
- openfund_core-1.0.5/README.md +29 -0
- openfund_core-1.0.5/pyproject.toml +47 -0
- openfund_core-1.0.5/src/core/Exchange.py +533 -0
- openfund_core-1.0.5/src/core/main.py +23 -0
- openfund_core-1.0.5/src/core/smc/SMCBase.py +130 -0
- openfund_core-1.0.5/src/core/smc/SMCFVG.py +86 -0
- openfund_core-1.0.5/src/core/smc/SMCLiquidity.py +7 -0
- openfund_core-1.0.5/src/core/smc/SMCOrderBlock.py +280 -0
- openfund_core-1.0.5/src/core/smc/SMCPDArray.py +75 -0
- openfund_core-1.0.5/src/core/smc/SMCStruct.py +296 -0
- {openfund_core-0.0.4/tests → openfund_core-1.0.5/src/core/smc}/__init__.py +0 -0
- openfund_core-1.0.5/src/core/utils/OPTools.py +30 -0
- openfund_core-0.0.4/LICENSE +0 -201
- openfund_core-0.0.4/PKG-INFO +0 -67
- openfund_core-0.0.4/README.md +0 -45
- openfund_core-0.0.4/pyproject.toml +0 -52
- openfund_core-0.0.4/src/openfund/core/__init__.py +0 -14
- openfund_core-0.0.4/src/openfund/core/api_tools/__init__.py +0 -16
- openfund_core-0.0.4/src/openfund/core/api_tools/binance_futures_tools.py +0 -23
- openfund_core-0.0.4/src/openfund/core/api_tools/binance_tools.py +0 -26
- openfund_core-0.0.4/src/openfund/core/api_tools/enums.py +0 -539
- openfund_core-0.0.4/src/openfund/core/base_collector.py +0 -72
- openfund_core-0.0.4/src/openfund/core/base_tool.py +0 -58
- openfund_core-0.0.4/src/openfund/core/factory.py +0 -97
- openfund_core-0.0.4/src/openfund/core/openfund_old/continuous_klines.py +0 -153
- openfund_core-0.0.4/src/openfund/core/openfund_old/depth.py +0 -92
- openfund_core-0.0.4/src/openfund/core/openfund_old/historical_trades.py +0 -123
- openfund_core-0.0.4/src/openfund/core/openfund_old/index_info.py +0 -67
- openfund_core-0.0.4/src/openfund/core/openfund_old/index_price_kline.py +0 -118
- openfund_core-0.0.4/src/openfund/core/openfund_old/klines.py +0 -95
- openfund_core-0.0.4/src/openfund/core/openfund_old/klines_qrr.py +0 -103
- openfund_core-0.0.4/src/openfund/core/openfund_old/mark_price.py +0 -121
- openfund_core-0.0.4/src/openfund/core/openfund_old/mark_price_klines.py +0 -122
- openfund_core-0.0.4/src/openfund/core/openfund_old/ticker_24hr_price_change.py +0 -99
- openfund_core-0.0.4/src/openfund/core/pyopenfund.py +0 -85
- openfund_core-0.0.4/src/openfund/core/services/um_futures_collector.py +0 -142
- openfund_core-0.0.4/src/openfund/core/sycu_exam/__init__.py +0 -1
- openfund_core-0.0.4/src/openfund/core/sycu_exam/exam.py +0 -19
- openfund_core-0.0.4/src/openfund/core/sycu_exam/random_grade_cplus.py +0 -440
- openfund_core-0.0.4/src/openfund/core/sycu_exam/random_grade_web.py +0 -404
- openfund_core-0.0.4/src/openfund/core/utils/time_tools.py +0 -25
- openfund_core-0.0.4/tests/api_tools/__init__.py +0 -0
- openfund_core-0.0.4/tests/api_tools/test_binance_tools.py +0 -48
- openfund_core-0.0.4/tests/api_tools/test_binance_um_futures.py +0 -61
- openfund_core-0.0.4/tests/conftest.py +0 -77
- openfund_core-0.0.4/tests/fixtures/sample_project/expected_pyproject.toml +0 -30
- openfund_core-0.0.4/tests/fixtures/sample_project/expected_pyproject_with_exclude.toml +0 -30
- openfund_core-0.0.4/tests/fixtures/sample_project/expected_pyproject_with_latest.toml +0 -30
- openfund_core-0.0.4/tests/fixtures/sample_project/expected_pyproject_with_latest_and_pinned.toml +0 -30
- openfund_core-0.0.4/tests/fixtures/sample_project/expected_pyproject_with_latest_and_preserve_wildcard.toml +0 -30
- openfund_core-0.0.4/tests/fixtures/sample_project/pyproject.toml +0 -30
- openfund_core-0.0.4/tests/services/__init__.py +0 -0
- openfund_core-0.0.4/tests/services/test_um_futures_collector.py +0 -27
- {openfund_core-0.0.4/src/openfund/core/openfund_old → openfund_core-1.0.5/src/core}/__init__.py +0 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: openfund-core
|
3
|
+
Version: 1.0.5
|
4
|
+
Summary: Openfund-core.
|
5
|
+
Requires-Python: >=3.9,<4.0
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
7
|
+
Classifier: Programming Language :: Python :: 3.9
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
12
|
+
Requires-Dist: apscheduler (>=3.11.0,<4.0.0)
|
13
|
+
Requires-Dist: ccxt (>=4.4.26,<5.0.0)
|
14
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
15
|
+
Requires-Dist: pyfiglet (>=1.0.2,<2.0.0)
|
16
|
+
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
|
19
|
+
# jiezhen
|
20
|
+
## 读秒循环接针
|
21
|
+
**视频说明地址** :https://www.youtube.com/watch?v=b-LhdQomOxk
|
22
|
+
|
23
|
+
**环境**:`python3.9`
|
24
|
+
**开平仓模式** ,不支持单向持仓
|
25
|
+
**策略原理** :上次在火币看到一些很长的针,就想着如何把针接住,本质很简单,
|
26
|
+
1. 用ema判断短期趋势,尽可能下面挂单接住插下来的针。
|
27
|
+
2. 如果跑15m周期以上,建议用1h的ema判断多空,或者人工介入判断
|
28
|
+
|
29
|
+
**源码配置** :
|
30
|
+
config_bak.json 改成config.json
|
31
|
+
|
32
|
+
#### apiKey: OKX API 的公钥,用于身份验证。
|
33
|
+
#### secret: OKX API 的私钥,用于签名请求。
|
34
|
+
#### password: OKX 的交易密码(或 API 密码)。
|
35
|
+
#### leverage: 默认持仓杠杆倍数
|
36
|
+
#### feishu_webhook: 飞书通知地址
|
37
|
+
#### monitor_interval: 循环间隔周期 / 单位秒
|
38
|
+
|
39
|
+
|
40
|
+
## 每个交易对都可以单独设置其交易参数:
|
41
|
+
#### long_amount_usdt: 做多交易时每笔订单分配的资金量(以 USDT 为单位)。
|
42
|
+
#### short_amount_usdt: 做空交易时每笔订单分配的资金量(以 USDT 为单位)。
|
43
|
+
#### value_multiplier: 用于放大交易价值的乘数,适合调整风险/回报比。
|
44
|
+
|
45
|
+
zhen.py 跟 zhen_2.py 的区别是:https://x.com/huojichuanqi/status/1858991226877603902
|
46
|
+
|
47
|
+
打赏地址trc20: TUunBuqQ1ZDYt9WrA3ZarndFPQgefXqZAM
|
48
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# jiezhen
|
2
|
+
## 读秒循环接针
|
3
|
+
**视频说明地址** :https://www.youtube.com/watch?v=b-LhdQomOxk
|
4
|
+
|
5
|
+
**环境**:`python3.9`
|
6
|
+
**开平仓模式** ,不支持单向持仓
|
7
|
+
**策略原理** :上次在火币看到一些很长的针,就想着如何把针接住,本质很简单,
|
8
|
+
1. 用ema判断短期趋势,尽可能下面挂单接住插下来的针。
|
9
|
+
2. 如果跑15m周期以上,建议用1h的ema判断多空,或者人工介入判断
|
10
|
+
|
11
|
+
**源码配置** :
|
12
|
+
config_bak.json 改成config.json
|
13
|
+
|
14
|
+
#### apiKey: OKX API 的公钥,用于身份验证。
|
15
|
+
#### secret: OKX API 的私钥,用于签名请求。
|
16
|
+
#### password: OKX 的交易密码(或 API 密码)。
|
17
|
+
#### leverage: 默认持仓杠杆倍数
|
18
|
+
#### feishu_webhook: 飞书通知地址
|
19
|
+
#### monitor_interval: 循环间隔周期 / 单位秒
|
20
|
+
|
21
|
+
|
22
|
+
## 每个交易对都可以单独设置其交易参数:
|
23
|
+
#### long_amount_usdt: 做多交易时每笔订单分配的资金量(以 USDT 为单位)。
|
24
|
+
#### short_amount_usdt: 做空交易时每笔订单分配的资金量(以 USDT 为单位)。
|
25
|
+
#### value_multiplier: 用于放大交易价值的乘数,适合调整风险/回报比。
|
26
|
+
|
27
|
+
zhen.py 跟 zhen_2.py 的区别是:https://x.com/huojichuanqi/status/1858991226877603902
|
28
|
+
|
29
|
+
打赏地址trc20: TUunBuqQ1ZDYt9WrA3ZarndFPQgefXqZAM
|
@@ -0,0 +1,47 @@
|
|
1
|
+
[tool.poetry]
|
2
|
+
name = "openfund-core"
|
3
|
+
version = "1.0.5"
|
4
|
+
description = "Openfund-core."
|
5
|
+
authors = []
|
6
|
+
readme = "README.md"
|
7
|
+
packages = [
|
8
|
+
{ include = "core", from = "src" },
|
9
|
+
]
|
10
|
+
|
11
|
+
[tool.poetry.dependencies]
|
12
|
+
python = "^3.9"
|
13
|
+
ccxt = "^4.4.26"
|
14
|
+
pandas = "^2.2.3"
|
15
|
+
pyyaml = "^6.0.2"
|
16
|
+
apscheduler = "^3.11.0"
|
17
|
+
pyfiglet ="^1.0.2"
|
18
|
+
|
19
|
+
|
20
|
+
[tool.poetry.group.dev.dependencies]
|
21
|
+
pytest = "^7.4.4"
|
22
|
+
pytest-mock = "^3.14.0"
|
23
|
+
pytest-asyncio = "^0.23.3"
|
24
|
+
pytest-cov = "^6.1.1"
|
25
|
+
|
26
|
+
[[tool.poetry.source]]
|
27
|
+
name = "mirrors"
|
28
|
+
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
|
29
|
+
priority = "primary"
|
30
|
+
|
31
|
+
[tool.pytest.ini_options] # mandatory section name
|
32
|
+
addopts = "-vs --no-header --no-summary"
|
33
|
+
log_cli = true
|
34
|
+
log_cli_level = "DEBUG"
|
35
|
+
log_cli_format = "%(asctime)s.%(msecs)03d - %(filename)s:%(lineno)d - %(funcName)s - %(levelname)s - %(message)s"
|
36
|
+
log_cli_datefmt = "%Y-%m-%d-%H:%M:%S"
|
37
|
+
testpaths = [
|
38
|
+
"tests",
|
39
|
+
]
|
40
|
+
|
41
|
+
[tool.poetry.scripts]
|
42
|
+
openfund-core = "core.main:main"
|
43
|
+
|
44
|
+
[build-system]
|
45
|
+
requires = ["poetry-core>=1.0.0"]
|
46
|
+
build-backend = "poetry.core.masonry.api"
|
47
|
+
|
@@ -0,0 +1,533 @@
|
|
1
|
+
import logging
|
2
|
+
import time
|
3
|
+
import ccxt
|
4
|
+
import pandas as pd
|
5
|
+
|
6
|
+
|
7
|
+
from decimal import Decimal
|
8
|
+
from core.utils.OPTools import OPTools
|
9
|
+
from ccxt.base.exchange import ConstructorArgs
|
10
|
+
|
11
|
+
|
12
|
+
class Exchange:
|
13
|
+
BUY_SIDE = 'buy'
|
14
|
+
SELL_SIDE = 'sell'
|
15
|
+
LONG_KEY = 'long'
|
16
|
+
SHORT_KEY = 'short'
|
17
|
+
SIDE_KEY = 'side'
|
18
|
+
SYMBOL_KEY = 'symbol'
|
19
|
+
ENTRY_PRICE_KEY = 'entryPrice'
|
20
|
+
MARK_PRICE_KEY = 'markPrice'
|
21
|
+
CONTRACTS_KEY = 'contracts'
|
22
|
+
def __init__(self, config:ConstructorArgs, exchangeKey:str = "okx",) :
|
23
|
+
# 配置交易所
|
24
|
+
self.exchange = getattr(ccxt, exchangeKey)(config)
|
25
|
+
self.logger = logging.getLogger(__name__)
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
def getMarket(self, symbol:str):
|
30
|
+
# 配置交易对
|
31
|
+
self.exchange.load_markets()
|
32
|
+
|
33
|
+
return self.exchange.market(symbol)
|
34
|
+
|
35
|
+
def get_tick_size(self,symbol) -> Decimal:
|
36
|
+
|
37
|
+
market = self.getMarket(symbol)
|
38
|
+
if market and 'precision' in market and 'price' in market['precision']:
|
39
|
+
return OPTools.toDecimal(market['precision']['price'])
|
40
|
+
else:
|
41
|
+
raise ValueError(f"{symbol}: 无法从市场数据中获取价格精度")
|
42
|
+
|
43
|
+
def amount_to_precision(self,symbol, contract_size):
|
44
|
+
return self.exchange.amount_to_precision(symbol, contract_size)
|
45
|
+
|
46
|
+
def get_position_mode(self):
|
47
|
+
|
48
|
+
try:
|
49
|
+
# 假设获取账户持仓模式的 API
|
50
|
+
response = self.exchange.private_get_account_config()
|
51
|
+
data = response.get('data', [])
|
52
|
+
if data and isinstance(data, list):
|
53
|
+
# 取列表的第一个元素(假设它是一个字典),然后获取 'posMode'
|
54
|
+
position_mode = data[0].get('posMode', 'single') # 默认值为单向
|
55
|
+
|
56
|
+
return position_mode
|
57
|
+
else:
|
58
|
+
|
59
|
+
return 'single' # 返回默认值
|
60
|
+
except Exception as e:
|
61
|
+
error_message = f"Error fetching position mode: {e}"
|
62
|
+
self.logger.error(error_message)
|
63
|
+
raise Exception(error_message)
|
64
|
+
|
65
|
+
def set_leverage(self,symbol, leverage, mgnMode='isolated',posSide=None):
|
66
|
+
try:
|
67
|
+
# 设置杠杆
|
68
|
+
params = {
|
69
|
+
# 'instId': instId,
|
70
|
+
'leverage': leverage,
|
71
|
+
'marginMode': mgnMode
|
72
|
+
}
|
73
|
+
if posSide:
|
74
|
+
params['side'] = posSide
|
75
|
+
|
76
|
+
self.exchange.set_leverage(leverage, symbol=symbol, params=params)
|
77
|
+
self.logger.info(f"{symbol} Successfully set leverage to {leverage}x")
|
78
|
+
except Exception as e:
|
79
|
+
error_message = f"{symbol} Error setting leverage: {e}"
|
80
|
+
self.logger.error(error_message)
|
81
|
+
raise Exception(error_message)
|
82
|
+
# 获取价格精度
|
83
|
+
def get_precision_length(self,symbol) -> int:
|
84
|
+
tick_size = self.get_tick_size(symbol)
|
85
|
+
return len(f"{tick_size:.15f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.15f}" else 0
|
86
|
+
|
87
|
+
def format_price(self, symbol, price:Decimal) -> str:
|
88
|
+
precision = self.get_precision_length(symbol)
|
89
|
+
return f"{price:.{precision}f}"
|
90
|
+
|
91
|
+
def convert_contract(self, symbol, amount, price:Decimal, direction='cost_to_contract'):
|
92
|
+
"""
|
93
|
+
进行合约与币的转换
|
94
|
+
:param symbol: 交易对符号,如 'BTC/USDT:USDT'
|
95
|
+
:param amount: 输入的数量,可以是合约数量或币的数量
|
96
|
+
:param direction: 转换方向,'amount_to_contract' 表示从数量转换为合约,'cost_to_contract' 表示从金额转换为合约
|
97
|
+
:return: 转换后的数量
|
98
|
+
"""
|
99
|
+
|
100
|
+
# 获取合约规模
|
101
|
+
market_contractSize = OPTools.toDecimal(self.getMarket(symbol)['contractSize'])
|
102
|
+
amount = OPTools.toDecimal(amount)
|
103
|
+
if direction == 'amount_to_contract':
|
104
|
+
contract_size = amount / market_contractSize
|
105
|
+
elif direction == 'cost_to_contract':
|
106
|
+
contract_size = amount / price / market_contractSize
|
107
|
+
else:
|
108
|
+
raise Exception(f"{symbol}:{direction} 是无效的转换方向,请输入 'amount_to_contract' 或 'cost_to_contract'。")
|
109
|
+
|
110
|
+
return self.amount_to_precision(symbol, contract_size)
|
111
|
+
|
112
|
+
|
113
|
+
def cancel_all_orders(self, symbol):
|
114
|
+
max_retries = 3
|
115
|
+
retry_count = 0
|
116
|
+
|
117
|
+
while retry_count < max_retries:
|
118
|
+
try:
|
119
|
+
# 获取所有未完成订单
|
120
|
+
params = {
|
121
|
+
# 'instId': instId
|
122
|
+
}
|
123
|
+
open_orders = self.exchange.fetch_open_orders(symbol=symbol, params=params)
|
124
|
+
|
125
|
+
# 批量取消所有订单
|
126
|
+
if open_orders:
|
127
|
+
order_ids = [order['id'] for order in open_orders]
|
128
|
+
self.exchange.cancel_orders(order_ids, symbol, params=params)
|
129
|
+
|
130
|
+
self.logger.debug(f"{symbol}: {order_ids} 挂单取消成功.")
|
131
|
+
else:
|
132
|
+
self.logger.debug(f"{symbol}: 无挂单.")
|
133
|
+
return True
|
134
|
+
|
135
|
+
except Exception as e:
|
136
|
+
retry_count += 1
|
137
|
+
if retry_count == max_retries:
|
138
|
+
error_message = f"{symbol} 取消挂单失败(重试{retry_count}次): {str(e)}"
|
139
|
+
self.logger.error(error_message)
|
140
|
+
raise Exception(error_message)
|
141
|
+
else:
|
142
|
+
self.logger.warning(f"{symbol} 取消挂单失败,正在进行第{retry_count}次重试: {str(e)}")
|
143
|
+
time.sleep(0.1) # 重试前等待0.1秒
|
144
|
+
|
145
|
+
def cancel_all_algo_orders(self, symbol, attachType=None) -> bool:
|
146
|
+
"""_summary_
|
147
|
+
|
148
|
+
Args:
|
149
|
+
symbol (_type_): _description_
|
150
|
+
attachType (_type_, optional): "TP"|"SL". Defaults to None.
|
151
|
+
"""
|
152
|
+
|
153
|
+
params = {
|
154
|
+
"ordType": "conditional",
|
155
|
+
}
|
156
|
+
try:
|
157
|
+
orders = self.fetch_open_orders(symbol=symbol,params=params)
|
158
|
+
except Exception as e:
|
159
|
+
error_message = f"!!{symbol} : Error fetching open orders: {e}"
|
160
|
+
self.logger.error(error_message)
|
161
|
+
raise Exception(error_message)
|
162
|
+
|
163
|
+
|
164
|
+
if len(orders) == 0:
|
165
|
+
self.logger.debug(f"{symbol} 未设置策略订单列表。")
|
166
|
+
return True
|
167
|
+
|
168
|
+
algo_ids = []
|
169
|
+
if attachType and attachType == 'SL':
|
170
|
+
algo_ids = [order['id'] for order in orders if order['stopLossPrice'] and order['stopLossPrice'] > 0.0 ]
|
171
|
+
elif attachType and attachType == 'TP':
|
172
|
+
algo_ids = [order['id'] for order in orders if order['takeProfitPrice'] and order['takeProfitPrice'] > 0.0]
|
173
|
+
else :
|
174
|
+
algo_ids = [order['id'] for order in orders ]
|
175
|
+
|
176
|
+
if len(algo_ids) == 0 :
|
177
|
+
self.logger.debug(f"{symbol} 未设置策略订单列表。")
|
178
|
+
return True
|
179
|
+
|
180
|
+
max_retries = 3
|
181
|
+
retry_count = 0
|
182
|
+
|
183
|
+
while retry_count < max_retries:
|
184
|
+
try:
|
185
|
+
params = {
|
186
|
+
"algoId": algo_ids,
|
187
|
+
"trigger": 'trigger'
|
188
|
+
}
|
189
|
+
rs = self.exchange.cancel_orders(ids=algo_ids, symbol=symbol, params=params)
|
190
|
+
|
191
|
+
return len(rs) > 0
|
192
|
+
|
193
|
+
except Exception as e:
|
194
|
+
retry_count += 1
|
195
|
+
if retry_count == max_retries:
|
196
|
+
error_message = f"!!{symbol} : Error cancelling order {algo_ids}: {e}"
|
197
|
+
self.logger.error(error_message)
|
198
|
+
raise Exception(error_message)
|
199
|
+
|
200
|
+
self.logger.warning(f"{symbol} : Error cancelling order {algo_ids}: {str(e)}")
|
201
|
+
time.sleep(0.1) # 重试前等待0.1秒
|
202
|
+
def place_algo_orders(self, symbol, position, price: Decimal, order_type, sl_or_tp='SL', params={}) -> bool:
|
203
|
+
"""
|
204
|
+
下单
|
205
|
+
Args:
|
206
|
+
symbol: 交易对
|
207
|
+
position: 仓位
|
208
|
+
price: 下单价格
|
209
|
+
order_type: 订单类型
|
210
|
+
"""
|
211
|
+
# 计算下单数量
|
212
|
+
amount = abs(position[self.CONTRACTS_KEY])
|
213
|
+
|
214
|
+
if amount <= 0:
|
215
|
+
self.logger.warning(f"{symbol}: amount is 0 for {symbol}")
|
216
|
+
return
|
217
|
+
|
218
|
+
# 止损单逻辑
|
219
|
+
adjusted_price = self.format_price(symbol, price)
|
220
|
+
|
221
|
+
# 默认市价止损,委托价格为-1时,执行市价止损。
|
222
|
+
sl_params = {
|
223
|
+
**params,
|
224
|
+
'slTriggerPx':adjusted_price ,
|
225
|
+
'slOrdPx':'-1', # 委托价格为-1时,执行市价止损
|
226
|
+
# 'slOrdPx' : adjusted_price,
|
227
|
+
'slTriggerPxType':'last',
|
228
|
+
'tdMode':position['marginMode'],
|
229
|
+
'sz': str(amount),
|
230
|
+
'cxlOnClosePos': True,
|
231
|
+
'reduceOnly':True,
|
232
|
+
}
|
233
|
+
|
234
|
+
tp_params = {
|
235
|
+
**params,
|
236
|
+
'tpTriggerPx':adjusted_price,
|
237
|
+
'tpOrdPx' : adjusted_price,
|
238
|
+
'tpOrdKind': 'condition',
|
239
|
+
'tpTriggerPxType':'last',
|
240
|
+
'tdMode':position['marginMode'],
|
241
|
+
'sz': str(amount),
|
242
|
+
'cxlOnClosePos': True,
|
243
|
+
'reduceOnly':True
|
244
|
+
}
|
245
|
+
|
246
|
+
order_params = sl_params if sl_or_tp == 'SL' else tp_params
|
247
|
+
# order_params.update(params)
|
248
|
+
|
249
|
+
if order_type == 'limit' and sl_or_tp =='SL':
|
250
|
+
order_params['slOrdPx'] = adjusted_price
|
251
|
+
|
252
|
+
orderSide = self.BUY_SIDE if position[self.SIDE_KEY] == self.SHORT_KEY else self.SELL_SIDE # 和持仓反向相反下单
|
253
|
+
|
254
|
+
order = {
|
255
|
+
'symbol': symbol,
|
256
|
+
'side': orderSide,
|
257
|
+
'type': order_type,
|
258
|
+
'amount': amount,
|
259
|
+
'price': adjusted_price,
|
260
|
+
'params': order_params
|
261
|
+
}
|
262
|
+
|
263
|
+
max_retries = 3
|
264
|
+
retry_count = 0
|
265
|
+
self.logger.debug(f"{symbol} : Pre Algo Order placed: {order} ")
|
266
|
+
while retry_count < max_retries:
|
267
|
+
try:
|
268
|
+
|
269
|
+
self.exchange.create_order(
|
270
|
+
**order
|
271
|
+
# symbol=symbol,
|
272
|
+
# type=order_type,
|
273
|
+
# price=adjusted_price,
|
274
|
+
# side=orderSide,
|
275
|
+
# amount=amount,
|
276
|
+
# params=order_params
|
277
|
+
)
|
278
|
+
|
279
|
+
break
|
280
|
+
|
281
|
+
except ccxt.NetworkError as e:
|
282
|
+
# 处理网络相关错误
|
283
|
+
retry_count += 1
|
284
|
+
self.logger.warning(f"{symbol} : 设置止盈止损时发生网络错误,正在进行第{retry_count}次重试: {str(e)}")
|
285
|
+
time.sleep(0.1) # 重试前等待1秒
|
286
|
+
continue
|
287
|
+
except ccxt.ExchangeError as e:
|
288
|
+
# 处理交易所API相关错误
|
289
|
+
retry_count += 1
|
290
|
+
self.logger.warning(f"{symbol} : 设置止盈止损单时发生交易所错误,正在进行第{retry_count}次重试: {str(e)}")
|
291
|
+
time.sleep(0.1)
|
292
|
+
continue
|
293
|
+
except Exception as e:
|
294
|
+
# 处理其他未预期的错误
|
295
|
+
retry_count += 1
|
296
|
+
self.logger.warning(f"{symbol} : 设置止盈止损单时发生未知错误,正在进行第{retry_count}次重试: {str(e)}")
|
297
|
+
time.sleep(0.1)
|
298
|
+
continue
|
299
|
+
|
300
|
+
if retry_count >= max_retries:
|
301
|
+
# 重试次数用完仍未成功设置止损单
|
302
|
+
error_message = f"!! {symbol}: 设置止盈止损单时重试次数用完仍未成功设置成功。 "
|
303
|
+
self.logger.error(error_message)
|
304
|
+
raise Exception(error_message)
|
305
|
+
self.logger.debug(f"{symbol} : --------- ++ Order placed done. --------")
|
306
|
+
return True
|
307
|
+
|
308
|
+
|
309
|
+
def place_order(self, symbol, price: Decimal, amount_usdt, side, leverage=20, order_type='limit', params={}) -> bool:
|
310
|
+
"""
|
311
|
+
下单
|
312
|
+
Args:
|
313
|
+
symbol: 交易对
|
314
|
+
price: 下单价格
|
315
|
+
amount_usdt: 下单金额
|
316
|
+
side: 下单方向
|
317
|
+
order_type: 订单类型
|
318
|
+
"""
|
319
|
+
# 格式化价格
|
320
|
+
adjusted_price = self.format_price(symbol, price)
|
321
|
+
|
322
|
+
if amount_usdt <= 0:
|
323
|
+
self.logger.warning(f"{symbol}: amount_usdt must be greater than 0")
|
324
|
+
return
|
325
|
+
|
326
|
+
pos_side = self.LONG_KEY if side == self.BUY_SIDE else self.SHORT_KEY
|
327
|
+
|
328
|
+
# 设置杠杆
|
329
|
+
self.set_leverage(symbol=symbol, leverage=leverage, mgnMode='isolated',posSide=pos_side)
|
330
|
+
# 20250220 SWAP类型计算合约数量
|
331
|
+
contract_size = self.convert_contract(symbol=symbol, price = OPTools.toDecimal(adjusted_price) ,amount=amount_usdt)
|
332
|
+
|
333
|
+
order_params = {
|
334
|
+
**params,
|
335
|
+
"tdMode": 'isolated',
|
336
|
+
"side": side,
|
337
|
+
"ordType": order_type,
|
338
|
+
"sz": contract_size,
|
339
|
+
"px": adjusted_price
|
340
|
+
}
|
341
|
+
|
342
|
+
# # 模拟盘(demo_trading)需要 posSide
|
343
|
+
# if self.is_demo_trading == 1 :
|
344
|
+
# params["posSide"] = pos_side
|
345
|
+
|
346
|
+
order = {
|
347
|
+
'symbol': symbol,
|
348
|
+
'side': side,
|
349
|
+
'type': order_type,
|
350
|
+
'amount': contract_size,
|
351
|
+
'price': adjusted_price,
|
352
|
+
'params': order_params
|
353
|
+
}
|
354
|
+
|
355
|
+
max_retries = 3
|
356
|
+
retry_count = 0
|
357
|
+
|
358
|
+
while retry_count < max_retries:
|
359
|
+
try:
|
360
|
+
# 使用ccxt创建订单
|
361
|
+
self.logger.debug(f"{symbol} : Pre Order placed: {order} ")
|
362
|
+
order_result = self.exchange.create_order(
|
363
|
+
**order
|
364
|
+
# symbol=symbol,
|
365
|
+
# type='limit',
|
366
|
+
# side=side,
|
367
|
+
# amount=amount_usdt,
|
368
|
+
# price=float(adjusted_price),
|
369
|
+
# params=params
|
370
|
+
)
|
371
|
+
except ccxt.NetworkError as e:
|
372
|
+
# 处理网络相关错误
|
373
|
+
retry_count += 1
|
374
|
+
self.logger.warning(f"{symbol} : 设置下单时发生网络错误,正在进行第{retry_count}次重试: {str(e)}")
|
375
|
+
time.sleep(0.1) # 重试前等待1秒
|
376
|
+
continue
|
377
|
+
except ccxt.ExchangeError as e:
|
378
|
+
# 处理交易所API相关错误
|
379
|
+
retry_count += 1
|
380
|
+
self.logger.warning(f"{symbol} : 设置下单时发生交易所错误,正在进行第{retry_count}次重试: {str(e)}")
|
381
|
+
time.sleep(0.1)
|
382
|
+
continue
|
383
|
+
except Exception as e:
|
384
|
+
# 处理其他未预期的错误
|
385
|
+
retry_count += 1
|
386
|
+
self.logger.warning(f"{symbol} : 设置下单时发生未知错误,正在进行第{retry_count}次重试: {str(e)}")
|
387
|
+
time.sleep(0.1)
|
388
|
+
continue
|
389
|
+
if retry_count >= max_retries:
|
390
|
+
# 重试次数用完仍未成功设置止损单
|
391
|
+
error_message = f"!! {symbol}: 设置止盈止损单时重试次数用完仍未成功设置成功。 "
|
392
|
+
self.logger.error(error_message)
|
393
|
+
raise Exception(error_message)
|
394
|
+
self.logger.debug(f"{symbol} : --------- ++ Order placed done. --------")
|
395
|
+
return True
|
396
|
+
|
397
|
+
def fetch_position(self, symbol):
|
398
|
+
"""_summary_
|
399
|
+
|
400
|
+
Args:
|
401
|
+
symbol (_type_): _description_
|
402
|
+
|
403
|
+
Returns:
|
404
|
+
_type_: _description_
|
405
|
+
"""
|
406
|
+
|
407
|
+
max_retries = 3
|
408
|
+
retry_count = 0
|
409
|
+
|
410
|
+
while retry_count < max_retries:
|
411
|
+
try:
|
412
|
+
position = self.exchange.fetch_position(symbol=symbol)
|
413
|
+
if position :
|
414
|
+
# self.logger.debug(f"{symbol} 有持仓合约数: {position['contracts']}")
|
415
|
+
return position
|
416
|
+
return None
|
417
|
+
except Exception as e:
|
418
|
+
retry_count += 1
|
419
|
+
if retry_count == max_retries:
|
420
|
+
error_message = f"!!{symbol} 获取持仓失败(重试{retry_count}次): {str(e)}"
|
421
|
+
self.logger.error(error_message)
|
422
|
+
raise Exception(error_message)
|
423
|
+
|
424
|
+
self.logger.warning(f"{symbol} 检查持仓失败,正在进行第{retry_count}次重试: {str(e)}")
|
425
|
+
time.sleep(0.1) # 重试前等待0.1秒
|
426
|
+
|
427
|
+
def fetch_positions(self):
|
428
|
+
"""_summary_
|
429
|
+
Returns:
|
430
|
+
_type_: _description_
|
431
|
+
"""
|
432
|
+
max_retries = 3
|
433
|
+
retry_count = 0
|
434
|
+
|
435
|
+
while retry_count < max_retries:
|
436
|
+
try:
|
437
|
+
positions = self.exchange.fetch_positions()
|
438
|
+
return positions
|
439
|
+
except Exception as e:
|
440
|
+
retry_count += 1
|
441
|
+
if retry_count == max_retries:
|
442
|
+
error_message = f"!! 获取持仓列表失败(重试{retry_count}次): {str(e)}"
|
443
|
+
self.logger.error(error_message)
|
444
|
+
raise Exception(error_message)
|
445
|
+
|
446
|
+
self.logger.warning(f"获取持仓列表失败,正在进行第{retry_count}次重试: {str(e)}")
|
447
|
+
time.sleep(0.1) # 重试前等待0.1秒
|
448
|
+
|
449
|
+
def fetch_open_orders(self,symbol,params={}):
|
450
|
+
max_retries = 3
|
451
|
+
retry_count = 0
|
452
|
+
|
453
|
+
while retry_count < max_retries:
|
454
|
+
try:
|
455
|
+
orders = self.exchange.fetch_open_orders(symbol=symbol,params=params)
|
456
|
+
return orders
|
457
|
+
|
458
|
+
except Exception as e:
|
459
|
+
retry_count += 1
|
460
|
+
if retry_count == max_retries:
|
461
|
+
error_message = f"{symbol} : fetching open orders(retry {retry_count} times): {str(e)}"
|
462
|
+
self.logger.error(error_message)
|
463
|
+
raise Exception(error_message)
|
464
|
+
|
465
|
+
self.logger.warning(f"{symbol} : Error fetching open orders: {str(e)}")
|
466
|
+
time.sleep(0.1) # 重试前等待0.1秒
|
467
|
+
def get_market_price(self, symbol) -> Decimal:
|
468
|
+
"""
|
469
|
+
获取最新价格
|
470
|
+
Args:
|
471
|
+
symbol: 交易对
|
472
|
+
"""
|
473
|
+
max_retries = 3
|
474
|
+
retry_count = 0
|
475
|
+
|
476
|
+
while retry_count < max_retries:
|
477
|
+
try:
|
478
|
+
ticker = self.exchange.fetch_ticker(symbol)
|
479
|
+
if ticker and 'last' in ticker:
|
480
|
+
return OPTools.toDecimal(ticker['last'])
|
481
|
+
else:
|
482
|
+
raise Exception(f"{symbol} : Unexpected response structure or missing 'last' price")
|
483
|
+
except Exception as e:
|
484
|
+
retry_count += 1
|
485
|
+
if retry_count == max_retries:
|
486
|
+
error_message = f"{symbol} 获取最新价格失败(重试{retry_count}次): {str(e)}"
|
487
|
+
self.logger.error(error_message)
|
488
|
+
raise Exception(error_message)
|
489
|
+
|
490
|
+
def get_historical_klines(self, symbol, bar='15m', limit=300, after:str=None, params={}):
|
491
|
+
"""
|
492
|
+
获取历史K线数据
|
493
|
+
Args:
|
494
|
+
symbol: 交易对
|
495
|
+
bar: K线周期
|
496
|
+
limit: 数据条数
|
497
|
+
after: 之后时间,格式为 "2025-05-21 23:00:00+08:00"
|
498
|
+
"""
|
499
|
+
|
500
|
+
params = {
|
501
|
+
**params,
|
502
|
+
# 'instId': instId,
|
503
|
+
}
|
504
|
+
since = None
|
505
|
+
if after:
|
506
|
+
since = self.exchange.parse8601(after)
|
507
|
+
limit = None
|
508
|
+
if since:
|
509
|
+
params['paginate'] = True
|
510
|
+
|
511
|
+
klines = self.exchange.fetch_ohlcv(symbol, timeframe=bar,since=since, limit=limit, params=params)
|
512
|
+
# if 'data' in response and len(response['data']) > 0:
|
513
|
+
if klines :
|
514
|
+
# return response['data']
|
515
|
+
return klines
|
516
|
+
else:
|
517
|
+
raise Exception(f"{symbol} : Unexpected response structure or missing candlestick data")
|
518
|
+
|
519
|
+
def get_historical_klines_df(self, symbol, bar='15m', limit=300, after:str=None, params={}) -> pd.DataFrame:
|
520
|
+
klines = self.get_historical_klines(symbol, bar=bar, limit=limit, after=after, params=params)
|
521
|
+
return self.format_klines(klines)
|
522
|
+
|
523
|
+
def format_klines(self, klines) -> pd.DataFrame:
|
524
|
+
"""_summary_
|
525
|
+
格式化K线数据
|
526
|
+
Args:
|
527
|
+
klines (_type_): _description_
|
528
|
+
"""
|
529
|
+
klines_df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
530
|
+
# 转换时间戳为日期时间
|
531
|
+
klines_df['timestamp'] = pd.to_datetime(klines_df['timestamp'], unit='ms').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
|
532
|
+
|
533
|
+
return klines_df
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import logging
|
2
|
+
from pyfiglet import Figlet
|
3
|
+
|
4
|
+
def main():
|
5
|
+
|
6
|
+
# import importlib.metadata
|
7
|
+
# package_name = __package__ or "openfund-core"
|
8
|
+
# version = importlib.metadata.version("openfund-core")
|
9
|
+
|
10
|
+
# 创建日志记录器并设置输出到屏幕
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
console_handler = logging.StreamHandler()
|
13
|
+
logger.addHandler(console_handler)
|
14
|
+
# # 设置日志级别为INFO
|
15
|
+
logger.setLevel(logging.INFO)
|
16
|
+
|
17
|
+
f = Figlet(font="standard") # 字体可选(如 "block", "bubble")
|
18
|
+
logger.info(f"\n{f.renderText("OpenFund Core")}")
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
if __name__ == "__main__":
|
23
|
+
main()
|