siglab-py 0.2.9__py3-none-any.whl → 0.3.0__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.
Potentially problematic release.
This version of siglab-py might be problematic. Click here for more details.
- siglab_py/ordergateway/gateway.py +67 -23
- siglab_py/tests/integration/market_data_util_tests.py +1 -1
- siglab_py/tests/unit/analytic_util_tests.py +3 -1
- siglab_py/tests/unit/trading_util_tests.py +60 -0
- siglab_py/util/analytic_util.py +16 -0
- siglab_py/util/notification_util.py +15 -10
- siglab_py/util/slack_notification_util.py +5 -1
- siglab_py/util/trading_util.py +66 -0
- {siglab_py-0.2.9.dist-info → siglab_py-0.3.0.dist-info}/METADATA +1 -1
- {siglab_py-0.2.9.dist-info → siglab_py-0.3.0.dist-info}/RECORD +12 -10
- {siglab_py-0.2.9.dist-info → siglab_py-0.3.0.dist-info}/WHEEL +0 -0
- {siglab_py-0.2.9.dist-info → siglab_py-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -116,7 +116,7 @@ To debug from vscode, launch.json:
|
|
|
116
116
|
"order_type": "limit",
|
|
117
117
|
"leg_room_bps": 5,
|
|
118
118
|
"slices": 5,
|
|
119
|
-
"wait_fill_threshold_ms":
|
|
119
|
+
"wait_fill_threshold_ms": 5000,
|
|
120
120
|
"executions": {},
|
|
121
121
|
"filled_amount": 0,
|
|
122
122
|
"average_cost": 0
|
|
@@ -134,7 +134,7 @@ To debug from vscode, launch.json:
|
|
|
134
134
|
"order_type": "limit",
|
|
135
135
|
"leg_room_bps": 5,
|
|
136
136
|
"slices": 5,
|
|
137
|
-
"wait_fill_threshold_ms":
|
|
137
|
+
"wait_fill_threshold_ms": 5000,
|
|
138
138
|
"executions": {
|
|
139
139
|
"xxx": { <-- order id from exchange
|
|
140
140
|
"info": { <-- ccxt convention, raw response from exchanges under info tag
|
|
@@ -201,6 +201,7 @@ param : Dict = {
|
|
|
201
201
|
"default_fees_ccy" : None,
|
|
202
202
|
"loop_freq_ms" : 500, # reduce this if you need trade faster
|
|
203
203
|
"loops_random_delay_multiplier" : 1, # Add randomness to time between slices are sent off. Set to 1 if no random delay needed.
|
|
204
|
+
"wait_fill_threshold_ms" : 5000,
|
|
204
205
|
|
|
205
206
|
'current_filename' : current_filename,
|
|
206
207
|
|
|
@@ -288,6 +289,7 @@ def parse_args():
|
|
|
288
289
|
|
|
289
290
|
parser.add_argument("--default_fees_ccy", help="If you're trading crypto, CEX fees USDT, DEX fees USDC in many cases. Default None, in which case gateway won't aggregatge fees from executions for you.", default=None)
|
|
290
291
|
parser.add_argument("--loop_freq_ms", help="Loop delays. Reduce this if you want to trade faster.", default=500)
|
|
292
|
+
parser.add_argument("--wait_fill_threshold_ms", help="Wait for fills for how long?", default=5000)
|
|
291
293
|
|
|
292
294
|
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')
|
|
293
295
|
parser.add_argument("--aws_kms_key_id", help="AWS KMS key ID", default=None)
|
|
@@ -314,6 +316,7 @@ def parse_args():
|
|
|
314
316
|
param['rate_limit_ms'] = int(args.rate_limit_ms)
|
|
315
317
|
param['default_fees_ccy'] = args.default_fees_ccy
|
|
316
318
|
param['loop_freq_ms'] = int(args.loop_freq_ms)
|
|
319
|
+
param['wait_fill_threshold_ms'] = int(args.wait_fill_threshold_ms)
|
|
317
320
|
|
|
318
321
|
if args.encrypt_decrypt_with_aws_kms:
|
|
319
322
|
if args.encrypt_decrypt_with_aws_kms=='Y':
|
|
@@ -460,7 +463,7 @@ async def watch_orders_task(
|
|
|
460
463
|
except Exception as loop_err:
|
|
461
464
|
print(f"watch_orders_task error: {loop_err}")
|
|
462
465
|
|
|
463
|
-
await asyncio.sleep(
|
|
466
|
+
await asyncio.sleep(param['loop_freq_ms']/1000)
|
|
464
467
|
|
|
465
468
|
async def send_heartbeat(exchange):
|
|
466
469
|
|
|
@@ -494,10 +497,25 @@ async def execute_one_position(
|
|
|
494
497
|
multiplier = market['contractSize'] if 'contractSize' in market and market['contractSize'] else 1
|
|
495
498
|
position.multiplier = multiplier
|
|
496
499
|
|
|
500
|
+
log(f"{position.ticker} min_amount: {min_amount}, multiplier: {multiplier}")
|
|
501
|
+
|
|
497
502
|
slices : List[Order] = position.to_slices()
|
|
503
|
+
|
|
504
|
+
# Residual handling in last slice
|
|
505
|
+
last_slice = slices[-1]
|
|
506
|
+
last_slice_rounded_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, last_slice.amount/multiplier) # After divided by multiplier, rounded_slice_amount_in_base_ccy in number of contracts actually (Not in base ccy).
|
|
507
|
+
last_slice_rounded_amount_in_base_ccy = float(last_slice_rounded_amount_in_base_ccy) if last_slice_rounded_amount_in_base_ccy else 0
|
|
508
|
+
if last_slice_rounded_amount_in_base_ccy<=min_amount:
|
|
509
|
+
slices.pop()
|
|
510
|
+
slices[-1].amount += last_slice.amount
|
|
511
|
+
|
|
512
|
+
log(f"{position.ticker} Last slice residual smaller than min_amount. Amount is added to prev slice instead. last_slice_amount: {last_slice.amount/multiplier}, last_slice_rounded_amount: {last_slice_rounded_amount_in_base_ccy}")
|
|
513
|
+
|
|
498
514
|
i = 0
|
|
499
515
|
for slice in slices:
|
|
500
516
|
try:
|
|
517
|
+
log(f"{position.ticker} sending slice# {i}")
|
|
518
|
+
|
|
501
519
|
dt_now : datetime = datetime.now()
|
|
502
520
|
|
|
503
521
|
slice_amount_in_base_ccy : float = slice.amount
|
|
@@ -625,9 +643,9 @@ async def execute_one_position(
|
|
|
625
643
|
log(f"Order dispatched: {order_id}. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
|
|
626
644
|
|
|
627
645
|
if not order_status or order_status!='closed':
|
|
628
|
-
start_time = time.time()
|
|
629
646
|
wait_threshold_sec = position.wait_fill_threshold_ms / 1000
|
|
630
|
-
|
|
647
|
+
|
|
648
|
+
start_time = time.time()
|
|
631
649
|
elapsed_sec = time.time() - start_time
|
|
632
650
|
while elapsed_sec < wait_threshold_sec:
|
|
633
651
|
order_update = None
|
|
@@ -646,8 +664,11 @@ async def execute_one_position(
|
|
|
646
664
|
break
|
|
647
665
|
|
|
648
666
|
loops_random_delay_multiplier : int = random.randint(1, param['loops_random_delay_multiplier']) if param['loops_random_delay_multiplier']!=1 else 1
|
|
649
|
-
loop_freq_sec : int =
|
|
667
|
+
loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
|
|
650
668
|
await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
|
|
669
|
+
|
|
670
|
+
elapsed_sec = time.time() - start_time
|
|
671
|
+
log(f"{position.ticker} waiting for order update ... elapsed_sec: {elapsed_sec}")
|
|
651
672
|
|
|
652
673
|
|
|
653
674
|
# Cancel hung limit order, resend as market
|
|
@@ -659,15 +680,10 @@ async def execute_one_position(
|
|
|
659
680
|
remaining_amount = order_update['remaining']
|
|
660
681
|
order_update['multiplier'] = multiplier
|
|
661
682
|
|
|
662
|
-
log(f"Final order_update before cancel+resend: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
|
|
663
|
-
|
|
664
683
|
position.append_execution(order_id, order_update)
|
|
665
684
|
|
|
666
|
-
if order_status!='closed':
|
|
667
|
-
|
|
668
|
-
filled_amount = order_update['filled']
|
|
669
|
-
remaining_amount = order_update['remaining']
|
|
670
|
-
|
|
685
|
+
if order_status!='closed':
|
|
686
|
+
log(f"Final order_update before cancel+resend: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
|
|
671
687
|
await exchange.cancel_order(order_id, position.ticker) # type: ignore
|
|
672
688
|
position.get_execution(order_id)['status'] = 'canceled'
|
|
673
689
|
log(f"Canceled unfilled/partial filled order: {order_id}. Resending remaining_amount: {remaining_amount} as market order.", log_level=LogLevel.INFO)
|
|
@@ -688,7 +704,11 @@ async def execute_one_position(
|
|
|
688
704
|
executed_resent_order['multiplier'] = multiplier
|
|
689
705
|
position.append_execution(order_id, executed_resent_order)
|
|
690
706
|
|
|
691
|
-
|
|
707
|
+
wait_threshold_sec = position.wait_fill_threshold_ms / 1000
|
|
708
|
+
|
|
709
|
+
start_time = time.time()
|
|
710
|
+
elapsed_sec = time.time() - start_time
|
|
711
|
+
while (not order_status or order_status!='closed') and (elapsed_sec < wait_threshold_sec):
|
|
692
712
|
order_update = None
|
|
693
713
|
if order_id in executions:
|
|
694
714
|
order_update = executions[order_id]
|
|
@@ -699,11 +719,24 @@ async def execute_one_position(
|
|
|
699
719
|
filled_amount = order_update['filled']
|
|
700
720
|
remaining_amount = order_update['remaining']
|
|
701
721
|
|
|
702
|
-
|
|
722
|
+
elapsed_sec = time.time() - start_time
|
|
723
|
+
log(f"Waiting for resent market order to close {order_id} ... elapsed_sec: {elapsed_sec}")
|
|
724
|
+
|
|
725
|
+
loops_random_delay_multiplier : int = random.randint(1, param['loops_random_delay_multiplier']) if param['loops_random_delay_multiplier']!=1 else 1
|
|
726
|
+
loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
|
|
727
|
+
await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
|
|
703
728
|
|
|
704
|
-
|
|
729
|
+
if (not order_status or order_status!='closed'):
|
|
730
|
+
# If no update from websocket, do one last fetch via REST
|
|
731
|
+
order_update = await exchange.fetch_order(order_id, position.ticker) # type: ignore
|
|
732
|
+
order_status = order_update['status']
|
|
733
|
+
filled_amount = order_update['filled']
|
|
734
|
+
remaining_amount = order_update['remaining']
|
|
735
|
+
order_update['multiplier'] = multiplier
|
|
705
736
|
|
|
706
|
-
log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
|
|
737
|
+
log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount} {json.dumps(order_update, indent=4)}")
|
|
738
|
+
else:
|
|
739
|
+
log(f"{position.ticker} {order_id} status (From REST): {json.dumps(order_update, indent=4)}")
|
|
707
740
|
|
|
708
741
|
slice.dispatched_price = rounded_limit_price
|
|
709
742
|
slice.dispatched_amount = rounded_slice_amount_in_base_ccy
|
|
@@ -722,8 +755,10 @@ async def execute_one_position(
|
|
|
722
755
|
)
|
|
723
756
|
raise slice_err
|
|
724
757
|
finally:
|
|
758
|
+
log(f"{position.ticker} done slice# {i}")
|
|
725
759
|
i += 1
|
|
726
|
-
|
|
760
|
+
|
|
761
|
+
log(f"{position.ticker} patch_executions")
|
|
727
762
|
position.patch_executions()
|
|
728
763
|
|
|
729
764
|
log(f"Dispatched slices:")
|
|
@@ -749,13 +784,22 @@ async def execute_one_position(
|
|
|
749
784
|
log(f"Executions:")
|
|
750
785
|
log(f"{json.dumps(position.to_dict(), indent=4)}")
|
|
751
786
|
|
|
752
|
-
|
|
787
|
+
notification_summary = {
|
|
788
|
+
'ticker' : position.ticker,
|
|
789
|
+
'side' : position.side,
|
|
790
|
+
'num_executions' : len(position.get_executions()),
|
|
791
|
+
'filled_amount' : position.filled_amount,
|
|
792
|
+
'average_cost' : position.average_cost,
|
|
793
|
+
'pos' : position.pos,
|
|
794
|
+
'done' : position.done
|
|
795
|
+
}
|
|
796
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} execute_one_position done. {position.ticker} {position.side} {position.amount}", message=notification_summary, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
|
|
753
797
|
|
|
754
798
|
except Exception as position_execution_err:
|
|
755
799
|
err_msg = f"Execution failed: {position_execution_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}"
|
|
756
800
|
log(f"Execution failed: {err_msg}")
|
|
757
801
|
|
|
758
|
-
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} execute_one_position failed!!! {position.ticker} {position.side} {position.amount}", message=position.get_executions(), footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.ERROR) # type: ignore
|
|
802
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} execute_one_position failed!!! {position.ticker} {position.side} {position.amount}", message=position.get_executions(), footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.ERROR, logger=logger) # type: ignore
|
|
759
803
|
|
|
760
804
|
position.done = False
|
|
761
805
|
position.execution_err = err_msg
|
|
@@ -806,7 +850,7 @@ async def work(
|
|
|
806
850
|
reduce_only=order['reduce_only'],
|
|
807
851
|
fees_ccy=order['fees_ccy'] if 'fees_ccy' in order else param['default_fees_ccy'],
|
|
808
852
|
slices=order['slices'],
|
|
809
|
-
wait_fill_threshold_ms=order['wait_fill_threshold_ms']
|
|
853
|
+
wait_fill_threshold_ms=order['wait_fill_threshold_ms'] if order['wait_fill_threshold_ms']>0 else param['wait_fill_threshold_ms']
|
|
810
854
|
)
|
|
811
855
|
for order in orders
|
|
812
856
|
]
|
|
@@ -844,7 +888,7 @@ async def work(
|
|
|
844
888
|
except Exception as loop_error:
|
|
845
889
|
log(f"Error: {loop_error} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}")
|
|
846
890
|
finally:
|
|
847
|
-
await asyncio.sleep(
|
|
891
|
+
await asyncio.sleep(param['loop_freq_ms']/1000)
|
|
848
892
|
|
|
849
893
|
async def main():
|
|
850
894
|
parse_args()
|
|
@@ -898,7 +942,7 @@ async def main():
|
|
|
898
942
|
# Once exchange instantiated, try fetch_balance to confirm connectivity and test credentials.
|
|
899
943
|
balances = await exchange.fetch_balance() # type: ignore
|
|
900
944
|
log(f"{param['gateway_id']}: account balances {balances}")
|
|
901
|
-
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} started", message=balances, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL)
|
|
945
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} started", message=balances, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
|
|
902
946
|
|
|
903
947
|
await work(param=param, exchange=exchange, redis_client=redis_client, notification_params=notification_params)
|
|
904
948
|
|
|
@@ -100,7 +100,7 @@ class MarketDataUtilTests(unittest.TestCase):
|
|
|
100
100
|
'defaultType': 'swap' }
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
exchange : Exchange = okx(param)
|
|
103
|
+
exchange : Exchange = okx(param) # type: ignore
|
|
104
104
|
normalized_symbols = [ 'BTC/USDT:USDT' ]
|
|
105
105
|
pd_candles: Union[pd.DataFrame, None] = fetch_candles(
|
|
106
106
|
start_ts=start_date.timestamp(),
|
|
@@ -49,7 +49,9 @@ class AnalyticUtilTests(unittest.TestCase):
|
|
|
49
49
|
'week_of_month', 'apac_trading_hr', 'emea_trading_hr', 'amer_trading_hr',
|
|
50
50
|
'is_green', 'pct_change_close',
|
|
51
51
|
'sma_short_periods', 'sma_long_periods', 'ema_short_periods', 'ema_long_periods', 'ema_close',
|
|
52
|
-
'std', 'std_percent',
|
|
52
|
+
'std', 'std_percent',
|
|
53
|
+
'candle_height_percent', 'candle_height_percent_rounded',
|
|
54
|
+
'log_return', 'interval_historical_volatility',
|
|
53
55
|
'chop_against_ema',
|
|
54
56
|
'ema_volume_short_periods', 'ema_volume_long_periods',
|
|
55
57
|
'max_short_periods', 'max_long_periods', 'idmax_short_periods', 'idmax_long_periods', 'min_short_periods', 'min_long_periods', 'idmin_short_periods', 'idmin_long_periods',
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import Union
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from util.trading_util import *
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# @unittest.skip("Skip all integration tests.")
|
|
10
|
+
class TradingUtilTests(unittest.TestCase):
|
|
11
|
+
def test_timestamp_to_active_trading_regions_case1(self):
|
|
12
|
+
tp_min_percent : float = 1.5
|
|
13
|
+
tp_max_percent : float = 2.5
|
|
14
|
+
sl_percent_trailing : float = 50 # Trailing stop loss in percent
|
|
15
|
+
default_effective_sl_percent_trailing : float = 50
|
|
16
|
+
|
|
17
|
+
pnl_percent_notional : float = 0.5 # Trade's current pnl in percent.
|
|
18
|
+
|
|
19
|
+
effective_tp_trailing_percent = calc_eff_trailing_sl(
|
|
20
|
+
tp_min_percent = tp_min_percent,
|
|
21
|
+
tp_max_percent = tp_max_percent,
|
|
22
|
+
sl_percent_trailing = sl_percent_trailing,
|
|
23
|
+
pnl_percent_notional = pnl_percent_notional,
|
|
24
|
+
default_effective_sl_percent_trailing = default_effective_sl_percent_trailing
|
|
25
|
+
)
|
|
26
|
+
assert(effective_tp_trailing_percent==50) # Generous trailing SL when trading starting out and pnl small.
|
|
27
|
+
|
|
28
|
+
def test_timestamp_to_active_trading_regions_case2(self):
|
|
29
|
+
tp_min_percent : float = 1.5
|
|
30
|
+
tp_max_percent : float = 2.5
|
|
31
|
+
sl_percent_trailing : float = 50 # Trailing stop loss in percent
|
|
32
|
+
default_effective_sl_percent_trailing : float = 50
|
|
33
|
+
|
|
34
|
+
pnl_percent_notional : float = 2 # Trade's current pnl in percent.
|
|
35
|
+
|
|
36
|
+
effective_tp_trailing_percent = calc_eff_trailing_sl(
|
|
37
|
+
tp_min_percent = tp_min_percent,
|
|
38
|
+
tp_max_percent = tp_max_percent,
|
|
39
|
+
sl_percent_trailing = sl_percent_trailing,
|
|
40
|
+
pnl_percent_notional = pnl_percent_notional,
|
|
41
|
+
default_effective_sl_percent_trailing = default_effective_sl_percent_trailing
|
|
42
|
+
)
|
|
43
|
+
assert(effective_tp_trailing_percent==25) # Intermediate trailing SL
|
|
44
|
+
|
|
45
|
+
def test_timestamp_to_active_trading_regions_case3(self):
|
|
46
|
+
tp_min_percent : float = 1.5
|
|
47
|
+
tp_max_percent : float = 2.5
|
|
48
|
+
sl_percent_trailing : float = 50 # Trailing stop loss in percent
|
|
49
|
+
default_effective_sl_percent_trailing : float = 50
|
|
50
|
+
|
|
51
|
+
pnl_percent_notional : float = 2.5 # Trade's current pnl in percent.
|
|
52
|
+
|
|
53
|
+
effective_tp_trailing_percent = calc_eff_trailing_sl(
|
|
54
|
+
tp_min_percent = tp_min_percent,
|
|
55
|
+
tp_max_percent = tp_max_percent,
|
|
56
|
+
sl_percent_trailing = sl_percent_trailing,
|
|
57
|
+
pnl_percent_notional = pnl_percent_notional,
|
|
58
|
+
default_effective_sl_percent_trailing = default_effective_sl_percent_trailing
|
|
59
|
+
)
|
|
60
|
+
assert(effective_tp_trailing_percent==0) # Most tight trailing SL
|
siglab_py/util/analytic_util.py
CHANGED
|
@@ -101,9 +101,25 @@ def compute_candles_stats(
|
|
|
101
101
|
pd_candles['std'] = pd_candles['close'].rolling(window=sliding_window_how_many_candles).std()
|
|
102
102
|
|
|
103
103
|
pd_candles['std_percent'] = pd_candles['std'] / pd_candles['ema_close'] * 100
|
|
104
|
+
|
|
104
105
|
pd_candles['candle_height_percent'] = pd_candles['candle_height'] / pd_candles['ema_close'] * 100
|
|
105
106
|
pd_candles['candle_height_percent_rounded'] = pd_candles['candle_height_percent'].round().astype('Int64')
|
|
106
107
|
|
|
108
|
+
'''
|
|
109
|
+
To annualize volatility:
|
|
110
|
+
if candle_interval == '1m':
|
|
111
|
+
annualization_factor = np.sqrt(365 * 24 * 60) # 1-minute candles
|
|
112
|
+
elif candle_interval == '1h':
|
|
113
|
+
annualization_factor = np.sqrt(365 * 24) # 1-hour candles
|
|
114
|
+
elif candle_interval == '1d':
|
|
115
|
+
annualization_factor = np.sqrt(365) # 1-day candles
|
|
116
|
+
pd_candles['annualized_volatility'] = (
|
|
117
|
+
pd_candles['interval_historical_volatility'] * annualization_factor
|
|
118
|
+
)
|
|
119
|
+
'''
|
|
120
|
+
pd_candles['log_return'] = np.log(pd_candles['close'] / pd_candles['close'].shift(1))
|
|
121
|
+
pd_candles['interval_historical_volatility'] = pd_candles['log_return'].rolling(window=sliding_window_how_many_candles).std()
|
|
122
|
+
|
|
107
123
|
pd_candles['chop_against_ema'] = (
|
|
108
124
|
(~pd_candles['is_green'] & (pd_candles['close'] > pd_candles['ema_close'])) | # Case 1: Green candle and close > EMA
|
|
109
125
|
(pd_candles['is_green'] & (pd_candles['close'] < pd_candles['ema_close'])) # Case 2: Red candle and close < EMA
|
|
@@ -14,19 +14,24 @@ def dispatch_notification(
|
|
|
14
14
|
message : Union[str, Dict, pd.DataFrame],
|
|
15
15
|
footer : str,
|
|
16
16
|
params : Dict[str, Any],
|
|
17
|
-
log_level : LogLevel = LogLevel.INFO
|
|
17
|
+
log_level : LogLevel = LogLevel.INFO,
|
|
18
|
+
logger = None
|
|
18
19
|
):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
try:
|
|
21
|
+
if isinstance(message, Dict):
|
|
22
|
+
_message = json.dumps(message, indent=2, separators=(' ', ':'))
|
|
23
|
+
elif isinstance(message, pd.DataFrame):
|
|
24
|
+
_message = tabulate(message, headers='keys', tablefmt='orgtbl') # type: ignore
|
|
25
|
+
else:
|
|
26
|
+
_message = message
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
utc_time = datetime.now(timezone.utc)
|
|
29
|
+
footer = f"UTC {utc_time} {footer}"
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
slack_dispatch_notification(title, _message, footer, params, log_level)
|
|
32
|
+
except Exception as any_notification_error:
|
|
33
|
+
if logger:
|
|
34
|
+
logger.info(f"Failed to dispatch notification: {any_notification_error}")
|
|
30
35
|
|
|
31
36
|
if __name__ == '__main__':
|
|
32
37
|
params : Dict[str, Any] = {
|
|
@@ -13,10 +13,14 @@ def slack_dispatch_notification(
|
|
|
13
13
|
message : str,
|
|
14
14
|
footer : str,
|
|
15
15
|
params : Dict[str, Any],
|
|
16
|
-
log_level : LogLevel = LogLevel.INFO
|
|
16
|
+
log_level : LogLevel = LogLevel.INFO,
|
|
17
|
+
max_message_len : int = 1800
|
|
17
18
|
):
|
|
18
19
|
slack_params = params['slack']
|
|
19
20
|
|
|
21
|
+
# Slack slack ... https://stackoverflow.com/questions/60344831/slack-api-invalid-block
|
|
22
|
+
message = message[:max_message_len]
|
|
23
|
+
|
|
20
24
|
if log_level.value==LogLevel.INFO.value or log_level.value==LogLevel.DEBUG.value:
|
|
21
25
|
webhook_url = slack_params['info']['webhook_url']
|
|
22
26
|
elif log_level.value==LogLevel.CRITICAL.value:
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
'''
|
|
3
|
+
pnl_percent_notional = Trade's current pnl in percent.
|
|
4
|
+
|
|
5
|
+
Examples,
|
|
6
|
+
y-axis:
|
|
7
|
+
max (i.e most tight) = 0%
|
|
8
|
+
sl_percent_trailing = 50% (Trailing stop loss in percent)
|
|
9
|
+
|
|
10
|
+
x-axis:
|
|
11
|
+
min TP = 1.5% <-- min TP
|
|
12
|
+
max TP = 2.5% <-- max TP
|
|
13
|
+
|
|
14
|
+
slope = (0-50)/(2.5-1.5) = -50
|
|
15
|
+
effective_tp_trailing_percent = slope * (pnl_percent_notional - 1.5%) + sl_percent_trailing
|
|
16
|
+
|
|
17
|
+
Case 1. pnl_percent_notional = 0.5% (Trade starting off, only +50bps pnl. i.e. min TP)
|
|
18
|
+
effective_tp_trailing_percent = slope * (pnl_percent_notional - 0.5%) + sl_percent_trailing
|
|
19
|
+
= -50 * (1.5-1.5) + 50%
|
|
20
|
+
= 0 + 50
|
|
21
|
+
= 50% (Most loose)
|
|
22
|
+
|
|
23
|
+
Case 2. pnl_percent_notional = 2% (Deeper into profit, +200bps pnl)
|
|
24
|
+
effective_tp_trailing_percent = slope * (pnl_percent_notional - 1.5%) + sl_percent_trailing
|
|
25
|
+
= -50 * (2-1.5) +50%
|
|
26
|
+
= -25 + 50
|
|
27
|
+
= 25% (Somewhat tight)
|
|
28
|
+
|
|
29
|
+
Case 3. pnl_percent_notional = 2.5% (Very deep in profit, +250bps pnl. i.e. max TP)
|
|
30
|
+
effective_tp_trailing_percent = slope * (pnl_percent_notional - 1.5%) + sl_percent_trailing
|
|
31
|
+
= -50 * (2.5-1.5) +50%
|
|
32
|
+
= -50 + 50
|
|
33
|
+
= 0 (Most tight)
|
|
34
|
+
|
|
35
|
+
So you see, effective_tp_trailing_percent gets smaller and smaller as pnl approach max TP, finally zero.
|
|
36
|
+
|
|
37
|
+
How to use it?
|
|
38
|
+
if loss_trailing>=effective_tp_trailing_percent and pnl_percent_notional > tp_min_percent:
|
|
39
|
+
Fire trailing stops and take profit.
|
|
40
|
+
|
|
41
|
+
What's 'loss_trailing'? 'loss_trailing' is essentially pnl drop from max_unrealized_pnl_live.
|
|
42
|
+
|
|
43
|
+
Say, when trade started off:
|
|
44
|
+
unrealized_pnl_live = $80
|
|
45
|
+
max_unrealized_pnl_live = $100
|
|
46
|
+
loss_trailing = (1 - unrealized_pnl_live/max_unrealized_pnl_live) = (1-80/100) = 0.2 (Or 20%)
|
|
47
|
+
|
|
48
|
+
If pnl worsen:
|
|
49
|
+
unrealized_pnl_live = $40
|
|
50
|
+
max_unrealized_pnl_live = $100
|
|
51
|
+
loss_trailing = (1 - unrealized_pnl_live/max_unrealized_pnl_live) = (1-40/100) = 0.6 (Or 60%)
|
|
52
|
+
'''
|
|
53
|
+
def calc_eff_trailing_sl(
|
|
54
|
+
tp_min_percent : float,
|
|
55
|
+
tp_max_percent : float,
|
|
56
|
+
sl_percent_trailing : float,
|
|
57
|
+
pnl_percent_notional : float,
|
|
58
|
+
default_effective_sl_percent_trailing : float = 50
|
|
59
|
+
) -> float:
|
|
60
|
+
slope = (0 - sl_percent_trailing) / (tp_max_percent - tp_min_percent)
|
|
61
|
+
effective_sl_percent_trailing = (
|
|
62
|
+
slope * (pnl_percent_notional - tp_min_percent) + sl_percent_trailing
|
|
63
|
+
if pnl_percent_notional>tp_min_percent
|
|
64
|
+
else default_effective_sl_percent_trailing
|
|
65
|
+
)
|
|
66
|
+
return effective_sl_percent_trailing
|
|
@@ -15,22 +15,24 @@ siglab_py/market_data_providers/test_provider.py,sha256=wBLCgcWjs7FGZJXWsNyn30lk
|
|
|
15
15
|
siglab_py/ordergateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
siglab_py/ordergateway/client.py,sha256=EwoVKxEcngIs8-b4MThPBdZfFIWJg1OFAKG9bwC5BYw,14826
|
|
17
17
|
siglab_py/ordergateway/encrypt_keys_util.py,sha256=-qi87db8To8Yf1WS1Q_Cp2Ya7ZqgWlRqSHfNXCM7wE4,1339
|
|
18
|
-
siglab_py/ordergateway/gateway.py,sha256=
|
|
18
|
+
siglab_py/ordergateway/gateway.py,sha256=mRakbgT1SPH1KPGw3_NdOQzbg_mMP2QbK4tSaLHiXWU,45892
|
|
19
19
|
siglab_py/ordergateway/test_ordergateway.py,sha256=4PE2flp_soGcD3DrI7zJOzZndjkb6I5XaDrFNNq4Huo,4009
|
|
20
20
|
siglab_py/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
siglab_py/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
siglab_py/tests/integration/market_data_util_tests.py,sha256=
|
|
22
|
+
siglab_py/tests/integration/market_data_util_tests.py,sha256=p-RWIJZLyj0lAdfi4QTIeAttCm_e8mEVWFKh4OWuogU,7189
|
|
23
23
|
siglab_py/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
-
siglab_py/tests/unit/analytic_util_tests.py,sha256=
|
|
24
|
+
siglab_py/tests/unit/analytic_util_tests.py,sha256=gkhapr6Xemq5lq-EjS3Cq-NzH_O2H6E3lFvGIFHsdMg,3778
|
|
25
25
|
siglab_py/tests/unit/market_data_util_tests.py,sha256=A1y83itISmMJdn6wLpfwcr4tGola8wTf1D1xbelMvgw,2026
|
|
26
|
+
siglab_py/tests/unit/trading_util_tests.py,sha256=T1IejsEVmGP2hU9F0RzgGITbNKih0jW44UVna0g1aYs,2689
|
|
26
27
|
siglab_py/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
-
siglab_py/util/analytic_util.py,sha256=
|
|
28
|
+
siglab_py/util/analytic_util.py,sha256=rFfZsL-qsqL_FgN7zPRL3rQtBe_9Kglh8Linjv-30VM,44600
|
|
28
29
|
siglab_py/util/aws_util.py,sha256=KGmjHrr1rpnnxr33nXHNzTul4tvyyxl9p6gpwNv0Ygc,2557
|
|
29
30
|
siglab_py/util/market_data_util.py,sha256=9Uze8DE5z90H4Qm15R55ZllAi5trUkwCAW-BWYbfaW8,19420
|
|
30
|
-
siglab_py/util/notification_util.py,sha256=
|
|
31
|
+
siglab_py/util/notification_util.py,sha256=vySgHjpHgwFDLW0tHSi_AGh9JBbPc25IUgvWxmjAeT8,2658
|
|
31
32
|
siglab_py/util/retry_util.py,sha256=mxYuRFZRZoaQQjENcwPmxhxixtd1TFvbxIdPx4RwfRc,743
|
|
32
|
-
siglab_py/util/slack_notification_util.py,sha256=
|
|
33
|
-
siglab_py
|
|
34
|
-
siglab_py-0.
|
|
35
|
-
siglab_py-0.
|
|
36
|
-
siglab_py-0.
|
|
33
|
+
siglab_py/util/slack_notification_util.py,sha256=G27n-adbT3Q6oaHSMvu_Nom794rrda5PprSF-zvmzkM,1912
|
|
34
|
+
siglab_py/util/trading_util.py,sha256=VyxcGkKg1kzR1F_ACNyBeYPKKmVYjWrKRYrfxyGH2og,3038
|
|
35
|
+
siglab_py-0.3.0.dist-info/METADATA,sha256=lCAwa_JFHMWiNYNFd1XKc94BMPgzw8xFTK3K70vJ7vQ,979
|
|
36
|
+
siglab_py-0.3.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
37
|
+
siglab_py-0.3.0.dist-info/top_level.txt,sha256=AbD4VR9OqmMOGlMJLkAVPGQMtUPIQv0t1BF5xmcLJSk,10
|
|
38
|
+
siglab_py-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|