1
1
import asyncio
2
- from datetime import datetime
2
+ import bisect
3
+ import csv
4
+ import platform
5
+ import sys
6
+ from collections import defaultdict
7
+ from decimal import Decimal , InvalidOperation
8
+ from typing import Any
3
9
4
10
from pytr .utils import get_logger , preview
5
11
6
12
13
+ def alarms_dict_from_alarms_row (isin , alarms , max_values ) -> dict [str , Any ]:
14
+ alarmRow = {
15
+ "ISIN" : isin ,
16
+ }
17
+ for i in range (1 , max_values + 1 ):
18
+ alarmRow [f"alarm{ i } " ] = alarms [i - 1 ] if i <= len (alarms ) else None
19
+ return alarmRow
20
+
21
+
7
22
class Alarms :
8
- def __init__ (self , tr ):
23
+ def __init__ (self , tr , input = [], fp = None , remove_current_alarms = True ):
9
24
self .tr = tr
25
+ self .input = input
26
+ self .fp = fp
27
+ self .remove_current_alarms = remove_current_alarms
10
28
self .log = get_logger (__name__ )
29
+ self .data = {}
11
30
12
31
async def alarms_loop (self ):
13
32
recv = 0
14
33
await self .tr .price_alarm_overview ()
15
34
while True :
16
- _subscription_id , subscription , response = await self .tr .recv ()
35
+ _ , subscription , response = await self .tr .recv ()
17
36
18
37
if subscription ["type" ] == "priceAlarms" :
19
38
recv += 1
@@ -24,48 +43,143 @@ async def alarms_loop(self):
24
43
if recv == 1 :
25
44
return
26
45
27
- async def ticker_loop (self ):
28
- recv = 0
29
- await self . tr . price_alarm_overview ()
30
- while True :
31
- _subscription_id , subscription , response = await self .tr . recv ()
46
+ async def set_alarms (self ):
47
+ current_alarms = {}
48
+ new_alarms = {}
49
+ alarms_to_keep = {}
50
+ isins = self .data . keys ()
32
51
33
- if subscription ["type" ] == "priceAlarms" :
34
- recv += 1
35
- self .alarms = response
36
- else :
37
- print (f"unmatched subscription of type '{ subscription ['type' ]} ':\n { preview (response )} " )
52
+ if not isins :
53
+ print ("No instruments given to set alarms for" )
54
+ return
38
55
39
- if recv == 1 :
40
- return
56
+ for isin in isins :
57
+ current_alarms .setdefault (isin , {})
58
+ new_alarms .setdefault (isin , [])
59
+ alarms_to_keep .setdefault (isin , [])
60
+
61
+ for a in self .alarms :
62
+ if a ["instrumentId" ] in isins :
63
+ current_alarms [a ["instrumentId" ]][Decimal (a ["targetPrice" ])] = a ["id" ]
64
+
65
+ for isin in isins :
66
+ for a in self .data [isin ]:
67
+ if a in current_alarms [isin ]:
68
+ alarms_to_keep [isin ].append (a )
69
+ del current_alarms [isin ][a ]
70
+ else :
71
+ new_alarms [isin ].append (a )
72
+
73
+ if not self .remove_current_alarms :
74
+ current_alarms .clear ()
75
+
76
+ messages = []
77
+ if alarms_to_keep [isin ]:
78
+ messages .append (f"Keeping { ', ' .join (str (v ) for v in alarms_to_keep [isin ])} " )
79
+ if new_alarms [isin ]:
80
+ messages .append (f"Adding { ', ' .join (str (v ) for v in new_alarms [isin ])} " )
81
+ if current_alarms [isin ]:
82
+ messages .append (f"Removing { ', ' .join (str (v ) for v in sorted (current_alarms [isin ].keys ()))} " )
83
+ if not messages :
84
+ messages .append ("Nothing to do." )
85
+
86
+ print (f"{ isin } : { '; ' .join (messages )} " )
87
+
88
+ action_count = 0
89
+ for isin in isins :
90
+ for a in new_alarms [isin ]:
91
+ await self .tr .create_price_alarm (isin , float (a ))
92
+ action_count += 1
93
+
94
+ for a in current_alarms [isin ]:
95
+ await self .tr .cancel_price_alarm (current_alarms [isin ].get (a ))
96
+ action_count += 1
97
+
98
+ while action_count > 0 :
99
+ await self .tr .recv ()
100
+ action_count -= 1
101
+ return
41
102
42
103
def overview (self ):
43
- print ("ISIN status created target diff% createdAt triggeredAT" )
44
- self .log .debug (f"Processing { len (self .alarms )} alarms" )
45
-
46
- for a in self .alarms : # sorted(positions, key=lambda x: x['netValue'], reverse=True):
47
- self .log .debug (f" Processing { a } alarm" )
48
- ts = int (a ["createdAt" ]) / 1000.0
49
- target_price = float (a ["targetPrice" ])
50
- created_price = float (a ["createdPrice" ])
51
- created = datetime .fromtimestamp (ts ).isoformat (sep = " " , timespec = "minutes" )
52
- if a ["triggeredAt" ] is None :
53
- triggered = "-"
54
- else :
55
- ts = int (a ["triggeredAt" ]) / 1000.0
56
- triggered = datetime .fromtimestamp (ts ).isoformat (sep = " " , timespec = "minutes" )
104
+ alarms_per_ISIN = defaultdict (list )
105
+ isins = self .data .keys ()
106
+ for a in self .alarms :
107
+ if a ["status" ] != "active" :
108
+ continue
109
+ if isins and a ["instrumentId" ] not in isins :
110
+ continue
111
+ bisect .insort (alarms_per_ISIN [a ["instrumentId" ]], a ["targetPrice" ])
57
112
58
- if a ["createdPrice" ] == 0 :
59
- diffP = 0.0
60
- else :
61
- diffP = (target_price / created_price ) * 100 - 100
113
+ for isin in isins :
114
+ if isin not in alarms_per_ISIN :
115
+ alarms_per_ISIN [isin ] = []
62
116
63
- print (
64
- f"{ a ['instrumentId' ]} { a ['status' ]} { created_price :>7.2f} { target_price :>7.2f} "
65
- + f"{ diffP :>5.1f} % { created } { triggered } "
117
+ max_values = max (len (v ) for v in alarms_per_ISIN .values ())
118
+ if self .fp == sys .stdout :
119
+ print (f"ISIN { ' ' .join (f'Alarm{ i } ' for i in range (1 , max_values + 1 ))} " )
120
+ for isin , alarms in alarms_per_ISIN .items ():
121
+ print (f"{ isin } { ' ' .join (f'{ float (x ):>7.2f} ' for x in alarms )} " )
122
+ else :
123
+ print (f"Writing alarms to file { self .fp .name } ..." )
124
+ lineterminator = "\n " if platform .system () == "Windows" else "\r \n "
125
+ writer = csv .DictWriter (
126
+ self .fp ,
127
+ fieldnames = ["ISIN" ] + [f"alarm{ i } " for i in range (1 , max_values + 1 )],
128
+ delimiter = ";" ,
129
+ lineterminator = lineterminator ,
130
+ )
131
+ writer .writeheader ()
132
+ writer .writerows (
133
+ [alarms_dict_from_alarms_row (key , value , max_values ) for key , value in alarms_per_ISIN .items ()]
66
134
)
67
135
68
136
def get (self ):
137
+ cur_isin = None
138
+ for token in self .input :
139
+ if len (token ) == 12 and "." not in token :
140
+ cur_isin = token
141
+ self .data .setdefault (cur_isin , [])
142
+ else :
143
+ try :
144
+ cur_alarm = Decimal (token )
145
+ if cur_isin is not None :
146
+ bisect .insort (self .data [cur_isin ], cur_alarm )
147
+ except InvalidOperation :
148
+ raise ValueError (f"{ token } is no valid ISIN or decimal value that could represent an alarm." )
149
+
69
150
asyncio .get_event_loop ().run_until_complete (self .alarms_loop ())
70
151
71
152
self .overview ()
153
+
154
+ def set (self ):
155
+ if self .fp == sys .stdin :
156
+ cur_isin = None
157
+ for token in self .input :
158
+ if len (token ) == 12 and "." not in token :
159
+ cur_isin = token
160
+ self .data .setdefault (cur_isin , [])
161
+ else :
162
+ try :
163
+ cur_alarm = Decimal (token )
164
+ if cur_isin is not None :
165
+ bisect .insort (self .data [cur_isin ], cur_alarm )
166
+ except InvalidOperation :
167
+ raise ValueError (f"{ token } is no valid ISIN or decimal value that could represent an alarm." )
168
+ else :
169
+ lineterminator = "\n " if platform .system () == "Windows" else "\r \n "
170
+ reader = csv .DictReader (self .fp , delimiter = ";" , lineterminator = lineterminator )
171
+ fieldnames = reader .fieldnames
172
+ fieldnum = len (fieldnames )
173
+ for row in list (reader ):
174
+ isin = row [fieldnames [0 ]]
175
+ self .data .setdefault (isin , [])
176
+ for i in range (1 , fieldnum ):
177
+ value = row [fieldnames [i ]]
178
+ if value is not None and value != "" :
179
+ bisect .insort (self .data [isin ], Decimal (value .replace ("," , "" )))
180
+
181
+ # get current alarms
182
+ asyncio .get_event_loop ().run_until_complete (self .alarms_loop ())
183
+
184
+ # set/remove alarms
185
+ asyncio .get_event_loop ().run_until_complete (self .set_alarms ())
0 commit comments