Skip to content

Commit

Permalink
feat(client): Automatically create trading session with username and …
Browse files Browse the repository at this point in the history
…password (#169)
  • Loading branch information
bennycode committed May 11, 2021
1 parent 182878b commit 2cb80b3
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 40 deletions.
25 changes: 12 additions & 13 deletions README.md
Expand Up @@ -25,31 +25,30 @@ npm install ig-trading-api
yarn add ig-trading-api
```

## Setup
## Usage

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(APIClient.URL_LIVE, 'your-api-key');
```
### TypeScript

**TypeScript**
Recommended:

```typescript
import {APIClient} from 'ig-trading-api';
const client = new APIClient(APIClient.URL_LIVE, 'your-api-key');
const session = await client.rest.login.createSession('your-username', 'your-password');
console.info(`Your client ID is "${session.clientId}".`);
```

## Usage

### Login
Alternative:

```typescript
const session = await client.rest.login.createSession('your-username', 'your-password');
console.info(`Your client ID is "${session.clientId}".`);
import {APIClient} from 'ig-trading-api';
const client = new APIClient(APIClient.URL_LIVE, {
apiKey: 'your-api-key',
username: 'your-username',
password: 'your-password',
});
```

## Resources
Expand Down
42 changes: 22 additions & 20 deletions src/client/RESTClient.ts
Expand Up @@ -50,32 +50,34 @@ export class RESTClient {
this.auth = this.apiKey;
}

function randomNum(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1) + min);
}

