def test_historical_prices_v3_num_points_happy(self): # fetch_historical_prices v3 - number of data points, weekly resolution with open('tests/data/historic_prices_num_points.json', 'r') as file: response_body = json.loads(file.read()) responses.add( responses.GET, 'https://demo-api.ig.com/gateway/deal/prices/MT.D.GC.Month2.IP', match_querystring=False, headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') ig_service.crud_session.HEADERS["LOGGED_IN"] = {} result = ig_service.fetch_historical_prices_by_epic( epic='MT.D.GC.Month2.IP', resolution='W', numpoints=10) prices = result['prices'] assert isinstance(result, dict) assert isinstance(prices, pd.DataFrame) # assert DataFrame shape assert prices.shape[0] == 10 assert prices.shape[1] == 13 # assert time series rows are 1 week apart prices['tvalue'] = prices.index prices['delta'] = (prices['tvalue'] - prices['tvalue'].shift()) assert any(prices["delta"].dropna() == datetime.timedelta(weeks=1))
def test_historical_prices_v3_num_points_bad_resolution(self): # fetch_historical_prices v3 - number of data points, invalid resolution with open('tests/data/historic_prices_num_points.json', 'r') as file: response_body = json.loads(file.read()) responses.add( responses.GET, 'https://demo-api.ig.com/gateway/deal/prices/MT.D.GC.Month2.IP', match_querystring=False, headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) with pytest.raises(ValueError) as excinfo: ig_service = IGService('username', 'password', 'api_key', 'DEMO') ig_service.crud_session.HEADERS["LOGGED_IN"] = {} ig_service.fetch_historical_prices_by_epic( epic='MT.D.GC.Month2.IP', resolution='X', numpoints=10, format=ig_service.flat_prices) assert "Invalid frequency" in str(excinfo.value)
def test_historical_prices_by_epic_and_date_range_happy(self): # fetch_historical_prices_by_epic_and_date_range, daily resolution with open('tests/data/historic_prices_v1.json', 'r') as file: response_body = json.loads(file.read()) responses.add( responses.GET, 'https://demo-api.ig.com/gateway/deal/prices/MT.D.GC.Month2.IP/DAY', match_querystring=False, headers={'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987'}, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') ig_service.crud_session.HEADERS["LOGGED_IN"] = {} result = ig_service.fetch_historical_prices_by_epic_and_date_range( epic='MT.D.GC.Month2.IP', resolution='D', start_date='2020:09:01-00:00:00', end_date='2020:09:04-23:59:59') prices = result['prices'] assert isinstance(result, dict) assert isinstance(prices, pd.DataFrame) # assert DataFrame shape assert prices.shape[0] == 4 assert prices.shape[1] == 13 # assert time series rows are 1 day apart prices['tvalue'] = prices.index prices['delta'] = (prices['tvalue'] - prices['tvalue'].shift()) assert any(prices["delta"].dropna() == datetime.timedelta(days=1))
def test_fetch_accounts_happy(self): with open('tests/data/accounts_balances.json', 'r') as file: response_body = json.loads(file.read()) responses.add(responses.GET, 'https://demo-api.ig.com/gateway/deal/accounts', headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') ig_service.crud_session.HEADERS["LOGGED_IN"] = {} result = ig_service.fetch_accounts() pd.set_option('display.max_columns', 13) print(result) assert isinstance(result, pd.DataFrame) assert result.iloc[0]['accountId'] == 'XYZ987' assert result.iloc[0]['balance'] == 1000.0 assert result.iloc[1]['accountId'] == 'ABC123' assert pd.isna(result.iloc[1]['balance']) assert pd.isna(result.iloc[1]['deposit'])
def test_historical_prices_v3_num_points_bad_numpoints(self): # fetch_historical_prices v3 - number of data points, invalid numpoints responses.add( responses.GET, 'https://demo-api.ig.com/gateway/deal/prices/MT.D.GC.Month2.IP', match_querystring=False, headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json={ 'errorCode': 'Unable to convert value=3.14159 to type= Integer int' }, # noqa status=400) with pytest.raises(Exception): ig_service = IGService('username', 'password', 'api_key', 'DEMO') ig_service.crud_session.HEADERS["LOGGED_IN"] = {} result = ig_service.fetch_historical_prices_by_epic( epic='MT.D.GC.Month2.IP', resolution='X', numpoints=3.14159, format=ig_service.flat_prices) assert result['errorCode'].startswith('Unable to convert value')
def test_switch_account(self): with open('tests/data/switch.json', 'r') as file: response_body = json.loads(file.read()) responses.add(responses.POST, 'https://demo-api.ig.com/gateway/deal/session', headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) responses.add(responses.PUT, 'https://demo-api.ig.com/gateway/deal/session', headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') ig_service.create_session() result = ig_service.switch_account(account_id='XYZ987', default_account=True) assert result['trailingStopsEnabled'] is True assert result['dealingEnabled'] is True
def test_session_details(self): with open('tests/data/session.json', 'r') as file: response_body = json.loads(file.read()) responses.add(responses.POST, 'https://demo-api.ig.com/gateway/deal/session', headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) responses.add(responses.GET, 'https://demo-api.ig.com/gateway/deal/session', headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') ig_service.create_session() result = ig_service.read_session() assert result['clientId'] == '100112233' assert result['accountId'] == 'ABC123' assert result['currency'] == 'GBP'
def test_activities_by_date(self): # fetch_account_activity_by_date with open('tests/data/activities_v1.json', 'r') as file: response_body = json.loads(file.read()) url = "https://demo-api.ig.com/gateway/deal/" date_pat = '[0-9]{2}-[0-9]{2}-[0-9]{4}' # NOT a very god regexp for dates will suffice here responses.add( responses.GET, re.compile(f"{url}history/activity/{date_pat}/{date_pat}"), match_querystring=False, headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') to_date = datetime.now() from_date = to_date - timedelta(days=7) result = ig_service.fetch_account_activity_by_date(from_date, to_date) # we expect a pd.DataFrame with 17 columns and 3 rows assert isinstance(result, pd.DataFrame) assert result.shape[0] == 3 assert result.shape[1] == 17
def test_login_v2_encrypted_happy(self): with open('tests/data/accounts.json', 'r') as file: response_body = json.loads(file.read()) responses.add( responses.GET, 'https://demo-api.ig.com/gateway/deal/session/encryptionKey', json={ 'encryptionKey': 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp9te7zwed8HhdRFsn47EI8exZ1Yi+bJoKtclGTiuaP1T+4AclNqB2mIya/Ik6IV6A2pt4FFVoqvrhJA46dWi4XgA4Ojhl2Xxw4++blAMgT3jU7N5nY13LdJzZuYv/oPZKRcEj6RrlBV68HjrTnjAMWARl0jFbVCiLWovTGJ0stx/zJAKX0GFyuUlsoaJISJJRYeOLUtZ8Z4BE6ZkmKnz4V8YNyyoWCyXQp+IKCZrfoEdlMOPBgsjbRy02Gh9xZqcm2erLsp40F+w3AjHUqQQi7eQuPQaPWq9Lhm8cVDH2CB2BtfM8Ew8T5/A36eqa5eoeQcZaMnLUQP5UYtG2Wd//wIDAQAB', # noqa 'timeStamp': '1601218928621' }, status=200) responses.add(responses.POST, 'https://demo-api.ig.com/gateway/deal/session', headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') result = ig_service.create_session(encryption=True, version='2') assert ig_service.crud_session.CLIENT_TOKEN == 'abc123' assert ig_service.crud_session.SECURITY_TOKEN == 'xyz987' assert result['accountType'] == 'SPREADBET' assert result['currentAccountId'] == 'ABC123' assert len(result['accounts']) == 2 assert result['accounts'][1]['accountName'] == 'Demo-cfd' assert result['accounts'][1]['accountType'] == 'CFD' assert result['trailingStopsEnabled'] is True
def test_historical_prices_v3_defaults_happy(self): # fetch_historical_prices v3 - default params with open('tests/data/historic_prices.json', 'r') as file: response_body = json.loads(file.read()) responses.add( responses.GET, 'https://demo-api.ig.com/gateway/deal/prices/MT.D.GC.Month2.IP', headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') ig_service.crud_session.HEADERS["LOGGED_IN"] = {} result = ig_service.fetch_historical_prices_by_epic( epic='MT.D.GC.Month2.IP') prices = result['prices'] assert isinstance(result, dict) assert isinstance(prices, pd.DataFrame) # with no other params, default returns 10 rows at MINUTE resolution assert prices.shape[0] == 10 assert prices.shape[1] == 13 # assert time series rows are 1 minute apart prices['tvalue'] = prices.index prices['delta'] = (prices['tvalue'] - prices['tvalue'].shift()) assert any(prices["delta"].dropna() == datetime.timedelta(minutes=1))
def test_login_v1_happy(self): with open('tests/data/accounts.json', 'r') as file: response_body = json.loads(file.read()) responses.add(responses.POST, 'https://demo-api.ig.com/gateway/deal/session', headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') result = ig_service.create_session() assert ig_service.crud_session.CLIENT_TOKEN == 'abc123' assert ig_service.crud_session.SECURITY_TOKEN == 'xyz987' assert result['accountType'] == 'SPREADBET' assert result['currentAccountId'] == 'ABC123' assert len(result['accounts']) == 2 assert result['accounts'][1]['accountName'] == 'Demo-cfd' assert result['accounts'][1]['accountType'] == 'CFD' assert result['trailingStopsEnabled'] is True
def test_workingorders_happy(self): with open('tests/data/workingorders.json', 'r') as file: response_body = json.loads(file.read()) responses.add(responses.GET, 'https://demo-api.ig.com/gateway/deal/workingorders', headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') ig_service.crud_session.HEADERS["LOGGED_IN"] = {} result = ig_service.fetch_working_orders() pd.set_option('display.max_columns', 50) print(result) assert isinstance(result, pd.DataFrame) assert result.iloc[0]['instrumentName'] == 'Spot Gold' assert result.iloc[0]['exchangeId'] == 'FX_C_GCSI_ST' assert result.iloc[0]['marketStatus'] == 'EDITS_ONLY' assert result.iloc[0]['level'] == 2000.0 assert result.iloc[0]['epic'] == 'CS.D.CFDGOLD.CFDGC.IP' assert result.iloc[0]['currencyCode'] == 'USD'
def test_activities_by_period(self): # test_activities_by_period with open('tests/data/activities_v1.json', 'r') as file: response_body = json.loads(file.read()) responses.add( responses.GET, re.compile( 'https://demo-api.ig.com/gateway/deal/history/activity/.+'), match_querystring=False, headers={ 'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987' }, json=response_body, status=200) ig_service = IGService('username', 'password', 'api_key', 'DEMO') result = ig_service.fetch_account_activity_by_period(10000000) # we expect a pd.DataFrame with 17 columns and 3 rows assert isinstance(result, pd.DataFrame) assert result.shape[0] == 3 assert result.shape[1] == 17
def test_create_open_position(self, ig_service: IGService): epic = 'IX.D.FTSE.DAILY.IP' market_info = ig_service.fetch_market_by_epic(epic) status = market_info.snapshot.marketStatus min_bet = market_info.dealingRules.minDealSize.value bid = market_info.snapshot.bid offer = market_info.snapshot.offer if status != 'TRADEABLE': pytest.skip('Skipping open position test, market not open') open_result = ig_service.create_open_position( epic=epic, direction='BUY', currency_code='GBP', order_type='MARKET', expiry='DFB', force_open='false', guaranteed_stop='false', size=min_bet, level=None, limit_level=None, limit_distance=None, quote_id=None, stop_distance=None, stop_level=None, trailing_stop=None, trailing_stop_increment=None) assert open_result['dealStatus'] == 'ACCEPTED' assert open_result['reason'] == 'SUCCESS' time.sleep(10) update_v1_result = ig_service.update_open_position(offer * 1.5, bid * 0.5, open_result['dealId'], version='1') assert update_v1_result['dealStatus'] == 'ACCEPTED' assert update_v1_result['reason'] == 'SUCCESS' time.sleep(10) update_v2_result = ig_service.update_open_position(offer * 1.4, bid * 0.4, open_result['dealId'], trailing_stop=True, trailing_stop_distance=25.0, trailing_stop_increment=10.0) assert update_v2_result['dealStatus'] == 'ACCEPTED' assert update_v2_result['reason'] == 'SUCCESS' time.sleep(10) close_result = ig_service.close_open_position(deal_id=open_result['dealId'], direction='SELL', epic=None, expiry='DFB', level=None, order_type='MARKET', quote_id=None, size=0.5, session=None) assert close_result['dealStatus'] == 'ACCEPTED' assert close_result['reason'] == 'SUCCESS'
def test_fetch_markets_by_epics(self, ig_service: IGService): markets_list = ig_service.fetch_markets_by_epics("IX.D.SPTRD.MONTH1.IP,IX.D.FTSE.MONTH1.IP", version='1') assert isinstance(markets_list, list) assert len(markets_list) == 2 assert markets_list[0].instrument.name == 'FTSE 100' assert markets_list[0].dealingRules is not None assert markets_list[1].instrument.name == 'US 500' markets_list = ig_service.fetch_markets_by_epics("MT.D.PL.Month2.IP,MT.D.PA.Month1.IP,MT.D.HG.Month1.IP", detailed=False) assert len(markets_list) == 3 assert markets_list[0].instrument.name == None assert markets_list[0].snapshot.bid != 0 assert markets_list[0].snapshot.offer != 0 assert markets_list[0].dealingRules is None assert markets_list[1].instrument.name == None assert markets_list[1].snapshot.bid != 0 assert markets_list[1].snapshot.offer != 0 assert markets_list[1].dealingRules is None assert markets_list[2].instrument.name == None assert markets_list[2].snapshot.bid != 0 assert markets_list[2].snapshot.offer != 0 assert markets_list[2].dealingRules is None
def test_create_session_v3(self, retrying): ig_service = IGService(config.username, config.password, config.api_key, config.acc_type, acc_number=config.acc_number, retryer=retrying) ig_service.create_session(version='3') assert 'X-IG-API-KEY' in ig_service.session.headers assert 'Authorization' in ig_service.session.headers assert 'IG-ACCOUNT-ID' in ig_service.session.headers assert len(ig_service.fetch_accounts()) == 2
def test_create_session_bad_api_key(self, retrying): ig_service = IGService(config.username, config.password, 'wrong', config.acc_type, retryer=retrying) with pytest.raises(IGException): ig_service.create_session()
def test_create_session_v3_no_acc_num(self, retrying): ig_service = IGService(config.username, config.password, config.api_key, config.acc_type, retryer=retrying) with pytest.raises(IGException): ig_service.create_session(version='3')
def test_create_session_encrypted_password(self, retrying): ig_service = IGService(config.username, config.password, config.api_key, config.acc_type, retryer=retrying) ig_service.create_session(encryption=True) assert 'CST' in ig_service.session.headers
def watchlist_id(ig_service: IGService): """test fixture creates a dummy watchlist for use in tests, and returns the ID. In teardown it also deletes the dummy watchlist""" epics = ['CS.D.GBPUSD.TODAY.IP', 'IX.D.FTSE.DAILY.IP'] now = datetime.now() data = ig_service.create_watchlist(f"test_{now.strftime('%Y%m%d%H%H%S')}", epics) watchlist_id = data['watchlistId'] yield watchlist_id ig_service.delete_watchlist(watchlist_id)
def ig_service(): """test fixture logs into IG with the configured credentials""" if config.acc_type == 'LIVE': pytest.fail( 'this integration test should not be executed with a LIVE account') ig_service = IGService(config.username, config.password, config.api_key, config.acc_type) ig_service.create_session() return ig_service
def test_retry(self): with open('tests/data/accounts_balances.json', 'r') as file: response_body = json.loads(file.read()) url = 'https://demo-api.ig.com/gateway/deal/accounts' headers = {'CST': 'abc123', 'X-SECURITY-TOKEN': 'xyz987'} api_exceeded = { 'errorCode': 'error.public-api.exceeded-api-key-allowance' } account_exceeded = { 'errorCode': 'error.public-api.exceeded-account-allowance' } trading_exceeded = { 'errorCode': 'error.public-api.exceeded-account-trading-allowance' } responses.add( Response(method='GET', url=url, headers={}, json=api_exceeded, status=403)) responses.add( Response(method='GET', url=url, headers={}, json=account_exceeded, status=403)) responses.add( Response(method='GET', url=url, headers={}, json=trading_exceeded, status=403)) responses.add( Response(method='GET', url=url, headers=headers, json=response_body, status=200)) ig_service = IGService( 'username', 'password', 'api_key', 'DEMO', retryer=Retrying( wait=tenacity.wait_exponential(), retry=tenacity.retry_if_exception_type(ApiExceededException))) result = ig_service.fetch_accounts() assert responses.assert_call_count(url, 4) is True assert isinstance(result, pd.DataFrame) assert result.iloc[0]['accountId'] == 'XYZ987'
def get_session(): ig_service = IGService( config.username, config.password, config.api_key, config.acc_type, acc_number=config.acc_number, retryer=DEFAULT_RETRY) ig_service.create_session(version='3') return ig_service
def ig_service(request, retrying): """test fixture logs into IG with the configured credentials. Tests both v2 and v3 types""" if config.acc_type == 'LIVE': pytest.fail('this integration test should not be executed with a LIVE account') ig_service = IGService(config.username, config.password, config.api_key, config.acc_type, acc_number=config.acc_number, retryer=retrying) service = ig_service.create_session(version=request.param) if request.param == '2' and service['accountType'] != 'SPREADBET': pytest.fail('Integration test currently only works with a spreadbet account') yield ig_service ig_service.logout()
def test_login_v2_encrypted_bad_key(): responses.add(responses.GET, 'https://demo-api.ig.com/gateway/deal/session/encryptionKey', json={'errorCode': 'error.security.api-key-invalid'}, status=403) ig_service = IGService('username', 'password', 'api_key', 'DEMO') with pytest.raises(IGException): result = ig_service.create_session(encryption=True) assert result['errorCode'] == 'error.security.api-key-invalid'
def test_create_working_order_guaranteed_stop_loss(self, ig_service: IGService): epic = 'CS.D.GBPUSD.TODAY.IP' market_info = ig_service.fetch_market_by_epic(epic) status = market_info.snapshot.marketStatus min_bet = market_info.dealingRules.minDealSize.value offer = market_info.snapshot.offer bid = market_info.snapshot.bid logging.info(f"min bet: {min_bet}") logging.info(f"offer: {offer}") logging.info(f"bid: {bid}") if status != 'TRADEABLE': pytest.skip('Skipping create working order test, market not open') create_result = ig_service.create_working_order( epic=epic, direction='BUY', currency_code='GBP', order_type='LIMIT', expiry='DFB', guaranteed_stop='true', time_in_force='GOOD_TILL_CANCELLED', size=min_bet, level=offer * 0.90, limit_level=None, limit_distance=None, stop_distance=200, stop_level=None) logging.info(f"result: {create_result['dealStatus']}, reason {create_result['reason']}") assert create_result['dealStatus'] == 'ACCEPTED' assert create_result['reason'] == 'SUCCESS' assert create_result['guaranteedStop'] time.sleep(10) update_result = ig_service.update_working_order( good_till_date=None, level=offer * 0.85, limit_distance=None, limit_level=None, stop_distance=None, stop_level=offer * 0.80, guaranteed_stop='true', time_in_force='GOOD_TILL_CANCELLED', order_type='LIMIT', deal_id=create_result['dealId']) logging.info(f"result: {update_result['dealStatus']}, reason {update_result['reason']}") assert update_result['dealStatus'] == 'ACCEPTED' assert update_result['reason'] == 'SUCCESS' assert update_result['guaranteedStop'] time.sleep(10) delete_result = ig_service.delete_working_order(create_result['dealId']) assert delete_result['dealStatus'] == 'ACCEPTED' assert delete_result['reason'] == 'SUCCESS'
def test_login_v1_bad_api_key(self): responses.add(responses.POST, 'https://demo-api.ig.com/gateway/deal/session', headers={}, json={'errorCode': 'error.security.api-key-invalid'}, status=403) ig_service = IGService('username', 'password', 'api_key', 'DEMO') with pytest.raises(Exception): result = ig_service.create_session(version='1') assert result['errorCode'] == 'error.security.api-key-invalid'
def test_read_session_fetch_session_tokens(self, ig_service: IGService): ig_service.read_session(fetch_session_tokens='true') assert 'X-IG-API-KEY' in ig_service.session.headers assert 'CST' in ig_service.session.headers assert 'X-SECURITY-TOKEN' in ig_service.session.headers if ig_service.session.headers['VERSION'] == '2': assert 'Authorization' not in ig_service.session.headers assert 'IG-ACCOUNT-ID' not in ig_service.session.headers if ig_service.session.headers['VERSION'] == '3': assert 'Authorization' in ig_service.session.headers assert 'IG-ACCOUNT-ID' in ig_service.session.headers
def test_login_v1_bad_credentials(): responses.add(responses.POST, 'https://demo-api.ig.com/gateway/deal/session', headers={'CST': 'abc123abc123abc123abc123abc123abc123'}, json={'errorCode': 'error.security.invalid-details'}, status=401) ig_service = IGService('username', 'password', 'api_key', 'DEMO') with pytest.raises(Exception): result = ig_service.create_session() assert result['errorCode'] == 'error.security.invalid-details'
def test_login_v2_bad_credentials(self): responses.add(responses.POST, 'https://demo-api.ig.com/gateway/deal/session', headers={}, json={'errorCode': 'error.security.invalid-details'}, status=401) ig_service = IGService('username', 'password', 'api_key', 'DEMO') with pytest.raises(Exception): result = ig_service.create_session(version='2') assert result['errorCode'] == 'error.security.invalid-details' assert ig_service.crud_session.CLIENT_TOKEN is None assert ig_service.crud_session.SECURITY_TOKEN is None