-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5a2ba95
commit c4c3059
Showing
79 changed files
with
5,629,067 additions
and
120 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# ====================================================================================================================== | ||
|
||
# API Key | ||
API_KEY=... | ||
|
||
# If true, fixtures are recorded on disk, and the existing fixtures are replaced. | ||
RECORD_API_CALLS=false | ||
|
||
# ====================================================================================================================== |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Environment file | ||
/.env | ||
|
||
# IDEs | ||
/.idea/ | ||
/.bak/ | ||
|
||
# Python | ||
/amberdata_derivatives/__pycache__/ |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,47 @@ | ||
# amberdata-derivatives | ||
# Amberdata Derivatives SDK | ||
|
||
amberdata-derivatives is a Python library to access the [Amberdata Derivatives API](https://docs.amberdata.io/reference/information). | ||
amberdata-derivatives is a Python library to access the [Amberdata Derivatives API](https://docs.amberdata.io/reference/derivatives-information). | ||
|
||
--- | ||
|
||
**Documentation**: https://docs.amberdata.io/reference/information | ||
**Documentation**: https://docs.amberdata.io/reference/derivatives-information | ||
|
||
--- | ||
|
||
## Install | ||
## Installation | ||
|
||
```bash | ||
pip install git+https://github.com/amberdata/amberdata-derivatives-sdk.git | ||
``` | ||
|
||
## Demo | ||
## Integration | ||
|
||
```python | ||
from amberdata_derivatives import AmberdataDerivatives | ||
|
||
amberdata_client = AmberdataDerivatives(api_key="ENTER YOUR AD API KEY HERE") | ||
amberdata_client.get_term_structure(currency='BTC', exchange='deribit') | ||
amberdata_client.get_term_structure_constant(currency='BTC', exchange='deribit') | ||
``` | ||
|
||
Rather than hardcoding the API key, it can be stored in an environment file and loaded dynamically at runtime. | ||
|
||
```bash | ||
$ cat .env | ||
API_KEY=<Enter your API key here> | ||
``` | ||
|
||
```python | ||
from amberdata_derivatives import AmberdataDerivatives | ||
|
||
from dotenv import load_dotenv | ||
load_dotenv() | ||
|
||
amberdata_client = AmberdataDerivatives(api_key=os.getenv("API_KEY")) | ||
amberdata_client.get_term_structure_constant(currency='BTC', exchange='deribit') | ||
``` | ||
|
||
## Unit tests | ||
|
||
```python | ||
python3 -m unittest -v tests/*.py | ||
``` |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
# ====================================================================================================================== | ||
|
||
from amberdata_derivatives import AmberdataDerivatives | ||
import inspect | ||
import json | ||
import jsonschema | ||
from dotenv import load_dotenv | ||
import os | ||
import pathlib | ||
import unittest | ||
|
||
load_dotenv() | ||
|
||
|
||
# ====================================================================================================================== | ||
|
||
class BaseTestCase(unittest.TestCase): | ||
def setUp(self): | ||
self.record_api_calls = os.getenv("RECORD_API_CALLS", "false") == "true" | ||
self.amberdata_client = AmberdataDerivatives(api_key=os.getenv("API_KEY")) | ||
self.fixtures_directory = "tests/fixtures" | ||
self.schemata_directory = "tests/schemata" | ||
pathlib.Path(self.fixtures_directory).mkdir(parents=True, exist_ok=True) | ||
pathlib.Path(self.schemata_directory).mkdir(parents=True, exist_ok=True) | ||
|
||
def load_schema(self, filename: str): | ||
with open(self.schemata_directory + "/" + filename, 'r') as f: | ||
schema = json.load(f) | ||
return schema | ||
|
||
def validate_response_data(self, response, file=None): | ||
self.__record_response_data(response) | ||
|
||
file = self.__ensure_fixture_file(self.fixtures_directory, inspect.stack()[1].function, file) | ||
|
||
with open(file, 'r') as f: | ||
expected = json.load(f) | ||
|
||
self.assertEqual(expected, response) | ||
|
||
def validate_response_schema(self, response, file=None, schema=None): | ||
if schema is None: | ||
file = self.__ensure_fixture_file(self.schemata_directory, inspect.stack()[1].function, file) | ||
|
||
with open(file, 'r') as f: | ||
schema = json.load(f) | ||
|
||
jsonschema.validate(response, schema) | ||
|
||
def validate_response_200(self, response, num_elements=None, min_elements=None, max_elements=None): | ||
payload = response['payload'] | ||
|
||
self.assertEqual("Successful request", response['description']) | ||
self.assertEqual(200, response['status']) | ||
self.assertEqual("OK", response['title']) | ||
self.assertEqual("2023-09-30", payload['metadata']['api-version']) | ||
|
||
if num_elements is not None: | ||
self.assertEqual(len(payload['data']), num_elements) | ||
|
||
if min_elements is not None: | ||
self.assertGreaterEqual(len(payload['data']), min_elements) | ||
|
||
if max_elements is not None: | ||
self.assertLessEqual(len(payload['data']), max_elements) | ||
|
||
def validate_response_400(self, response, message: str): | ||
description = "Request was invalid or cannot be served. See message for details" | ||
|
||
self.assertEqual(description, response['description']) | ||
self.assertEqual(400, response['status']) | ||
self.assertEqual("BAD REQUEST", response['title']) | ||
self.assertEqual(True, response['error']) | ||
self.assertEqual(message, response['message']) | ||
|
||
def validate_response_field(self, response, field_name: str, field_value): | ||
data = response['payload']['data'] | ||
|
||
for element in data: | ||
self.assertEqual(element[field_name], field_value) | ||
|
||
def validate_response_field_timestamp(self, response, field_name: str, isHr=False, isIso=False, isMilliseconds=False, isMinutely=False, isHourly=False, isDaily=False): | ||
data = response['payload']['data'] | ||
|
||
# Match timestamps with format: 2024-04-01 02:59:00 000 | ||
if isHr: | ||
regex = r'^[1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9] [0-9]{3}$' | ||
regex = r'^[1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:00 000$' if isMinutely else regex | ||
regex = r'^[1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:00:00 000$' if isHourly else regex | ||
regex = r'^[1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9] 00:00:00 000$' if isDaily else regex | ||
|
||
for element in data: | ||
self.assertRegex(element[field_name], regex) | ||
|
||
# Match timestamps with format: 2024-04-01T02:59:00.000Z | ||
if isIso: | ||
regex = r'^[1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$' | ||
regex = r'^[1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:00\.000Z$' if isMinutely else regex | ||
regex = r'^[1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:00:00\.000Z$' if isHourly else regex | ||
regex = r'^[1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9]T00:00:00\.000Z$' if isDaily else regex | ||
|
||
for element in data: | ||
self.assertRegex(element[field_name], regex) | ||
|
||
# Match timestamps expressed as milliseconds | ||
if isMilliseconds: | ||
for element in data: | ||
# Slow implementation requiring a conversion from int to string. | ||
# Milliseconds start with 1, followed by 12 digits | ||
# self.assertRegex(str(element[field_name]), r'^1[0-9]{12}$') | ||
|
||
# Better implementation comparing numbers. | ||
# 1293840000000 = 2011-01-01 00:00:00 | ||
# 1893456000000 = 2030-01-01 00:00:00 | ||
self.__assert_between(element[field_name], 1293840000000, 1893456000000) | ||
|
||
# ================================================================================================================== | ||
|
||
def __ensure_fixture_file(self, directory: str, filename: str, file): | ||
if file is None: | ||
path = directory + "/" + type(self).__name__ | ||
pathlib.Path(path).mkdir(parents=True, exist_ok=True) | ||
file = path + "/" + filename + ".json" | ||
return file | ||
|
||
@staticmethod | ||
def __assert_between(x, low, high): | ||
# Naive implementation | ||
# self.assertGreaterEqual(x, low) | ||
# self.assertLessEqual(x, high) | ||
|
||
# Implementation requiring only one call/comparison | ||
if not isinstance(x, int): | ||
raise AssertionError('%r is not an integer' % x) | ||
|
||
if not (low <= x <= high): | ||
raise AssertionError('%r not between %r and %r' % (x, low, high)) | ||
|
||
def __record_response_data(self, response, file=None): | ||
if not self.record_api_calls: | ||
return | ||
|
||
file = self.__ensure_fixture_file(self.fixtures_directory, inspect.stack()[2].function, file) | ||
|
||
with open(file, 'w') as f: | ||
json.dump(response, f, indent=2, sort_keys=True) | ||
|
||
# ====================================================================================================================== |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# ====================================================================================================================== | ||
|
||
from .base_test_case import BaseTestCase | ||
import unittest | ||
|
||
|
||
# ====================================================================================================================== | ||
|
||
class EndpointDeltaSurfaceConstantTestCase(BaseTestCase): | ||
def setUp(self): | ||
super().setUp() | ||
self.schema = self.load_schema('endpoint.delta_surface_constant.json') | ||
|
||
# ================================================================================================================== | ||
|
||
def test_default(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='BTC') | ||
self.validate_response_schema(response, schema=self.schema) | ||
self.validate_response_200(response, num_elements=10) | ||
self.validate_response_field(response, 'exchange', 'deribit') | ||
self.validate_response_field(response, 'currency', 'BTC') | ||
self.validate_response_field_timestamp(response, 'timestamp', isIso=True) # TODO: this should be 'isMilliseconds=True' | ||
|
||
def test_timestamp(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='BTC', startDate='2024-04-01T00:00:00', endDate='2024-04-02T00:00:00') | ||
self.validate_response_data(response) | ||
self.validate_response_schema(response, schema=self.schema) | ||
self.validate_response_200(response, num_elements=14400) | ||
self.validate_response_field(response, 'exchange', 'deribit') | ||
self.validate_response_field(response, 'currency', 'BTC') | ||
self.validate_response_field_timestamp(response, 'timestamp', isIso=True) # TODO: this should be 'isMilliseconds=True' | ||
|
||
def test_timestamp_timeFormat_hr(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='BTC', startDate='2024-04-01T00:00:00', endDate='2024-04-02T00:00:00', timeFormat='hr') | ||
self.validate_response_data(response) | ||
self.validate_response_schema(response, schema=self.schema) | ||
self.validate_response_200(response, num_elements=14400) | ||
self.validate_response_field(response, 'exchange', 'deribit') | ||
self.validate_response_field(response, 'currency', 'BTC') | ||
self.validate_response_field_timestamp(response, 'timestamp', isHr=True, isMinutely=True) | ||
|
||
def test_timestamp_timeFormat_iso(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='BTC', startDate='2024-04-01T00:00:00', endDate='2024-04-02T00:00:00', timeFormat='iso') | ||
self.validate_response_data(response) | ||
self.validate_response_schema(response, schema=self.schema) | ||
self.validate_response_200(response, num_elements=14400) | ||
self.validate_response_field(response, 'exchange', 'deribit') | ||
self.validate_response_field(response, 'currency', 'BTC') | ||
self.validate_response_field_timestamp(response, 'timestamp', isIso=True, isMinutely=True) | ||
|
||
def test_timestamp_timeInterval_days(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='BTC', startDate='2024-04-01T00:00:00', endDate='2024-04-02T00:00:00', timeFormat='hr', timeInterval='d') | ||
self.validate_response_data(response) | ||
self.validate_response_schema(response, schema=self.schema) | ||
self.validate_response_200(response, num_elements=10) | ||
self.validate_response_field(response, 'exchange', 'deribit') | ||
self.validate_response_field(response, 'currency', 'BTC') | ||
self.validate_response_field_timestamp(response, 'timestamp', isHr=True, isDaily=True) | ||
|
||
def test_timestamp_timeInterval_hours(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='BTC', startDate='2024-04-01T00:00:00', endDate='2024-04-02T00:00:00', timeFormat='hr', timeInterval='h') | ||
self.validate_response_data(response) | ||
self.validate_response_schema(response, schema=self.schema) | ||
self.validate_response_200(response, num_elements=240) | ||
self.validate_response_field(response, 'exchange', 'deribit') | ||
self.validate_response_field(response, 'currency', 'BTC') | ||
self.validate_response_field_timestamp(response, 'timestamp', isHr=True, isHourly=True) | ||
|
||
def test_timestamp_timeInterval_minutes(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='BTC', startDate='2024-04-01T00:00:00', endDate='2024-04-02T00:00:00', timeFormat='hr', timeInterval='m') | ||
self.validate_response_data(response) | ||
self.validate_response_schema(response, schema=self.schema) | ||
self.validate_response_200(response, num_elements=14400) | ||
self.validate_response_field(response, 'exchange', 'deribit') | ||
self.validate_response_field(response, 'currency', 'BTC') | ||
self.validate_response_field_timestamp(response, 'timestamp', isHr=True, isMinutely=True) | ||
|
||
# ================================================================================================================== | ||
|
||
def test_invalid_parameter(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='BTC', invalid='parameter') | ||
self.validate_response_data(response) | ||
self.validate_response_400(response, "Parameter 'invalid' is not supported.") | ||
|
||
def test_invalid_timestamp(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='BTC', startDate='<timestamp>') | ||
self.validate_response_data(response) | ||
self.validate_response_400(response, "Invalid timestamp value: '<timestamp>'.") | ||
|
||
def test_unknown_exchange(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='<exchange>', currency='BTC') | ||
self.validate_response_data(response) | ||
self.validate_response_200(response, num_elements=0) | ||
|
||
def test_unknown_currency(self): | ||
response = self.amberdata_client.get_delta_surfaces_constant(exchange='deribit', currency='<currency>') | ||
self.validate_response_data(response) | ||
self.validate_response_200(response, num_elements=0) | ||
|
||
|
||
# ====================================================================================================================== | ||
|
||
if __name__ == '__main__': | ||
unittest.main() | ||
|
||
# ====================================================================================================================== |
Oops, something went wrong.