Last active
June 17, 2024 03:48
-
-
Save normanlmfung/187cbd0465cc4e585f850d15e5129731 to your computer and use it in GitHub Desktop.
okx_economic_calendar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import time | |
import datetime | |
from datetime import datetime, timedelta | |
import enum | |
from typing import List, Dict | |
import os.path | |
import pandas as pd | |
from tabulate import tabulate | |
from ccxt import okx as ccxt_okx | |
from ccxt.base.exchange import Exchange | |
from mds.ccxt.exchange_base import ExchangeBase | |
EconomicCalendarEventPosNeg : enum = enum.Enum('PosNeg', 'UNDEFINED SMALLER_BETTER BIGGER_BETTER') | |
def _fix_column_types(pd_economic_calendar : pd.DataFrame): | |
pd_economic_calendar['datetime'] = pd.to_datetime(pd_economic_calendar['datetime']) | |
# This is to make it easy to do grouping with Excel pivot table | |
pd_economic_calendar['year'] = pd_economic_calendar['datetime'].dt.year | |
pd_economic_calendar['month'] = pd_economic_calendar['datetime'].dt.month | |
pd_economic_calendar['day'] = pd_economic_calendar['datetime'].dt.day | |
pd_economic_calendar['hour'] = pd_economic_calendar['datetime'].dt.hour | |
pd_economic_calendar['minute'] = pd_economic_calendar['datetime'].dt.minute | |
class okx(ccxt_okx, ExchangeBase): | |
def fetch_economic_calendar( | |
self, | |
regions : List[str] = None, | |
importance : str = None, | |
start_ts : int = -1, # in ms | |
end_ts : int = -1, # in ms | |
limit : int = 100, | |
region_map : Dict[str, str] = None, | |
event_name_map : Dict[str, str] = None, | |
event_posneg_map : Dict = None, | |
rate_limit_winodw_sec : float = 5 # hit max once per five seconds! | |
) -> pd.DataFrame: | |
all_economic_calendars : List[Dict] = [] | |
need_fetch_using_sliding_window : bool = False | |
for region in regions: | |
economic_calendars = self._fetch_economic_calendar( | |
region=region, importance=importance, start_ts=start_ts, end_ts=end_ts, limit=limit, | |
region_map=region_map, event_name_map=event_name_map, event_posneg_map=event_posneg_map, | |
rate_limit_winodw_sec=rate_limit_winodw_sec | |
) | |
if len(economic_calendars)<limit: | |
all_economic_calendars = all_economic_calendars + economic_calendars | |
else: | |
need_fetch_using_sliding_window = True | |
break | |
time.sleep(rate_limit_winodw_sec) | |
if not need_fetch_using_sliding_window: | |
pd_economic_calendars = pd.DataFrame(all_economic_calendars) | |
_fix_column_types(pd_economic_calendars) | |
return pd_economic_calendars | |
if end_ts==-1: | |
end_ts = (datetime.fromtimestamp(start_ts/1000) + timedelta(days=1)).timestamp() *1000 | |
end_ts = int(end_ts) | |
time.sleep(rate_limit_winodw_sec) | |
all_economic_calendars.clear() | |
for region in regions: | |
this_cutoff = start_ts | |
while this_cutoff<=end_ts: | |
economic_calendars = self._fetch_economic_calendar( | |
region=region, importance=importance, start_ts=this_cutoff, limit=limit, | |
region_map=region_map, event_name_map=event_name_map, event_posneg_map=event_posneg_map, | |
rate_limit_winodw_sec=rate_limit_winodw_sec | |
) | |
if economic_calendars: | |
all_economic_calendars = all_economic_calendars + economic_calendars | |
record_ts = max([int(economic_calendar['calendar_item_timestamp_ms']) for economic_calendar in economic_calendars]) | |
this_cutoff = record_ts + 1 | |
time.sleep(rate_limit_winodw_sec) | |
pd_economic_calendars = pd.DataFrame(all_economic_calendars) | |
_fix_column_types(pd_economic_calendars) | |
return pd_economic_calendars | |
def _fetch_economic_calendar( | |
self, | |
region : str = None, | |
importance : str = None, | |
start_ts : int = -1, # in ms | |
end_ts : int = -1, # in ms | |
limit : int = 100, | |
region_map : Dict[str, str] = None, | |
event_name_map : Dict[str, str] = None, | |
event_posneg_map : Dict = None, | |
rate_limit_winodw_sec : float = 5 # hit max once per five seconds! | |
) -> List[Dict]: | |
''' | |
Note, this API requires authentication even in doc it's marked 'public' | |
https://www.okx.com/docs-v5/en/?python#public-data-rest-api-get-economic-calendar-data | |
https://www.okx.com/docs-v5/en/?python#public-data-websocket-economic-calendar-channel | |
''' | |
params = {} | |
if region: | |
params['region'] = region | |
if importance: | |
if importance=='high': | |
importance = 3 | |
elif importance=='medium': | |
importance = 2 | |
elif importance=='low': | |
importance = 1 | |
else: | |
importance = -1 | |
if importance!=-1: | |
params['importance'] = importance | |
if start_ts!=-1: | |
params['before'] = str(start_ts) | |
if end_ts!=-1: | |
params['after'] = str(end_ts) | |
if limit!=100: | |
limit['limit'] = str(limit) | |
path = 'public/economic-calendar' | |
# Because this API requires authentication, you need specify api=private. Look at okx.py sign() method. If you pass api=public, sign wont add API key headers. | |
response = self.request(path = path, method='GET', api = 'private', params=params) | |
economic_calendars = response['data'] | |
for calendar_item in economic_calendars: | |
calendar_item_timestamp_ms = int(calendar_item['date']) | |
calendar_item_timestamp_sec = int(calendar_item_timestamp_ms/1000) | |
calendar_item['calendar_item_timestamp_ms'] = calendar_item_timestamp_ms | |
calendar_item['calendar_item_timestamp_sec'] = calendar_item_timestamp_sec | |
calendar_item['datetime'] = datetime.fromtimestamp(calendar_item_timestamp_sec) | |
calendar_item['source'] = self.name | |
if region: | |
calendar_item['region'] = region | |
else: | |
if region_map and calendar_item['region'] in region_map: | |
calendar_item['region'] = region_map[calendar_item['region']] | |
calendar_item['event_code'] = None | |
if event_name_map and calendar_item['event'] in event_name_map: | |
calendar_item['event_code'] = event_name_map[calendar_item['event']] | |
actual = calendar_item['actual'] | |
previous = calendar_item['previous'] | |
forecast = calendar_item['forecast'] | |
lower_case_words_to_be_removed = [ '$', 'cny' ] | |
# OKX source from https://www.tradingview.com/economic-calendar, look there to see what formatting logic you need. | |
if actual: | |
actual = actual.lower() | |
for word in lower_case_words_to_be_removed: | |
actual = actual.replace(word,'') | |
actual = actual.strip() | |
if '%' in actual: | |
actual = float(actual.replace('%',''))/100 | |
elif 'k'==actual[-1]: | |
actual = actual.replace('k','000') | |
actual = float(actual) | |
elif 'm'==actual[-1]: | |
actual = actual.replace('m','000000') | |
actual = float(actual) | |
elif 'b'==actual[-1]: | |
actual = actual.replace('b','000000000') | |
actual = float(actual) | |
else: | |
actual = float(actual) | |
else: | |
actual = None | |
if previous: | |
previous = previous.lower() | |
for word in lower_case_words_to_be_removed: | |
previous = previous.replace(word,'') | |
previous = previous.strip() | |
if '%' in previous: | |
previous = float(previous.replace('%',''))/100 | |
elif 'k'==previous[-1]: | |
previous = previous.replace('k','000') | |
previous = float(previous) | |
elif 'm'==previous[-1]: | |
previous = previous.replace('m','000000') | |
previous = float(previous) | |
elif 'b'==previous[-1]: | |
previous = previous.replace('b','000000000') | |
previous = float(previous) | |
else: | |
previous = float(previous) | |
else: | |
previous = None | |
if forecast: | |
forecast = forecast.lower() | |
for word in lower_case_words_to_be_removed: | |
forecast = forecast.replace(word,'') | |
forecast = forecast.strip() | |
if '%' in forecast: | |
forecast = float(forecast.replace('%',''))/100 | |
elif 'k'==forecast[-1]: | |
forecast = forecast.replace('k','000') | |
forecast = float(forecast) | |
elif 'm'==forecast[-1]: | |
forecast = forecast.replace('m','000000') | |
forecast = float(forecast) | |
elif 'b'==forecast[-1]: | |
forecast = forecast.replace('b','000000000') | |
forecast = float(forecast) | |
else: | |
forecast = float(forecast) | |
else: | |
forecast = None | |
calendar_item['actual'] = actual | |
calendar_item['previous'] = previous | |
calendar_item['forecast'] = forecast | |
calendar_item['pos_neg'] = None | |
if actual and calendar_item['event_code']: | |
calendar_item['pos_neg'] = okx.classify_economic_calendar(calendar_item) | |
calendar_item.pop('date') | |
economic_calendars.sort(key=lambda x : x['datetime'], reverse=False) | |
return economic_calendars | |
def classify_economic_calendar(calendar_item : Dict) -> str: | |
actual = calendar_item['actual'] | |
previous = calendar_item['previous'] | |
forecast = calendar_item['forecast'] | |
if actual and forecast and previous and calendar_item['event_code']: | |
event_pos_neg = event_posneg_map[calendar_item['event_code']] | |
if event_pos_neg == EconomicCalendarEventPosNeg.SMALLER_BETTER: # Example Core CPI | |
if actual<previous: | |
if actual<forecast: | |
return 'bullish' | |
elif actual==forecast: | |
return 'bullish' | |
else: | |
# actual>forecast | |
return 'bearish' | |
elif actual==previous: | |
if actual<forecast: | |
return 'bullish' | |
elif actual==forecast: | |
return 'neutral' | |
else: | |
# actual>forecast | |
return 'bearish' | |
else: | |
assert(actual>previous) | |
if actual<forecast: | |
return 'neutral' # neutral or bearish? Debatable | |
elif actual==forecast: | |
return 'bearish' | |
else: | |
# actual>forecast | |
return 'bearish' | |
elif event_pos_neg == EconomicCalendarEventPosNeg.BIGGER_BETTER: # Example GDP | |
if actual>previous: | |
if actual>forecast: | |
return 'bullish' | |
elif actual==forecast: | |
return 'bullish' | |
else: | |
# actual<forecast | |
return 'bearish' | |
elif actual==previous: | |
if actual>forecast: | |
return 'bullish' | |
elif actual==forecast: | |
return 'neutral' | |
else: | |
# actual<forecast | |
return 'bearish' | |
else: | |
assert(actual<previous) | |
if actual>forecast: | |
return 'neutral' # neutral or bearish? Debatable | |
elif actual==forecast: | |
return 'bearish' | |
else: | |
# actual>forecast | |
return 'bearish' | |
return "" | |
if __name__ == '__main__': | |
# Parameters | |
# Fetching economic calendars from OKX requires authentication. Also if your trading volume < usd 5mn (VIP1), regardless of your asset amount, you'd only get calendar items at most 3 months old. | |
apiKey : str = None | |
secret : str = None | |
passphrase : str = None | |
subaccount : str = None | |
# If start_ts older than three months and your account don't have that entitlement, start_ts will be set to today by OKX (i.e. forward looking only) | |
start_ts : int = int(datetime(2024,3,17).timestamp()*1000) | |
# start_ts : int = int(datetime.now().timestamp()*1000) | |
end_ts : int = int(datetime(2024,7,1).timestamp()*1000) | |
export_csv_file_name : str = "economic_calanedar.csv" | |
params = { | |
'apiKey' : apiKey, | |
'secret' : secret, | |
'password' : passphrase, # Other exchanges dont require this! This is saved in exchange.password! | |
'subaccount' : subaccount, | |
'rateLimit' : 100, # In ms | |
'options' : { | |
'defaultType': 'swap', # 'funding', 'spot', 'margin', 'future', 'swap', 'option' | |
} | |
} | |
exchange : Exchange = okx(params) | |
region_map = { | |
'United States' : 'united_states', | |
'China' : 'china' | |
} | |
event_name_map : Dict[str, str] = { | |
'Core Inflation Rate MoM' : 'core_inflation_rate_mom', | |
'Core Inflation Rate YoY' : 'core_inflation_rate_yoy', | |
'Inflation Rate MoM' : 'inflation_rate_mom', | |
'Inflation Rate YoY' : 'inflation_rate_yoy', | |
'Fed Interest Rate Decision' : 'fed_interest_rate_decision', | |
'Core PCE Price Index MoM' : 'core_pce_price_index_mom', | |
'Core PCE Price Index YoY' : 'core_pce_price_index_yoy', | |
'Unemployment Rate' : 'unemployment_rate', | |
'Non Farm Payrolls' : 'non_farm_payrolls', | |
'GDP Growth Rate QoQ Final' : 'gdp_growth_rate_qoq_final', | |
'GDP Growth Rate QoQ Adv' : 'gdp_growth_rate_qoq_adv', | |
# Careful with spelling here | |
'GDP Growth Rate QoQ 1st Est' : 'gdp_growth_rate_qoq_est', | |
'GDP Growth Rate QoQ 2nd Est' : 'gdp_growth_rate_qoq_est', | |
'GDP Growth Rate QoQ 3rd Est' : 'gdp_growth_rate_qoq_est', | |
'GDP Growth Rate QoQ 4th Est' : 'gdp_growth_rate_qoq_est' | |
} | |
event_posneg_map : Dict = { | |
'core_inflation_rate_mom' : EconomicCalendarEventPosNeg.SMALLER_BETTER, | |
'core_inflation_rate_yoy' : EconomicCalendarEventPosNeg.SMALLER_BETTER, | |
'inflation_rate_mom' : EconomicCalendarEventPosNeg.SMALLER_BETTER, | |
'inflation_rate_yoy' : EconomicCalendarEventPosNeg.SMALLER_BETTER, | |
'fed_interest_rate_decision' : EconomicCalendarEventPosNeg.SMALLER_BETTER, | |
'core_pce_price_index_mom' : EconomicCalendarEventPosNeg.SMALLER_BETTER, | |
'core_pce_price_index_yoy' : EconomicCalendarEventPosNeg.SMALLER_BETTER, | |
'unemployment_rate' : EconomicCalendarEventPosNeg.BIGGER_BETTER, # more people out of job better as FEDs in position to ease. | |
'non_farm_payrolls' : EconomicCalendarEventPosNeg.BIGGER_BETTER, # more people out of job better as FEDs in position to ease. | |
'gdp_growth_rate_qoq_final' : EconomicCalendarEventPosNeg.BIGGER_BETTER, | |
'gdp_growth_rate_qoq' : EconomicCalendarEventPosNeg.BIGGER_BETTER, | |
'gdp_growth_rate_qoq_adv' : EconomicCalendarEventPosNeg.BIGGER_BETTER, | |
'gdp_growth_rate_qoq_est' : EconomicCalendarEventPosNeg.BIGGER_BETTER | |
} | |
existing_csv : bool = os.path.isfile(export_csv_file_name) | |
if existing_csv: | |
pd_old_economic_calendars = pd.read_csv(export_csv_file_name) | |
pd_new_economic_calendars = exchange.fetch_economic_calendar( | |
regions=['united_states', 'china'], | |
importance='high', | |
start_ts=start_ts, end_ts=end_ts, | |
region_map=region_map, | |
event_name_map=event_name_map, | |
event_posneg_map=event_posneg_map, | |
rate_limit_winodw_sec=5 | |
) | |
min_calendar_item_timestamp_ms = pd_new_economic_calendars['calendar_item_timestamp_ms'].min() | |
if existing_csv: | |
pd_old_economic_calendars = pd_old_economic_calendars[pd_old_economic_calendars['calendar_item_timestamp_ms']<min_calendar_item_timestamp_ms] | |
pd_economic_calendars = pd.concat([pd_old_economic_calendars, pd_new_economic_calendars], axis=0) | |
else: | |
pd_economic_calendars = pd_new_economic_calendars | |
print(f"{tabulate(pd_economic_calendars, headers='keys', tablefmt='psql')}") | |
pd_economic_calendars.to_csv(export_csv_file_name) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment