Merge branch '14-implement-action-fetching-for-alpaca' into 'main'
Resolve "Implement action fetching for Alpaca" Closes #14 See merge request finvis/usxi!13
This commit is contained in:
commit
985a155dbe
@ -73,7 +73,6 @@ module.exports = {
|
|||||||
"@typescript-eslint/no-shadow": ["error"],
|
"@typescript-eslint/no-shadow": ["error"],
|
||||||
"no-throw-literal": "error",
|
"no-throw-literal": "error",
|
||||||
"no-undef-init": "error",
|
"no-undef-init": "error",
|
||||||
"no-undefined": "error",
|
|
||||||
"no-underscore-dangle": "error",
|
"no-underscore-dangle": "error",
|
||||||
"no-unneeded-ternary": "error",
|
"no-unneeded-ternary": "error",
|
||||||
"no-unused-expressions": "error",
|
"no-unused-expressions": "error",
|
||||||
|
54
src/alpaca/actions.ts
Normal file
54
src/alpaca/actions.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import Alpaca from '@alpacahq/alpaca-trade-api';
|
||||||
|
import { Action, ActionSide, ActionFetchOptions, ActionFetchResponse, ActionDateType } from '../interface/actions';
|
||||||
|
|
||||||
|
interface AlpacaActivity {
|
||||||
|
id: string;
|
||||||
|
activity_type: string;
|
||||||
|
transaction_time: string;
|
||||||
|
type: string;
|
||||||
|
price: string;
|
||||||
|
qty: string;
|
||||||
|
side: string;
|
||||||
|
symbol: string;
|
||||||
|
leaves_qty: string;
|
||||||
|
order_id: string;
|
||||||
|
cum_qty: string;
|
||||||
|
order_status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AlpacaActionProvider {
|
||||||
|
readonly alpaca: Alpaca;
|
||||||
|
|
||||||
|
readonly fetchActions = (options: ActionFetchOptions): Promise<ActionFetchResponse> => {
|
||||||
|
return (this.alpaca.getAccountActivities({
|
||||||
|
activityTypes: "FILL",
|
||||||
|
until: options.dateOptions?.dateType === ActionDateType.Before ? options.dateOptions.date : undefined,
|
||||||
|
after: options.dateOptions?.dateType === ActionDateType.After ? options.dateOptions.date : undefined,
|
||||||
|
direction: "desc",
|
||||||
|
date: options.dateOptions?.dateType === ActionDateType.On ? options.dateOptions.date : undefined,
|
||||||
|
pageSize: options.pageSize,
|
||||||
|
pageToken: undefined,
|
||||||
|
}) as Promise<AlpacaActivity[]>).then((activities) => {
|
||||||
|
return new ActionFetchResponse(
|
||||||
|
activities
|
||||||
|
.filter((activity) => activity.order_status === "filled")
|
||||||
|
.map((activity) => {
|
||||||
|
return new Action(
|
||||||
|
activity.symbol,
|
||||||
|
parseInt(activity.qty, 10),
|
||||||
|
activity.side === "buy" ? ActionSide.Buy : ActionSide.Sell,
|
||||||
|
parseFloat(activity.price),
|
||||||
|
new Date(activity.transaction_time),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}).catch((err) => {
|
||||||
|
return err;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(alpaca: Alpaca) {
|
||||||
|
this.alpaca = alpaca;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import Alpaca from '@alpacahq/alpaca-trade-api';
|
import Alpaca from '@alpacahq/alpaca-trade-api';
|
||||||
import { AlpacaPortfolioProvider } from './portfolio';
|
import { AlpacaPortfolioProvider } from './portfolio';
|
||||||
import { AlpacaQuoteProvider } from './quote';
|
import { AlpacaQuoteProvider } from './quote';
|
||||||
|
import { AlpacaActionProvider } from './actions';
|
||||||
import { Exchange } from '../interface/exchange';
|
import { Exchange } from '../interface/exchange';
|
||||||
import { PortfolioProvider } from '../interface/portfolio';
|
import { PortfolioProvider } from '../interface/portfolio';
|
||||||
import { QuoteProvider } from '../interface/quote';
|
import { QuoteProvider } from '../interface/quote';
|
||||||
@ -51,7 +52,7 @@ export class AlpacaExchange implements Exchange {
|
|||||||
|
|
||||||
this.portfolioProvider = new AlpacaPortfolioProvider(this.alpaca);
|
this.portfolioProvider = new AlpacaPortfolioProvider(this.alpaca);
|
||||||
this.quoteProvider = new AlpacaQuoteProvider(this.alpaca);
|
this.quoteProvider = new AlpacaQuoteProvider(this.alpaca);
|
||||||
this.actionProvider = null!;
|
this.actionProvider = new AlpacaActionProvider(this.alpaca);
|
||||||
|
|
||||||
this.name = 'Alpaca';
|
this.name = 'Alpaca';
|
||||||
}
|
}
|
||||||
|
@ -2,3 +2,7 @@ export * from './interface/exchange';
|
|||||||
export * from './interface/portfolio';
|
export * from './interface/portfolio';
|
||||||
export * from './interface/quote';
|
export * from './interface/quote';
|
||||||
export * from './interface/actions';
|
export * from './interface/actions';
|
||||||
|
|
||||||
|
export * from './alpaca/exchange';
|
||||||
|
export * from './alpaca/portfolio';
|
||||||
|
export * from './alpaca/quote';
|
||||||
|
@ -30,6 +30,11 @@ export class Action {
|
|||||||
*/
|
*/
|
||||||
readonly pricePerShare: number;
|
readonly pricePerShare: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of the action
|
||||||
|
*/
|
||||||
|
readonly timestamp: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a user Action.
|
* Represents a user Action.
|
||||||
* @constructor
|
* @constructor
|
||||||
@ -37,12 +42,14 @@ export class Action {
|
|||||||
* @param {number} quantity - The quantity of the asset being traded.
|
* @param {number} quantity - The quantity of the asset being traded.
|
||||||
* @param {ActionSide} side - The side of the trade (buy or sell).
|
* @param {ActionSide} side - The side of the trade (buy or sell).
|
||||||
* @param {number} pricePerShare - The price per share of the asset being traded.
|
* @param {number} pricePerShare - The price per share of the asset being traded.
|
||||||
|
* @param {Date} timestamp - The timestamp of the action.
|
||||||
*/
|
*/
|
||||||
constructor(symbol: string, quantity: number, side: ActionSide, pricePerShare: number) {
|
constructor(symbol: string, quantity: number, side: ActionSide, pricePerShare: number, timestamp: Date) {
|
||||||
this.symbol = symbol;
|
this.symbol = symbol;
|
||||||
this.quantity = quantity;
|
this.quantity = quantity;
|
||||||
this.side = side;
|
this.side = side;
|
||||||
this.pricePerShare = pricePerShare;
|
this.pricePerShare = pricePerShare;
|
||||||
|
this.timestamp = timestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,13 +101,23 @@ export class ActionFetchOptions {
|
|||||||
* The date options for filtering actions.
|
* The date options for filtering actions.
|
||||||
*/
|
*/
|
||||||
readonly dateOptions?: ActionDateOptions;
|
readonly dateOptions?: ActionDateOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a set of options for an Action fetch.
|
||||||
|
* @constructor
|
||||||
|
* @param pageSize - The size of the page if paging is desired.
|
||||||
|
* @param dateOptions - The options for Date filtering.
|
||||||
|
*/
|
||||||
|
constructor(pageSize?: number, dateOptions?: ActionDateOptions) {
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
this.dateOptions = dateOptions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the response of a fetch action request.
|
* Represents the response of a fetch action request.
|
||||||
*/
|
*/
|
||||||
export interface ActionFetchResponse {
|
export class ActionFetchResponse {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of `Action` objects.
|
* An array of `Action` objects.
|
||||||
*/
|
*/
|
||||||
@ -111,6 +128,17 @@ export interface ActionFetchResponse {
|
|||||||
* Returns a promise that resolves to an `ActionFetchResponse` object.
|
* Returns a promise that resolves to an `ActionFetchResponse` object.
|
||||||
*/
|
*/
|
||||||
readonly fetchNextPage?: () => Promise<ActionFetchResponse>;
|
readonly fetchNextPage?: () => Promise<ActionFetchResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the Actions class.
|
||||||
|
* @constructor
|
||||||
|
* @param actions The list of actions.
|
||||||
|
* @param fetchNextPage A function that fetches the next page of actions.
|
||||||
|
*/
|
||||||
|
constructor(actions: Action[], fetchNextPage?: () => Promise<ActionFetchResponse>) {
|
||||||
|
this.actions = actions;
|
||||||
|
this.fetchNextPage = fetchNextPage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,19 +1,63 @@
|
|||||||
import { describe, expect, test } from '@jest/globals';
|
import { describe, expect, test } from '@jest/globals';
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { AlpacaExchange } from '../src/alpaca/exchange';
|
import { ActionDateOptions, ActionDateType, ActionFetchOptions, AlpacaExchange } from '../src/index';
|
||||||
|
import { createLogger, transports, format } from "winston";
|
||||||
|
|
||||||
|
const timeout = 10000;
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
transports: [new transports.Console()],
|
||||||
|
format: format.combine(
|
||||||
|
format.colorize(),
|
||||||
|
format.timestamp(),
|
||||||
|
format.printf(({ timestamp, level, message, service }) => {
|
||||||
|
return `[${timestamp}] ${service} ${level}: ${message}`;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
defaultMeta: {
|
||||||
|
service: "AlpacaTest",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('Alpaca Tests', () => {
|
describe('Alpaca Tests', () => {
|
||||||
test('portfolio fetch', () => {
|
test('portfolio fetch', async () => {
|
||||||
expect(process.env.ALPACA_API_KEY).toBeDefined();
|
expect(process.env.ALPACA_API_KEY).toBeDefined();
|
||||||
expect(process.env.ALPACA_SECRET_KEY).toBeDefined();
|
expect(process.env.ALPACA_SECRET_KEY).toBeDefined();
|
||||||
const exchange = new AlpacaExchange(process.env.ALPACA_API_KEY!, process.env.ALPACA_SECRET_KEY!, true);
|
const exchange = new AlpacaExchange(process.env.ALPACA_API_KEY!, process.env.ALPACA_SECRET_KEY!, true);
|
||||||
expect(exchange.portfolioProvider.fetchPortfolio()).resolves.toBeDefined();
|
await expect(exchange.portfolioProvider.fetchPortfolio()).resolves.toBeDefined();
|
||||||
});
|
}, timeout);
|
||||||
|
|
||||||
test('quote fetch', () => {
|
test('quote fetch', async () => {
|
||||||
expect(process.env.ALPACA_API_KEY).toBeDefined();
|
expect(process.env.ALPACA_API_KEY).toBeDefined();
|
||||||
expect(process.env.ALPACA_SECRET_KEY).toBeDefined();
|
expect(process.env.ALPACA_SECRET_KEY).toBeDefined();
|
||||||
const exchange = new AlpacaExchange(process.env.ALPACA_API_KEY!, process.env.ALPACA_SECRET_KEY!, true);
|
const exchange = new AlpacaExchange(process.env.ALPACA_API_KEY!, process.env.ALPACA_SECRET_KEY!, true);
|
||||||
expect(exchange.quoteProvider.fetchQuote("AAPL")).resolves.toBeDefined();
|
await expect(exchange.quoteProvider.fetchQuote("AAPL")).resolves.toBeDefined();
|
||||||
});
|
}, timeout);
|
||||||
|
|
||||||
|
test('action fetch', async () => {
|
||||||
|
expect(process.env.ALPACA_API_KEY).toBeDefined();
|
||||||
|
expect(process.env.ALPACA_SECRET_KEY).toBeDefined();
|
||||||
|
const exchange = new AlpacaExchange(process.env.ALPACA_API_KEY!, process.env.ALPACA_SECRET_KEY!, true);
|
||||||
|
|
||||||
|
const fetchOptions = new ActionFetchOptions(undefined, new ActionDateOptions(new Date("2023-10-23T13:30:28.163Z"), ActionDateType.On));
|
||||||
|
|
||||||
|
const response = await exchange.actionProvider.fetchActions(fetchOptions);
|
||||||
|
|
||||||
|
expect(response).toBeDefined();
|
||||||
|
|
||||||
|
logger.info(JSON.stringify(response));
|
||||||
|
|
||||||
|
for (const action of response.actions) {
|
||||||
|
expect(action).toBeDefined();
|
||||||
|
expect(action.symbol).toBeDefined();
|
||||||
|
expect(action.quantity).toBeDefined();
|
||||||
|
expect(action.side).toBeDefined();
|
||||||
|
expect(action.pricePerShare).toBeDefined();
|
||||||
|
|
||||||
|
expect(action.timestamp).toBeDefined();
|
||||||
|
expect(action.timestamp.getFullYear()).toBe(2023);
|
||||||
|
expect(action.timestamp.getMonth()).toBe(9);
|
||||||
|
expect(action.timestamp.getDate()).toBe(23);
|
||||||
|
}
|
||||||
|
}, timeout);
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user