axiosRetry(this.httpClient, {
retries: Infinity,
retryCondition: (error: AxiosError) => {
const errorCode = error.response?.data?.errorCode;

const gotRateLimited = errorCode === 'error.public-api.exceeded-api-key-allowance';
const expiredSecurityToken = errorCode === 'error.security.oauth-token-invalid';
const missingToken = errorCode === 'error.security.client-token-missing';

if (gotRateLimited) {
return true;
} else if (expiredSecurityToken || missingToken) {
void this.login.refreshToken();
return true;
switch (errorCode) {
case 'error.public-api.exceeded-api-key-allowance':
// Got rate limited
return true;
case 'error.security.oauth-token-invalid':
// Security token expired
void this.login.refreshToken();
return true;
case 'error.security.client-token-missing':
// Trading session has not been initialized
const {username, password} = this.auth;
if (username && password) {
void this.login.createSession(this.auth.username, this.auth.password);
return true;
}
throw new Error(
`Cannot fulfill request because there is no active session and username & password have not been provided.`
);
default:
return true;
}

return true;
},
retryDelay: (retryCount: number) => {
/** Rate limits: https://labs.ig.com/faq */
return randomNum(1000, 3000) * retryCount;
},
retryDelay: axiosRetry.exponentialDelay,
});

this.httpClient.interceptors.request.use(async config => {
Expand All @@ -92,7 +94,7 @@ export class RESTClient {
updatedHeaders.CST = clientSessionToken;
} else {
if (accessToken) {
updatedHeaders.Authorization = 'Bearer ' + accessToken;
updatedHeaders.Authorization = `Bearer ${accessToken}`;
} else if (securityToken && clientSessionToken) {
updatedHeaders['X-SECURITY-TOKEN'] = securityToken;
updatedHeaders.CST = clientSessionToken;
Expand Down
86 changes: 86 additions & 0 deletions src/dealing/DealingAPI.test.ts
Expand Up @@ -15,6 +15,7 @@ import {
PositionOrderType,
PositionUpdateRequest,
} from './DealingAPI';
import {LoginAPI} from '../login';

describe('DealingAPI', () => {
describe('getAllOpenPositions', () => {
Expand Down Expand Up @@ -424,6 +425,91 @@ describe('DealingAPI', () => {
expect(error.config['axios-retry'].retryCount).toBe(amountOfRetries);
}
}, 10_000);

it('tries to init a trading session when no session was created', async () => {
const dealId = '12345';

nock(APIClient.URL_DEMO)
.post(
DealingAPI.URL.WORKINGORDERS_OTC + dealId,
{},
{
reqheaders: {
_method: 'DELETE',
},
}
)
.reply(
401,
JSON.stringify({
errorCode: 'error.security.client-token-missing',
})
);

const amountOfRetries = 0;

const apiClient = new APIClient(APIClient.URL_DEMO, {
apiKey: 'local-demo-api-key',
password: 'local-demo-password',
username: 'local-demo-username',
});

apiClient.rest.defaults['axios-retry'] = {
retries: amountOfRetries,
};

const createSession = spyOn<LoginAPI>(apiClient.rest.login, 'createSession').and.callFake(() => {});

try {
await apiClient.rest.dealing.deleteOrder(dealId);
fail('Expected error');
} catch (error) {
expect(createSession).toHaveBeenCalledTimes(1);
}
});

it('fails to automatically init a trading session if username and password are not supplied', async () => {
const dealId = '12345';

nock(APIClient.URL_DEMO)
.post(
DealingAPI.URL.WORKINGORDERS_OTC + dealId,
{},
{
reqheaders: {
_method: 'DELETE',
},
}
)
.reply(
401,
JSON.stringify({
errorCode: 'error.security.client-token-missing',
})
);

const amountOfRetries = 0;

const apiClient = new APIClient(APIClient.URL_DEMO, {
apiKey: 'local-demo-api-key',
});

apiClient.rest.defaults['axios-retry'] = {
retries: amountOfRetries,
};

const createSession = spyOn<LoginAPI>(apiClient.rest.login, 'createSession').and.callFake(() => {});

try {
await apiClient.rest.dealing.deleteOrder(dealId);
fail('Expected error');
} catch (error) {
expect(createSession).not.toHaveBeenCalled();
expect(error.message).toBe(
'Cannot fulfill request because there is no active session and username & password have not been provided.'
);
}
});
});

describe('updateOrder', () => {
Expand Down
5 changes: 4 additions & 1 deletion src/demo/account.ts
Expand Up @@ -27,4 +27,7 @@ async function main(): Promise<void> {
});
}

main().catch(console.error);
main().catch(error => {
console.error(error);
process.exit(1);
});
5 changes: 4 additions & 1 deletion src/demo/login.ts
Expand Up @@ -4,4 +4,7 @@ async function main(): Promise<void> {
await initClient();
}

main().catch(console.error);
main().catch(error => {
console.error(error);
process.exit(1);
});
5 changes: 4 additions & 1 deletion src/demo/order.ts
Expand Up @@ -43,4 +43,7 @@ async function main(): Promise<void> {
console.info(`Your deleted order deal reference is "${closeOrderSession.dealReference}".`);
}

main().catch(console.error);
main().catch(error => {
console.error(error);
process.exit(1);
});
5 changes: 4 additions & 1 deletion src/demo/position.ts
Expand Up @@ -58,4 +58,7 @@ async function main(): Promise<void> {
process.exit(0);
}

main().catch(console.error);
main().catch(error => {
console.error(error);
process.exit(1);
});
5 changes: 4 additions & 1 deletion src/demo/start.ts
Expand Up @@ -14,4 +14,7 @@ async function main(): Promise<void> {
global.IGClient = client;
}

main().catch(console.error);
main().catch(error => {
console.error(error);
process.exit(1);
});
5 changes: 4 additions & 1 deletion src/demo/stream.ts
Expand Up @@ -14,4 +14,7 @@ async function main(): Promise<void> {
);
}

main().catch(console.error);
main().catch(error => {
console.error(error);
process.exit(1);
});
2 changes: 1 addition & 1 deletion src/test/helpers/setupIG.ts
Expand Up @@ -10,7 +10,7 @@ declare global {
}

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

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

0 comments on commit 2cb80b3

Please sign in to comment.