Skip to content

Commit

Permalink
feat: Add search for markets (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode committed Dec 5, 2020
1 parent e76afb0 commit bac4839
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 10 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# IG Trading API

Unofficial [IG Trading API](https://labs.ig.com/rest-trading-api-guide) for Node.js, written in TypeScript.
![Language Details](https://img.shields.io/github/languages/top/bennycode/ig-trading-api) ![License](https://img.shields.io/npm/l/ig-trading-api.svg) ![Package Version](https://img.shields.io/npm/v/ig-trading-api.svg) ![Dependency Updates](https://img.shields.io/david/bennycode/ig-trading-api.svg)

Unofficial [IG Trading API](https://labs.ig.com/rest-trading-api-guide) for Node.js, written in TypeScript and covered by tests.

## Installation

Expand All @@ -18,18 +20,20 @@ yarn add ig-trading-api

## Setup

You can set the API gateway, when initializing the API client. Use `APIClient.URL_DEMO` (demo-api.ig.com) for demo accounts and `APIClient.URL_LIVE` (api.ig.com) for live account access.

**JavaScript / Node.js**

```javascript
const {APIClient} = require('ig-trading-api');
const client = new APIClient('your-api-key');
const client = new APIClient(APIClient.URL_LIVE, 'your-api-key');
```

**TypeScript**

```typescript
import {APIClient} from 'ig-trading-api';
const client = new APIClient('your-api-key');
const client = new APIClient(APIClient.URL_LIVE, 'your-api-key');
```

## Usage
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@
"release:major": "generate-changelog -M && yarn changelog:commit && yarn docs:release && npm version major",
"release:minor": "generate-changelog -m && yarn changelog:commit && yarn docs:release && npm version minor",
"release:patch": "generate-changelog -p && yarn changelog:commit && yarn docs:release && npm version patch",
"test": "exit 0 && nyc --nycrc-path=nyc.config.coverage.json jasmine --config=jasmine.json",
"test:dev": "exit 0 && nyc --nycrc-path=nyc.config.json jasmine --config=jasmine.json"
"test": "nyc --nycrc-path=nyc.config.coverage.json jasmine --config=jasmine.json",
"test:dev": "nyc --nycrc-path=nyc.config.json jasmine --config=jasmine.json"
},
"version": "0.0.1"
}
8 changes: 5 additions & 3 deletions src/APIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {RESTClient} from './client/RESTClient';
export class APIClient {
readonly rest: RESTClient;

constructor(apiKey: string) {
const url = 'https://api.ig.com/gateway/deal/';
this.rest = new RESTClient(url, apiKey);
static URL_DEMO = 'https://demo-api.ig.com/gateway/deal/';
static URL_LIVE = 'https://api.ig.com/gateway/deal/';

constructor(baseUrl: string, apiKey: string) {
this.rest = new RESTClient(baseUrl, apiKey);
}
}
48 changes: 48 additions & 0 deletions src/client/RESTClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {RESTClient} from '.';
import {APIClient} from '../APIClient';
import nock from 'nock';
import {LoginAPI} from '../login';
import {AxiosRequestConfig} from 'axios';

describe('RESTClient', () => {
function createRESTClient(): RESTClient {
return new RESTClient(APIClient.URL_DEMO, '');
}

describe('defaults', () => {
it('supports overriding the timeout limit', () => {
const rest = createRESTClient();
expect(rest.defaults.timeout).toBe(5000);
rest.defaults.timeout = 2500;
expect(rest.defaults.timeout).toBe(2500);
});
});

describe('interceptors', () => {
beforeAll(() => {
nock(APIClient.URL_DEMO)
.persist()
.defaultReplyHeaders({
CST: 'test',
'X-SECURITY-TOKEN': 'test',
})
.post(LoginAPI.URL.SESSION)
.query(true)
.reply(200, JSON.stringify({}));
});

it('supports custom HTTP interceptors', async () => {
const rest = createRESTClient();

const onRequest = jasmine.createSpy('onRequest').and.callFake((config: AxiosRequestConfig) => config);

const myInterceptor = rest.interceptors.request.use(onRequest);
await rest.login.createSession('test-user', 'test-password');
expect(onRequest).toHaveBeenCalledTimes(1);

rest.interceptors.request.eject(myInterceptor);
await rest.login.createSession('test-user', 'test-password');
expect(onRequest).toHaveBeenCalledTimes(1);
});
});
});
3 changes: 3 additions & 0 deletions src/client/RESTClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios, {AxiosInstance, AxiosInterceptorManager, AxiosRequestConfig, AxiosResponse} from 'axios';
import {LoginAPI} from '../login';
import {MarketAPI} from '../market';

export interface Authorization {
clientSessionToken?: string;
Expand All @@ -19,6 +20,7 @@ export class RESTClient {
}

readonly login: LoginAPI;
readonly market: MarketAPI;

private readonly httpClient: AxiosInstance;
private readonly auth: Authorization = {};
Expand Down Expand Up @@ -51,5 +53,6 @@ export class RESTClient {
});

this.login = new LoginAPI(this.httpClient, this.auth);
this.market = new MarketAPI(this.httpClient);
}
}
2 changes: 1 addition & 1 deletion src/demo/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {APIClient} from '../APIClient';

async function main(): Promise<void> {
const {IG_API_KEY: apiKey, IG_USERNAME: username, IG_PASSWORD: password} = process.env;
const client = new APIClient(`${apiKey}`);
const client = new APIClient(APIClient.URL_LIVE, `${apiKey}`);
const session = await client.rest.login.createSession(`${username}`, `${password}`);
console.info(`Your client ID is "${session.clientId}".`);
}
Expand Down
48 changes: 48 additions & 0 deletions src/login/LoginAPI.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import nock from 'nock';
import {APIClient} from '../APIClient';
import {LoginAPI} from './LoginAPI';

describe('LoginAPI', () => {
describe('createSession', () => {
it('creates a trading session', async () => {
nock(APIClient.URL_DEMO)
.post(LoginAPI.URL.SESSION)
.query(true)
.reply(
200,
JSON.stringify({
accountInfo: {
available: 0,
balance: 0,
deposit: 0,
profitLoss: 0,
},
accountType: 'CFD',
accounts: [
{
accountId: 'ABC123',
accountName: 'CFD',
accountType: 'CFD',
preferred: true,
},
],
clientId: '133721337',
currencyIsoCode: 'EUR',
currencySymbol: 'E',
currentAccountId: 'ABC123',
dealingEnabled: false,
hasActiveDemoAccounts: true,
hasActiveLiveAccounts: true,
lightstreamerEndpoint: 'https://apd.marketdatasystems.com',
reroutingEnvironment: null,
timezoneOffset: 1,
trailingStopsEnabled: false,
})
);

const session = await global.client.rest.login.createSession('test-user', 'test-password');
expect(session.accounts[0].accountId).toBe('ABC123');
expect(session.clientId).toBe('133721337');
});
});
});
4 changes: 3 additions & 1 deletion src/login/LoginAPI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {AxiosInstance} from 'axios';
import {Authorization} from '../client';

export type ReroutingEnvironment = 'DEMO' | 'LIVE' | 'TEST' | 'UAT';

export interface AccountInfo {
available: number;
balance: number;
Expand All @@ -27,7 +29,7 @@ export interface TradingSession {
hasActiveDemoAccounts: boolean;
hasActiveLiveAccounts: boolean;
lightstreamerEndpoint: string;
reroutingEnvironment: null;
reroutingEnvironment?: ReroutingEnvironment;
timezoneOffset: number;
trailingStopsEnabled: boolean;
}
Expand Down
79 changes: 79 additions & 0 deletions src/market/MarketAPI.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import nock from 'nock';
import {APIClient} from '../APIClient';
import {MarketAPI} from './MarketAPI';

describe('MarketAPI', () => {
describe('searchMarkets', () => {
it('returns all markets matching the search term', async () => {
nock(APIClient.URL_DEMO)
.get(MarketAPI.URL.MARKETS)
.query({searchTerm: 'PFE'})
.reply(
200,
JSON.stringify({
markets: [
{
bid: null,
delayTime: 0,
epic: 'SE.D.PFE.CASH.IP',
expiry: '-',
high: 40.49,
instrumentName: 'Pfizer Inc (All Sessions)',
instrumentType: 'SHARES',
low: 38.24,
marketStatus: 'EDITS_ONLY',
netChange: 0.18,
offer: null,
percentageChange: 0.45,
scalingFactor: 100,
streamingPricesAvailable: false,
updateTime: '22:59:58',
updateTimeUTC: '21:59:58',
},
{
bid: null,
delayTime: 0,
epic: 'SI.D.PFNXUS.CASH.IP',
expiry: '-',
high: null,
instrumentName: 'Pfenex Inc',
instrumentType: 'SHARES',
low: null,
marketStatus: 'CLOSED',
netChange: 0,
offer: null,
percentageChange: 0,
scalingFactor: 100,
streamingPricesAvailable: false,
updateTime: '15:30:00',
updateTimeUTC: '14:30:00',
},
{
bid: null,
delayTime: 0,
epic: 'ED.D.PFVGY.CASH.IP',
expiry: '-',
high: 157,
instrumentName: 'Pfeiffer Vacuum Technology AG',
instrumentType: 'SHARES',
low: 152.4,
marketStatus: 'EDITS_ONLY',
netChange: 2.6,
offer: null,
percentageChange: 1.69,
scalingFactor: 100,
streamingPricesAvailable: false,
updateTime: '17:30:00',
updateTimeUTC: '16:30:00',
},
],
})
);

const marketSearch = await global.client.rest.market.searchMarkets('PFE');
expect(marketSearch.markets.length).toBe(3);
expect(marketSearch.markets[0].epic).toBe('SE.D.PFE.CASH.IP');
expect(marketSearch.markets[2].updateTimeUTC).toBe('16:30:00');
});
});
});
44 changes: 44 additions & 0 deletions src/market/MarketAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {AxiosInstance} from 'axios';

export interface Market {
bid?: number;
delayTime: number;
epic: string;
expiry: string;
high?: number;
instrumentName: string;
instrumentType: string;
low?: number;
marketStatus: string;
netChange: number;
offer?: number;
percentageChange: number;
scalingFactor: number;
streamingPricesAvailable: boolean;
updateTime: string;
updateTimeUTC: string;
}

export interface MarketSearch {
markets: Market[];
}

export class MarketAPI {
static readonly URL = {
MARKETS: `/markets`,
};

constructor(private readonly apiClient: AxiosInstance) {}

/**
* Returns all markets matching the search term.
*
* @param searchTerm - The term to be used in the search
* @see https://labs.ig.com/rest-trading-api-reference/service-detail?id=547
*/
async searchMarkets(searchTerm: string): Promise<MarketSearch> {
const resource = `${MarketAPI.URL.MARKETS}?searchTerm=${searchTerm}`;
const response = await this.apiClient.get<MarketSearch>(resource);
return response.data;
}
}
1 change: 1 addition & 0 deletions src/market/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './MarketAPI';
16 changes: 16 additions & 0 deletions src/test/helpers/setupIG.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {APIClient} from '../../APIClient';
import nock from 'nock';

declare global {
module NodeJS {
interface Global {
client: APIClient;
}
}
}

beforeEach(() => {
global.client = new APIClient(APIClient.URL_DEMO, '');
});

afterEach(() => nock.cleanAll());

0 comments on commit bac4839

Please sign in to comment.