Skip to content

Instantly share code, notes, and snippets.

@normanlmfung
Last active June 17, 2024 03:48
Show Gist options
  • Save normanlmfung/187cbd0465cc4e585f850d15e5129731 to your computer and use it in GitHub Desktop.
Save normanlmfung/187cbd0465cc4e585f850d15e5129731 to your computer and use it in GitHub Desktop.
okx_economic_calendar
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