13
13
import aioredis
14
14
import requests
15
15
from aioredis .exceptions import ResponseError
16
+ from fastapi import BackgroundTasks
16
17
from fastapi import Depends
17
18
from fastapi import FastAPI
18
19
from pydantic import BaseSettings
19
20
20
-
21
21
DEFAULT_KEY_PREFIX = 'is-bitcoin-lit'
22
22
SENTIMENT_API_URL = 'http://api.senticrypt.com/v1/bitcoin.json'
23
23
TWO_MINUTES = 60 * 60
@@ -49,25 +49,15 @@ def __init__(self, prefix: str = DEFAULT_KEY_PREFIX):
49
49
self .prefix = prefix
50
50
51
51
@prefixed_key
52
- def timeseries_30_second_sentiment_key (self ) -> str :
52
+ def timeseries_sentiment_key (self ) -> str :
53
53
"""A time series containing 30-second snapshots of BTC sentiment."""
54
54
return f'sentiment:mean:30s'
55
55
56
56
@prefixed_key
57
- def timeseries_1_hour_sentiment_key (self ) -> str :
58
- """A time series containing 1-hour snapshots of BTC sentiment."""
59
- return f'sentiment:mean:1h'
60
-
61
- @prefixed_key
62
- def timeseries_30_second_price_key (self ) -> str :
57
+ def timeseries_price_key (self ) -> str :
63
58
"""A time series containing 30-second snapshots of BTC price."""
64
59
return f'price:mean:30s'
65
60
66
- @prefixed_key
67
- def timeseries_1_hour_price_key (self ) -> str :
68
- """A time series containing 1-hour snapshots of BTC price."""
69
- return f'price:mean:1h'
70
-
71
61
@prefixed_key
72
62
def cache_key (self ) -> str :
73
63
return f'cache'
@@ -112,8 +102,8 @@ def make_keys():
112
102
113
103
114
104
async def persist (keys : Keys , data : BitcoinSentiments ):
115
- ts_sentiment_key = keys .timeseries_30_second_sentiment_key ()
116
- ts_price_key = keys .timeseries_30_second_price_key ()
105
+ ts_sentiment_key = keys .timeseries_sentiment_key ()
106
+ ts_price_key = keys .timeseries_price_key ()
117
107
await add_many_to_timeseries (
118
108
(
119
109
(ts_price_key , 'btc_price' ),
@@ -129,61 +119,35 @@ async def get_hourly_average(ts_key: str, top_of_the_hour: int):
129
119
)
130
120
# Return the average without the timestamp. The response is a list
131
121
# of the structure [timestamp, average].
132
- return response [0 ][1 ]
133
-
134
-
135
- async def get_current_hour_data (keys ):
136
- ts_sentiment_key = keys .timeseries_30_second_sentiment_key ()
137
- ts_price_key = keys .timeseries_30_second_price_key ()
138
- top_of_the_hour = int (
139
- datetime .utcnow ().replace (
140
- minute = 0 ,
141
- second = 0 ,
142
- microsecond = 0 ,
143
- ).timestamp () * 1000 ,
144
- )
145
- current_hour_avg_sentiment = await get_hourly_average (ts_sentiment_key , top_of_the_hour )
146
- current_hour_avg_price = await get_hourly_average (ts_price_key , top_of_the_hour )
122
+ return response
147
123
148
- return {
149
- 'time' : datetime .fromtimestamp (top_of_the_hour / 1000 , tz = timezone .utc ).isoformat (),
150
- 'price' : current_hour_avg_price ,
151
- 'sentiment' : current_hour_avg_sentiment ,
152
- }
124
+
125
+ def datetime_parser (dct ):
126
+ for k , v in dct .items ():
127
+ if isinstance (v , str ) and v .endswith ('+00:00' ):
128
+ try :
129
+ dct [k ] = datetime .datetime .fromisoformat (v )
130
+ except :
131
+ pass
132
+ return dct
153
133
154
134
155
- async def get_current_hour_cache (keys : Keys ):
135
+ async def get_cache (keys : Keys ):
156
136
current_hour_cache_key = keys .cache_key ()
157
137
current_hour_stats = await redis .get (current_hour_cache_key )
158
138
159
139
if current_hour_stats :
160
- return json .loads (current_hour_stats )
140
+ return json .loads (current_hour_stats , object_hook = datetime_parser )
161
141
162
142
163
- async def refresh_hourly_cache (keys : Keys ):
164
- current_hour_stats = await get_current_hour_data (keys )
143
+ async def set_cache (data , keys : Keys ):
144
+ def serialize_dates (v ): return v .isoformat (
145
+ ) if isinstance (v , datetime ) else v
165
146
await redis .set (
166
- keys .cache_key (), json .dumps (current_hour_stats ),
147
+ keys .cache_key (),
148
+ json .dumps (data , default = serialize_dates ),
167
149
ex = TWO_MINUTES ,
168
150
)
169
- return current_hour_stats
170
-
171
-
172
- async def set_current_hour_cache (keys : Keys ):
173
- # First, scrape the sentiment API and persist the data.
174
- data = requests .get (SENTIMENT_API_URL ).json ()
175
- await persist (keys , data )
176
-
177
- # Now that we've ingested raw sentiment data, aggregate it for the current
178
- # hour and cache the result.
179
- return await refresh_hourly_cache (keys )
180
-
181
-
182
- @app .get ('/refresh' )
183
- async def bitcoin (keys : Keys = Depends (make_keys )):
184
- data = requests .get (SENTIMENT_API_URL ).json ()
185
- await persist (keys , data )
186
- await refresh_hourly_cache (keys )
187
151
188
152
189
153
def get_direction (last_three_hours , key : str ):
@@ -195,25 +159,24 @@ def get_direction(last_three_hours, key: str):
195
159
return 'flat'
196
160
197
161
198
- @ app . get ( '/is-bitcoin-lit' )
199
- async def bitcoin ( keys : Keys = Depends ( make_keys )):
200
- now = datetime .utcnow ()
201
- sentiment_1h_key = keys . timeseries_1_hour_sentiment_key ()
202
- price_1h_key = keys . timeseries_1_hour_price_key ()
203
- current_hour_stats_cached = await get_current_hour_cache ( keys )
204
-
205
- if not current_hour_stats_cached :
206
- current_hour_stats_cached = await set_current_hour_cache ( keys )
207
-
208
- three_hours_ago_ms = int (( now - timedelta ( hours = 3 )). timestamp () * 1000 )
209
- sentiment = await redis . execute_command ( 'TS.RANGE' , sentiment_1h_key , three_hours_ago_ms , '+' )
210
- price = await redis . execute_command ( 'TS.RANGE' , price_1h_key , three_hours_ago_ms , '+' )
211
- past_hours = [{
162
+ def now ():
163
+ """Wrap call to utcnow, so that we can mock this function in tests."""
164
+ return datetime .utcnow ()
165
+
166
+
167
+ async def calculate_three_hours_of_data ( keys : Keys ) -> Dict [ str , str ]:
168
+ sentiment_key = keys . timeseries_sentiment_key ()
169
+ price_key = keys . timeseries_price_key ()
170
+ three_hours_ago_ms = int (( now () - timedelta ( hours = 3 )). timestamp () * 1000 )
171
+
172
+ sentiment = await get_hourly_average ( sentiment_key , three_hours_ago_ms )
173
+ price = await get_hourly_average ( price_key , three_hours_ago_ms )
174
+
175
+ last_three_hours = [{
212
176
'price' : data [0 ][1 ], 'sentiment' : data [1 ][1 ],
213
177
'time' : datetime .fromtimestamp (data [0 ][0 ] / 1000 , tz = timezone .utc ),
214
178
}
215
179
for data in zip (price , sentiment )]
216
- last_three_hours = past_hours + [current_hour_stats_cached ]
217
180
218
181
return {
219
182
'hourly_average_of_averages' : last_three_hours ,
@@ -222,6 +185,25 @@ async def bitcoin(keys: Keys = Depends(make_keys)):
222
185
}
223
186
224
187
188
+ @app .post ('/refresh' )
189
+ async def bitcoin (background_tasks : BackgroundTasks , keys : Keys = Depends (make_keys )):
190
+ data = requests .get (SENTIMENT_API_URL ).json ()
191
+ await persist (keys , data )
192
+ data = await calculate_three_hours_of_data (keys )
193
+ background_tasks .add_task (set_cache , data , keys )
194
+
195
+
196
+ @app .get ('/is-bitcoin-lit' )
197
+ async def bitcoin (background_tasks : BackgroundTasks , keys : Keys = Depends (make_keys )):
198
+ data = await get_cache (keys )
199
+
200
+ if not data :
201
+ data = await calculate_three_hours_of_data (keys )
202
+ background_tasks .add_task (set_cache , data , keys )
203
+
204
+ return data
205
+
206
+
225
207
async def make_timeseries (key ):
226
208
"""
227
209
Create a timeseries with the Redis key `key`.
@@ -243,36 +225,9 @@ async def make_timeseries(key):
243
225
log .info ('Could not create timeseries %s, error: %s' , key , e )
244
226
245
227
246
- async def make_rule (src : str , dest : str ):
247
- """
248
- Create a compaction rule from timeseries at `str` to `dest`.
249
-
250
- This rule aggregates metrics using 'avg' into hourly buckets.
251
- """
252
- try :
253
- await redis .execute_command (
254
- 'TS.CREATERULE' , src , dest , 'AGGREGATION' , 'avg' , HOURLY_BUCKET ,
255
- )
256
- except ResponseError as e :
257
- # Rule probably already exists.
258
- log .info (
259
- 'Could not create timeseries rule (from %s to %s), error: %s' , src , dest , e ,
260
- )
261
-
262
-
263
228
async def initialize_redis (keys : Keys ):
264
- ts_30_sec_sentiment = keys .timeseries_30_second_sentiment_key ()
265
- ts_1_hour_sentiment = keys .timeseries_1_hour_sentiment_key ()
266
- ts_30_sec_price = keys .timeseries_30_second_price_key ()
267
- ts_1_hour_price = keys .timeseries_1_hour_price_key ()
268
-
269
- await make_timeseries (ts_30_sec_sentiment )
270
- await make_timeseries (ts_1_hour_sentiment )
271
- await make_timeseries (ts_30_sec_price )
272
- await make_timeseries (ts_1_hour_price )
273
-
274
- await make_rule (ts_30_sec_sentiment , ts_1_hour_sentiment )
275
- await make_rule (ts_30_sec_price , ts_1_hour_price )
229
+ await make_timeseries (keys .timeseries_sentiment_key ())
230
+ await make_timeseries (keys .timeseries_price_key ())
276
231
277
232
278
233
@app .on_event ('startup' )
0 commit comments