siglab-py 0.1.30__py3-none-any.whl → 0.6.33__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.
- siglab_py/algo/__init__.py +0 -0
- siglab_py/algo/macdrsi_crosses_15m_tc_strategy.py +107 -0
- siglab_py/algo/strategy_base.py +122 -0
- siglab_py/algo/strategy_executor.py +1308 -0
- siglab_py/algo/tp_algo.py +529 -0
- siglab_py/backtests/__init__.py +0 -0
- siglab_py/backtests/backtest_core.py +2405 -0
- siglab_py/backtests/coinflip_15m_crypto.py +432 -0
- siglab_py/backtests/fibonacci_d_mv_crypto.py +541 -0
- siglab_py/backtests/macdrsi_crosses_15m_tc_crypto.py +473 -0
- siglab_py/constants.py +26 -1
- siglab_py/exchanges/binance.py +38 -0
- siglab_py/exchanges/deribit.py +83 -0
- siglab_py/exchanges/futubull.py +12 -2
- siglab_py/market_data_providers/candles_provider.py +11 -10
- siglab_py/market_data_providers/candles_ta_provider.py +5 -5
- siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +4 -4
- siglab_py/market_data_providers/futu_candles_ta_to_csv.py +7 -2
- siglab_py/market_data_providers/google_monitor.py +320 -0
- siglab_py/market_data_providers/orderbooks_provider.py +15 -12
- siglab_py/market_data_providers/tg_monitor.py +428 -0
- siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
- siglab_py/ordergateway/client.py +172 -41
- siglab_py/ordergateway/encrypt_keys_util.py +1 -1
- siglab_py/ordergateway/gateway.py +456 -347
- siglab_py/ordergateway/test_ordergateway.py +8 -7
- siglab_py/tests/integration/market_data_util_tests.py +75 -2
- siglab_py/tests/unit/analytic_util_tests.py +47 -12
- siglab_py/tests/unit/market_data_util_tests.py +45 -1
- siglab_py/tests/unit/simple_math_tests.py +252 -0
- siglab_py/tests/unit/trading_util_tests.py +65 -0
- siglab_py/util/analytic_util.py +476 -67
- siglab_py/util/datetime_util.py +39 -0
- siglab_py/util/market_data_util.py +528 -98
- siglab_py/util/module_util.py +40 -0
- siglab_py/util/notification_util.py +78 -0
- siglab_py/util/retry_util.py +16 -3
- siglab_py/util/simple_math.py +262 -0
- siglab_py/util/slack_notification_util.py +59 -0
- siglab_py/util/trading_util.py +118 -0
- {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -9
- siglab_py-0.6.33.dist-info/RECORD +56 -0
- {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
- siglab_py-0.1.30.dist-info/RECORD +0 -34
- {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
import argparse
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
import time
|
|
9
|
+
from typing import List, Dict, Any, Union
|
|
10
|
+
import json
|
|
11
|
+
import asyncio
|
|
12
|
+
from redis import StrictRedis
|
|
13
|
+
|
|
14
|
+
from siglab_py.exchanges.any_exchange import AnyExchange
|
|
15
|
+
from siglab_py.ordergateway.client import DivisiblePosition, execute_positions
|
|
16
|
+
from siglab_py.util.market_data_util import async_instantiate_exchange
|
|
17
|
+
from siglab_py.util.trading_util import calc_eff_trailing_sl
|
|
18
|
+
from siglab_py.util.notification_util import dispatch_notification
|
|
19
|
+
from siglab_py.util.aws_util import AwsKmsUtil
|
|
20
|
+
|
|
21
|
+
from siglab_py.constants import LogLevel, JSON_SERIALIZABLE_TYPES # type: ignore
|
|
22
|
+
|
|
23
|
+
current_filename = os.path.basename(__file__)
|
|
24
|
+
|
|
25
|
+
'''
|
|
26
|
+
Error: RuntimeError: aiodns needs a SelectorEventLoop on Windows.
|
|
27
|
+
Hack, by far the filthest hack I done in my career: Set SelectorEventLoop on Windows
|
|
28
|
+
'''
|
|
29
|
+
if sys.platform == 'win32':
|
|
30
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
31
|
+
|
|
32
|
+
'''
|
|
33
|
+
TP algo is an excecution algo. It doesn't decide entries for you. It however, execute the trade with target TP and SL with hard and also gradually tighted trailing stops.
|
|
34
|
+
|
|
35
|
+
For more on "Gradually Tighted Stops" and 'calc_eff_trailing_sl':
|
|
36
|
+
https://medium.com/@norman-lm-fung/gradually-tightened-trailing-stops-f7854bf1e02b
|
|
37
|
+
|
|
38
|
+
Now why does TP algo need apikey when gateway.py is sending the orders? TP algo perform position reconciliation (For contracts), and if positions don't match, algo would terminate.
|
|
39
|
+
Why would you do that? Imagine if trader want to intervene and close out positions from mobile?
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
set PYTHONPATH=%PYTHONPATH%;D:\dev\siglab\siglab_py
|
|
43
|
+
python tp_algo.py --gateway_id hyperliquid_01 --default_type linear --rate_limit_ms 100 --encrypt_decrypt_with_aws_kms Y --aws_kms_key_id xxx --apikey xxx --secret xxx --ticker SUSHI/USDC:USDC --side sell --order_type limit --amount_base_ccy 45 --slices 3 --wait_fill_threshold_ms 15000 --leg_room_bps 5 --tp_min_percent 1.5 --tp_max_percent 2.5 --sl_percent_trailing 50 --sl_percent 1 --reversal_num_intervals 2 --slack_info_url https://hooks.slack.com/services/xxx --slack_critial_url https://hooks.slack.com/services/xxx --slack_alert_url https://hooks.slack.com/services/xxx --load_entry_from_cache N
|
|
44
|
+
|
|
45
|
+
Debug from VSCode, launch.json:
|
|
46
|
+
{
|
|
47
|
+
"version": "0.2.0",
|
|
48
|
+
"configurations": [
|
|
49
|
+
{
|
|
50
|
+
"name": "Python Debugger: Current File",
|
|
51
|
+
"type": "debugpy",
|
|
52
|
+
"request": "launch",
|
|
53
|
+
"program": "${file}",
|
|
54
|
+
"console": "integratedTerminal",
|
|
55
|
+
"args" : [
|
|
56
|
+
"--gateway_id", "hyperliquid_01",
|
|
57
|
+
"--default_type", "linear",
|
|
58
|
+
"--rate_limit_ms", "100",
|
|
59
|
+
"--encrypt_decrypt_with_aws_kms", "Y",
|
|
60
|
+
"--aws_kms_key_id", "xxx",
|
|
61
|
+
"--apikey", "xxx",
|
|
62
|
+
"--secret", "xxx",
|
|
63
|
+
|
|
64
|
+
"--ticker", "SUSHI/USDC:USDC",
|
|
65
|
+
"--side", "sell",
|
|
66
|
+
"--order_type", "limit",
|
|
67
|
+
"--amount_base_ccy", "45",
|
|
68
|
+
"--slices", "3",
|
|
69
|
+
"--wait_fill_threshold_ms", "15000",
|
|
70
|
+
"--leg_room_bps", "5",
|
|
71
|
+
"--tp_min_percent", "1.5",
|
|
72
|
+
"--tp_max_percent", "2.5",
|
|
73
|
+
"--sl_percent_trailing", "50",
|
|
74
|
+
"--sl_percent", "1",
|
|
75
|
+
"--reversal_num_intervals", "2",
|
|
76
|
+
|
|
77
|
+
"--load_entry_from_cache", "N",
|
|
78
|
+
|
|
79
|
+
"--slack_info_url", "https://hooks.slack.com/services/xxx",
|
|
80
|
+
"--slack_critial_url", "https://hooks.slack.com/services/xxx",
|
|
81
|
+
"--slack_alert_url", "https://hooks.slack.com/services/xxx",
|
|
82
|
+
],
|
|
83
|
+
"env": {
|
|
84
|
+
"PYTHONPATH": "${workspaceFolder}"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
'''
|
|
90
|
+
param : Dict = {
|
|
91
|
+
'trailing_sl_min_percent_linear': 1.0, # This is threshold used for tp_algo to decide if use linear stops tightening, or non-linear. If tp_max_percent far (>100bps), there's more uncertainty if target can be reached: Go with linear.
|
|
92
|
+
'non_linear_pow' : 5, # For non-linear trailing stops tightening.
|
|
93
|
+
|
|
94
|
+
"loop_freq_ms" : 5000, # reduce this if you need trade faster
|
|
95
|
+
|
|
96
|
+
'current_filename' : current_filename,
|
|
97
|
+
|
|
98
|
+
'notification' : {
|
|
99
|
+
'footer' : None,
|
|
100
|
+
|
|
101
|
+
# slack webhook url's for notifications
|
|
102
|
+
'slack' : {
|
|
103
|
+
'info' : { 'webhook_url' : None },
|
|
104
|
+
'critical' : { 'webhook_url' : None },
|
|
105
|
+
'alert' : { 'webhook_url' : None },
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
'mds' : {
|
|
110
|
+
'topics' : {
|
|
111
|
+
|
|
112
|
+
},
|
|
113
|
+
'redis' : {
|
|
114
|
+
'host' : 'localhost',
|
|
115
|
+
'port' : 6379,
|
|
116
|
+
'db' : 0,
|
|
117
|
+
'ttl_ms' : 1000*60*15 # 15 min?
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logging.Formatter.converter = time.gmtime
|
|
123
|
+
logger = logging.getLogger()
|
|
124
|
+
log_level = logging.INFO # DEBUG --> INFO --> WARNING --> ERROR
|
|
125
|
+
logger.setLevel(log_level)
|
|
126
|
+
format_str = '%(asctime)s %(message)s'
|
|
127
|
+
formatter = logging.Formatter(format_str)
|
|
128
|
+
sh = logging.StreamHandler()
|
|
129
|
+
sh.setLevel(log_level)
|
|
130
|
+
sh.setFormatter(formatter)
|
|
131
|
+
logger.addHandler(sh)
|
|
132
|
+
|
|
133
|
+
def log(message : str, log_level : LogLevel = LogLevel.INFO):
|
|
134
|
+
if log_level.value<LogLevel.WARNING.value:
|
|
135
|
+
logger.info(f"{datetime.now()} {message}")
|
|
136
|
+
|
|
137
|
+
elif log_level.value==LogLevel.WARNING.value:
|
|
138
|
+
logger.warning(f"{datetime.now()} {message}")
|
|
139
|
+
|
|
140
|
+
elif log_level.value==LogLevel.ERROR.value:
|
|
141
|
+
logger.error(f"{datetime.now()} {message}")
|
|
142
|
+
|
|
143
|
+
def parse_args():
|
|
144
|
+
parser = argparse.ArgumentParser() # type: ignore
|
|
145
|
+
|
|
146
|
+
parser.add_argument("--gateway_id", help="gateway_id: Where are you sending your order?", default=None)
|
|
147
|
+
|
|
148
|
+
parser.add_argument("--default_type", help="default_type: spot, linear, inverse, futures ...etc", default='linear')
|
|
149
|
+
parser.add_argument("--rate_limit_ms", help="rate_limit_ms: Check your exchange rules", default=100)
|
|
150
|
+
parser.add_argument("--encrypt_decrypt_with_aws_kms", help="Y or N. If encrypt_decrypt_with_aws_kms=N, pass in apikey, secret and passphrase unencrypted (Not recommended, for testing only). If Y, they will be decrypted using AMS KMS key.", default='N')
|
|
151
|
+
parser.add_argument("--aws_kms_key_id", help="AWS KMS key ID", default=None)
|
|
152
|
+
parser.add_argument("--apikey", help="Exchange apikey", default=None)
|
|
153
|
+
parser.add_argument("--secret", help="Exchange secret", default=None)
|
|
154
|
+
parser.add_argument("--passphrase", help="Exchange passphrase", default=None)
|
|
155
|
+
|
|
156
|
+
parser.add_argument("--ticker", help="Ticker you're trading. Example BTC/USDC:USDC", default=None)
|
|
157
|
+
parser.add_argument("--side", help="order side, for entries. buy or sell", default=None)
|
|
158
|
+
parser.add_argument("--order_type", help="Order type: market or limit", default=None)
|
|
159
|
+
parser.add_argument("--amount_base_ccy", help="Order amount in base ccy (Not # contracts). Always positive, even for sell trades.", default=None)
|
|
160
|
+
parser.add_argument("--leg_room_bps", help="Leg room, for Limit orders only. A more positive leg room is a more aggressive order to get filled. i.e. Buy at higher price, Sell at lower price.", default=5)
|
|
161
|
+
parser.add_argument("--slices", help="Algo can break down larger order into smaller slices. Default: 1", default=1)
|
|
162
|
+
parser.add_argument("--wait_fill_threshold_ms", help="Limit orders will be cancelled if not filled within this time. Remainder will be sent off as market order.", default=15000)
|
|
163
|
+
|
|
164
|
+
parser.add_argument("--load_entry_from_cache", help="Y or N (default). This is for algo restart scenario where you don't want make entry again. In this case existing/running position loaded from cache.", default='N')
|
|
165
|
+
|
|
166
|
+
parser.add_argument("--tp_min_percent", help="For trailing stops. Min TP in percent, i.e. No TP until pnl at least this much.", default=None)
|
|
167
|
+
parser.add_argument("--tp_max_percent", help="For trailing stops. Max TP in percent, i.e. Price target", default=None)
|
|
168
|
+
parser.add_argument("--sl_percent_trailing", help="For trailing stops. trailing SL in percent, please refer to trading_util.calc_eff_trailing_sl for documentation.", default=None)
|
|
169
|
+
parser.add_argument("--default_effective_tp_trailing_percent", help="Default for sl_percent_trailing when pnl still below tp_min_percent. Default: float('inf'), meaing trailing stop won't be fired.", default=float('inf'))
|
|
170
|
+
parser.add_argument("--sl_percent", help="Hard stop in percent.", default=2)
|
|
171
|
+
parser.add_argument("--reversal_num_intervals", help="How many reversal candles to confirm reversal?", default=3)
|
|
172
|
+
parser.add_argument("--trailing_sl_min_percent_linear", help="This is threshold used for tp_algo to decide if use linear stops tightening, or non-linear. If tp_max_percent far (>200bps for example), there's more uncertainty if target can be reached: Go with linear. Default: 2% (200 bps)", default=2.0)
|
|
173
|
+
parser.add_argument("--non_linear_pow", help="For non-linear trailing stops tightening, have a look at call to 'calc_eff_trailing_sl'. Default: 5", default=5)
|
|
174
|
+
|
|
175
|
+
parser.add_argument("--loop_freq_ms", help="Loop delays. Reduce this if you want to trade faster.", default=5000)
|
|
176
|
+
|
|
177
|
+
parser.add_argument("--slack_info_url", help="Slack webhook url for INFO", default=None)
|
|
178
|
+
parser.add_argument("--slack_critial_url", help="Slack webhook url for CRITICAL", default=None)
|
|
179
|
+
parser.add_argument("--slack_alert_url", help="Slack webhook url for ALERT", default=None)
|
|
180
|
+
|
|
181
|
+
args = parser.parse_args()
|
|
182
|
+
|
|
183
|
+
param['gateway_id'] = args.gateway_id
|
|
184
|
+
param['default_type'] = args.default_type
|
|
185
|
+
param['rate_limit_ms'] = int(args.rate_limit_ms)
|
|
186
|
+
|
|
187
|
+
if args.encrypt_decrypt_with_aws_kms:
|
|
188
|
+
if args.encrypt_decrypt_with_aws_kms=='Y':
|
|
189
|
+
param['encrypt_decrypt_with_aws_kms'] = True
|
|
190
|
+
else:
|
|
191
|
+
param['encrypt_decrypt_with_aws_kms'] = False
|
|
192
|
+
else:
|
|
193
|
+
param['encrypt_decrypt_with_aws_kms'] = False
|
|
194
|
+
|
|
195
|
+
param['aws_kms_key_id'] = args.aws_kms_key_id
|
|
196
|
+
param['apikey'] = args.apikey
|
|
197
|
+
param['secret'] = args.secret
|
|
198
|
+
param['passphrase'] = args.passphrase
|
|
199
|
+
|
|
200
|
+
param['ticker'] = args.ticker
|
|
201
|
+
param['side'] = args.side
|
|
202
|
+
param['order_type'] = args.order_type
|
|
203
|
+
param['amount_base_ccy'] = float(args.amount_base_ccy)
|
|
204
|
+
param['leg_room_bps'] = int(args.leg_room_bps)
|
|
205
|
+
param['slices'] = int(args.slices)
|
|
206
|
+
param['wait_fill_threshold_ms'] = int(args.wait_fill_threshold_ms)
|
|
207
|
+
|
|
208
|
+
if args.load_entry_from_cache:
|
|
209
|
+
if args.load_entry_from_cache=='Y':
|
|
210
|
+
param['load_entry_from_cache'] = True
|
|
211
|
+
else:
|
|
212
|
+
param['load_entry_from_cache'] = False
|
|
213
|
+
else:
|
|
214
|
+
param['load_entry_from_cache'] = False
|
|
215
|
+
|
|
216
|
+
param['tp_min_percent'] = float(args.tp_min_percent)
|
|
217
|
+
param['tp_max_percent'] = float(args.tp_max_percent)
|
|
218
|
+
param['sl_percent_trailing'] = float(args.sl_percent_trailing)
|
|
219
|
+
param['default_effective_tp_trailing_percent'] = float(args.default_effective_tp_trailing_percent)
|
|
220
|
+
param['sl_percent'] = float(args.sl_percent)
|
|
221
|
+
param['reversal_num_intervals'] = int(args.reversal_num_intervals)
|
|
222
|
+
param['trailing_sl_min_percent_linear'] = float(args.trailing_sl_min_percent_linear)
|
|
223
|
+
param['non_linear_pow'] = float(args.non_linear_pow)
|
|
224
|
+
|
|
225
|
+
param['loop_freq_ms'] = int(args.loop_freq_ms)
|
|
226
|
+
|
|
227
|
+
param['notification']['slack']['info']['webhook_url'] = args.slack_info_url
|
|
228
|
+
param['notification']['slack']['critical']['webhook_url'] = args.slack_critial_url
|
|
229
|
+
param['notification']['slack']['alert']['webhook_url'] = args.slack_alert_url
|
|
230
|
+
|
|
231
|
+
param['notification']['footer'] = f"From {param['current_filename']} {param['gateway_id']}"
|
|
232
|
+
|
|
233
|
+
def init_redis_client() -> StrictRedis:
|
|
234
|
+
redis_client : StrictRedis = StrictRedis(
|
|
235
|
+
host = param['mds']['redis']['host'],
|
|
236
|
+
port = param['mds']['redis']['port'],
|
|
237
|
+
db = 0,
|
|
238
|
+
ssl = False
|
|
239
|
+
)
|
|
240
|
+
try:
|
|
241
|
+
redis_client.keys()
|
|
242
|
+
except ConnectionError as redis_conn_error:
|
|
243
|
+
err_msg = f"Failed to connect to redis: {param['mds']['redis']['host']}, port: {param['mds']['redis']['port']}"
|
|
244
|
+
raise ConnectionError(err_msg)
|
|
245
|
+
|
|
246
|
+
return redis_client
|
|
247
|
+
|
|
248
|
+
async def main():
|
|
249
|
+
parse_args()
|
|
250
|
+
redis_client : StrictRedis = init_redis_client()
|
|
251
|
+
|
|
252
|
+
gateway_id : str = param['gateway_id']
|
|
253
|
+
ordergateway_pending_orders_topic = 'ordergateway_pending_orders_$GATEWAY_ID$'
|
|
254
|
+
ordergateway_pending_orders_topic = ordergateway_pending_orders_topic.replace("$GATEWAY_ID$", gateway_id)
|
|
255
|
+
|
|
256
|
+
ordergateway_executions_topic = "ordergateway_executions_$GATEWAY_ID$"
|
|
257
|
+
ordergateway_executions_topic = ordergateway_executions_topic.replace("$GATEWAY_ID$", gateway_id)
|
|
258
|
+
|
|
259
|
+
position_cacnle_file : str = f"tp_algo_position_cache_{gateway_id}.csv"
|
|
260
|
+
|
|
261
|
+
notification_params : Dict[str, Any] = param['notification']
|
|
262
|
+
|
|
263
|
+
if not param['apikey']:
|
|
264
|
+
log("Loading credentials from .env")
|
|
265
|
+
|
|
266
|
+
load_dotenv()
|
|
267
|
+
|
|
268
|
+
encrypt_decrypt_with_aws_kms = os.getenv('ENCRYPT_DECRYPT_WITH_AWS_KMS')
|
|
269
|
+
encrypt_decrypt_with_aws_kms = True if encrypt_decrypt_with_aws_kms=='Y' else False
|
|
270
|
+
|
|
271
|
+
api_key : str = str(os.getenv('APIKEY'))
|
|
272
|
+
secret : str = str(os.getenv('SECRET'))
|
|
273
|
+
passphrase : str = str(os.getenv('PASSPHRASE'))
|
|
274
|
+
else:
|
|
275
|
+
log("Loading credentials from command line args")
|
|
276
|
+
|
|
277
|
+
encrypt_decrypt_with_aws_kms = param['encrypt_decrypt_with_aws_kms']
|
|
278
|
+
api_key : str = param['apikey']
|
|
279
|
+
secret : str = param['secret']
|
|
280
|
+
passphrase : str = param['passphrase']
|
|
281
|
+
|
|
282
|
+
if encrypt_decrypt_with_aws_kms:
|
|
283
|
+
aws_kms_key_id = str(os.getenv('AWS_KMS_KEY_ID'))
|
|
284
|
+
|
|
285
|
+
aws_kms = AwsKmsUtil(key_id=aws_kms_key_id, profile_name=None)
|
|
286
|
+
api_key = aws_kms.decrypt(api_key.encode())
|
|
287
|
+
secret = aws_kms.decrypt(secret.encode())
|
|
288
|
+
if passphrase:
|
|
289
|
+
passphrase = aws_kms.decrypt(passphrase.encode())
|
|
290
|
+
|
|
291
|
+
exchange : Union[AnyExchange, None] = await async_instantiate_exchange(
|
|
292
|
+
gateway_id=param['gateway_id'],
|
|
293
|
+
api_key=api_key,
|
|
294
|
+
secret=secret,
|
|
295
|
+
passphrase=passphrase,
|
|
296
|
+
default_type=param['default_type'],
|
|
297
|
+
rate_limit_ms=param['rate_limit_ms']
|
|
298
|
+
)
|
|
299
|
+
if exchange:
|
|
300
|
+
markets = await exchange.load_markets() # type: ignore
|
|
301
|
+
market = markets[param['ticker']]
|
|
302
|
+
multiplier = market['contractSize'] if 'contractSize' in market and market['contractSize'] else 1
|
|
303
|
+
|
|
304
|
+
balances = await exchange.fetch_balance() # type: ignore
|
|
305
|
+
log(f"Balances: {json.dumps(balances, indent=4)}") # type: ignore
|
|
306
|
+
|
|
307
|
+
if not param['load_entry_from_cache']:
|
|
308
|
+
# STEP 1. Make entry
|
|
309
|
+
entry_positions : List[DivisiblePosition] = [
|
|
310
|
+
DivisiblePosition(
|
|
311
|
+
ticker = param['ticker'],
|
|
312
|
+
side = param['side'],
|
|
313
|
+
amount = param['amount_base_ccy'],
|
|
314
|
+
leg_room_bps = param['leg_room_bps'],
|
|
315
|
+
order_type = param['order_type'],
|
|
316
|
+
slices = param['slices'],
|
|
317
|
+
wait_fill_threshold_ms = param['wait_fill_threshold_ms']
|
|
318
|
+
)
|
|
319
|
+
]
|
|
320
|
+
executed_positions : Union[Dict[JSON_SERIALIZABLE_TYPES, JSON_SERIALIZABLE_TYPES], None] = execute_positions(
|
|
321
|
+
redis_client=redis_client,
|
|
322
|
+
positions=entry_positions,
|
|
323
|
+
ordergateway_pending_orders_topic=ordergateway_pending_orders_topic,
|
|
324
|
+
ordergateway_executions_topic=ordergateway_executions_topic
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
with open(position_cacnle_file, 'r') as f:
|
|
328
|
+
executed_position = json.load(f)
|
|
329
|
+
executed_positions = [ executed_position ]
|
|
330
|
+
|
|
331
|
+
if executed_positions or param['load_entry_from_cache']:
|
|
332
|
+
executed_position = executed_positions[0] # We sent only one DivisiblePosition.
|
|
333
|
+
log(f"{'Entry dispatched.' if not param['load_entry_from_cache'] else 'Entry loaded from cache.'} {json.dumps(executed_position, indent=4)}") # type: ignore
|
|
334
|
+
if executed_position['done']:
|
|
335
|
+
def _reversal(
|
|
336
|
+
direction : str, # up or down
|
|
337
|
+
last_candles
|
|
338
|
+
) -> bool:
|
|
339
|
+
if direction == "down" and all([ candle[1]<candle[4] for candle in last_candles ]): # All green?
|
|
340
|
+
return True
|
|
341
|
+
elif direction == "up" and all([ candle[1]>candle[4] for candle in last_candles ]): # All red?
|
|
342
|
+
return True
|
|
343
|
+
else:
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
entry_price = executed_position['average_cost']
|
|
347
|
+
amount_filled_usdt = entry_price * abs(executed_position['filled_amount'])
|
|
348
|
+
if param['side'] == 'buy':
|
|
349
|
+
tp_min_price = entry_price * (1+param['tp_min_percent']/100)
|
|
350
|
+
tp_max_price = entry_price * (1+param['tp_max_percent']/100)
|
|
351
|
+
sl_price = entry_price * (1-param['sl_percent']/100)
|
|
352
|
+
else:
|
|
353
|
+
tp_min_price = entry_price * (1-param['tp_min_percent']/100)
|
|
354
|
+
tp_max_price = entry_price * (1-param['tp_max_percent']/100)
|
|
355
|
+
sl_price = entry_price * (1+param['sl_percent']/100)
|
|
356
|
+
|
|
357
|
+
if not param['load_entry_from_cache']:
|
|
358
|
+
executed_position['position'] = {
|
|
359
|
+
'status' : 'open',
|
|
360
|
+
'max_unrealized_pnl' : 0,
|
|
361
|
+
'entry_price' : entry_price,
|
|
362
|
+
'amount_base_ccy' : executed_position['filled_amount'],
|
|
363
|
+
'tp_min_price' : tp_min_price,
|
|
364
|
+
'tp_max_price' : tp_max_price,
|
|
365
|
+
'sl_price' : sl_price,
|
|
366
|
+
'multiplier' : multiplier
|
|
367
|
+
}
|
|
368
|
+
with open(position_cacnle_file, 'w') as f:
|
|
369
|
+
json.dump(executed_position, f)
|
|
370
|
+
|
|
371
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Execution done, Entry succeeded. {param['ticker']} {param['side']} {param['amount_base_ccy']} (USD amount: {amount_filled_usdt}) @ {entry_price}", message=executed_position['position'], footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
|
|
372
|
+
|
|
373
|
+
unrealized_pnl : float = 0
|
|
374
|
+
max_unrealized_pnl : float = 0
|
|
375
|
+
unrealized_pnl_percent : float = 0
|
|
376
|
+
max_unrealized_pnl_percent : float = 0
|
|
377
|
+
loss_trailing : float = 0
|
|
378
|
+
effective_tp_trailing_percent : float = param['default_effective_tp_trailing_percent']
|
|
379
|
+
reversal : bool = False
|
|
380
|
+
tp : bool = False
|
|
381
|
+
sl : bool = False
|
|
382
|
+
position_break : bool = False
|
|
383
|
+
while (not tp and not sl and not position_break):
|
|
384
|
+
try:
|
|
385
|
+
position_from_exchange = await exchange.fetch_position(param['ticker'])
|
|
386
|
+
|
|
387
|
+
if exchange.options['defaultType']!='spot':
|
|
388
|
+
if not position_from_exchange and param['load_entry_from_cache']:
|
|
389
|
+
position_break = True
|
|
390
|
+
|
|
391
|
+
err_msg = f"{param['ticker']}: Position break! expected: {executed_position['position']['amount_base_ccy']}, actual: 0"
|
|
392
|
+
log(err_msg)
|
|
393
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Position break! {param['ticker']}", message=err_msg, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
|
|
394
|
+
|
|
395
|
+
if position_from_exchange:
|
|
396
|
+
position_from_exchange_num_contracts = position_from_exchange['contracts']
|
|
397
|
+
if position_from_exchange and position_from_exchange['side']=='short':
|
|
398
|
+
position_from_exchange_num_contracts = position_from_exchange_num_contracts *-1 if position_from_exchange_num_contracts>0 else position_from_exchange_num_contracts
|
|
399
|
+
|
|
400
|
+
position_from_exchange_base_ccy = position_from_exchange_num_contracts * multiplier
|
|
401
|
+
|
|
402
|
+
if position_from_exchange_base_ccy!=executed_position['position']['amount_base_ccy']:
|
|
403
|
+
position_break = True
|
|
404
|
+
|
|
405
|
+
err_msg = f"{param['ticker']}: Position break! expected: {executed_position['position']['amount_base_ccy']}, actual: {position_from_exchange_base_ccy}"
|
|
406
|
+
log(err_msg)
|
|
407
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Position break! {param['ticker']}", message=err_msg, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
|
|
408
|
+
|
|
409
|
+
if position_break:
|
|
410
|
+
log(f"Position break! Exiting execution. Did you manually close the trade?")
|
|
411
|
+
break
|
|
412
|
+
|
|
413
|
+
else:
|
|
414
|
+
trailing_candles = await exchange.fetch_ohlcv(symbol=param['ticker'], timeframe='1h', limit=24) # type: ignore
|
|
415
|
+
trailing_candles = trailing_candles[-param['reversal_num_intervals']:]
|
|
416
|
+
|
|
417
|
+
reversal = _reversal(
|
|
418
|
+
direction='up' if param['side']=='buy' else 'down',
|
|
419
|
+
last_candles=trailing_candles
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
ob = await exchange.fetch_order_book(symbol=param['ticker'], limit=10)
|
|
423
|
+
best_ask = min([x[0] for x in ob['asks']])
|
|
424
|
+
best_bid = max([x[0] for x in ob['bids']])
|
|
425
|
+
mid = (best_ask+best_bid)/2
|
|
426
|
+
spread_bps = (best_ask/best_bid - 1) * 10000
|
|
427
|
+
|
|
428
|
+
if param['side']=='buy':
|
|
429
|
+
unrealized_pnl = (mid - entry_price) * param['amount_base_ccy']
|
|
430
|
+
else:
|
|
431
|
+
unrealized_pnl = (entry_price - mid) * param['amount_base_ccy']
|
|
432
|
+
unrealized_pnl_percent = unrealized_pnl / amount_filled_usdt * 100
|
|
433
|
+
|
|
434
|
+
if unrealized_pnl>max_unrealized_pnl:
|
|
435
|
+
max_unrealized_pnl = unrealized_pnl
|
|
436
|
+
max_unrealized_pnl_percent = max_unrealized_pnl / amount_filled_usdt * 100
|
|
437
|
+
executed_position['position']['max_unrealized_pnl'] = max_unrealized_pnl
|
|
438
|
+
|
|
439
|
+
with open(position_cacnle_file, 'w') as f:
|
|
440
|
+
json.dump(executed_position, f)
|
|
441
|
+
|
|
442
|
+
if (
|
|
443
|
+
(unrealized_pnl_percent>=param['tp_min_percent'] and unrealized_pnl<max_unrealized_pnl)
|
|
444
|
+
or loss_trailing>0 # once your trade pnl crosses tp_min_percent, trailing stops is (and will remain) active.
|
|
445
|
+
):
|
|
446
|
+
loss_trailing = (1 - unrealized_pnl/max_unrealized_pnl) * 100
|
|
447
|
+
|
|
448
|
+
'''
|
|
449
|
+
Have a look at this for a visual explaination how "Gradually tightened stops" works:
|
|
450
|
+
https://github.com/r0bbar/siglab/blob/master/siglab_py/tests/manual/trading_util_tests.ipynb
|
|
451
|
+
'''
|
|
452
|
+
_effective_tp_trailing_percent = calc_eff_trailing_sl(
|
|
453
|
+
tp_min_percent = param['tp_min_percent'],
|
|
454
|
+
tp_max_percent = param['tp_max_percent'],
|
|
455
|
+
sl_percent_trailing = param['sl_percent_trailing'],
|
|
456
|
+
pnl_percent_notional = max_unrealized_pnl_percent, # Note: Use [max]_unrealized_pnl_percent, not unrealized_pnl_percent!
|
|
457
|
+
default_effective_tp_trailing_percent = param['default_effective_tp_trailing_percent'],
|
|
458
|
+
linear=True if param['tp_max_percent'] >= param['trailing_sl_min_percent_linear'] else False, # If tp_max_percent far (>100bps for example), there's more uncertainty if target can be reached: Go with linear.
|
|
459
|
+
pow=param['non_linear_pow']
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Once pnl pass tp_min_percent, trailing stops will be activated. Even if pnl fall back below tp_min_percent.
|
|
463
|
+
effective_tp_trailing_percent = min(effective_tp_trailing_percent, _effective_tp_trailing_percent)
|
|
464
|
+
|
|
465
|
+
log(f"unrealized_pnl: {round(unrealized_pnl,4)}, unrealized_pnl_percent: {round(unrealized_pnl_percent,4)}, max_unrealized_pnl_percent: {round(max_unrealized_pnl_percent,4)}, loss_trailing: {loss_trailing}, effective_tp_trailing_percent: {effective_tp_trailing_percent}, reversal: {reversal}")
|
|
466
|
+
|
|
467
|
+
# STEP 2. Exit
|
|
468
|
+
if unrealized_pnl>0:
|
|
469
|
+
if reversal and unrealized_pnl_percent >= param['tp_min_percent']:
|
|
470
|
+
tp = True
|
|
471
|
+
elif loss_trailing>=effective_tp_trailing_percent:
|
|
472
|
+
tp = True
|
|
473
|
+
else:
|
|
474
|
+
if abs(unrealized_pnl_percent)>=param['sl_percent']:
|
|
475
|
+
sl = True
|
|
476
|
+
|
|
477
|
+
if tp or sl:
|
|
478
|
+
exit_positions : List[DivisiblePosition] = [
|
|
479
|
+
DivisiblePosition(
|
|
480
|
+
ticker = param['ticker'],
|
|
481
|
+
side = 'sell' if param['side']=='buy' else 'buy',
|
|
482
|
+
amount = param['amount_base_ccy'],
|
|
483
|
+
leg_room_bps = param['leg_room_bps'],
|
|
484
|
+
order_type = param['order_type'],
|
|
485
|
+
slices = param['slices'],
|
|
486
|
+
wait_fill_threshold_ms = param['wait_fill_threshold_ms'],
|
|
487
|
+
|
|
488
|
+
reduce_only=True
|
|
489
|
+
)
|
|
490
|
+
]
|
|
491
|
+
executed_positions : Union[Dict[JSON_SERIALIZABLE_TYPES, JSON_SERIALIZABLE_TYPES], None] = execute_positions(
|
|
492
|
+
redis_client=redis_client,
|
|
493
|
+
positions=exit_positions,
|
|
494
|
+
ordergateway_pending_orders_topic=ordergateway_pending_orders_topic,
|
|
495
|
+
ordergateway_executions_topic=ordergateway_executions_topic
|
|
496
|
+
)
|
|
497
|
+
if executed_positions:
|
|
498
|
+
executed_position_close = executed_positions[0] # We sent only one DivisiblePosition.
|
|
499
|
+
log(f"Position closing. {json.dumps(executed_position, indent=4)}") # type: ignore
|
|
500
|
+
if executed_position_close['done']:
|
|
501
|
+
executed_position_close['position'] = executed_position['position']
|
|
502
|
+
|
|
503
|
+
if param['side']=='buy':
|
|
504
|
+
closed_pnl = (executed_position_close['average_cost'] - entry_price) * param['amount_base_ccy']
|
|
505
|
+
else:
|
|
506
|
+
closed_pnl = (entry_price - executed_position_close['average_cost']) * param['amount_base_ccy']
|
|
507
|
+
executed_position_close['position']['max_unrealized_pnl'] = max_unrealized_pnl
|
|
508
|
+
executed_position_close['position']['closed_pnl'] = closed_pnl
|
|
509
|
+
executed_position_close['position']['status'] = 'closed'
|
|
510
|
+
|
|
511
|
+
with open(position_cacnle_file, 'w') as f:
|
|
512
|
+
json.dump(executed_position_close, f)
|
|
513
|
+
|
|
514
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Execution done, {'TP' if tp else 'SL'} succeeded. closed_pnl: {closed_pnl}", message=executed_position_close, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
|
|
515
|
+
|
|
516
|
+
else:
|
|
517
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Exit execution failed. {param['ticker']}", message=executed_position_close, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
|
|
518
|
+
|
|
519
|
+
await exchange.close()
|
|
520
|
+
|
|
521
|
+
except Exception as loop_err:
|
|
522
|
+
log(f"Error: {loop_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}", log_level=LogLevel.ERROR)
|
|
523
|
+
finally:
|
|
524
|
+
time.sleep(int(param['loop_freq_ms']/1000))
|
|
525
|
+
|
|
526
|
+
else:
|
|
527
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Entry execution failed. {param['ticker']} {param['side']} {param['amount_base_ccy']}", message=executed_position, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.ERROR, logger=logger)
|
|
528
|
+
|
|
529
|
+
asyncio.run(main())
|
|
File without changes
|