diff --git a/.eslintrc.js b/.eslintrc.js index f8e2339..e4da6a6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -73,7 +73,6 @@ module.exports = { "@typescript-eslint/no-shadow": ["error"], "no-throw-literal": "error", "no-undef-init": "error", - "no-undefined": "error", "no-underscore-dangle": "error", "no-unneeded-ternary": "error", "no-unused-expressions": "error", diff --git a/src/alpaca/actions.ts b/src/alpaca/actions.ts new file mode 100644 index 0000000..471f6dc --- /dev/null +++ b/src/alpaca/actions.ts @@ -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 => { + 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).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; + } +} diff --git a/src/alpaca/exchange.ts b/src/alpaca/exchange.ts index eceadd4..3ada9ee 100644 --- a/src/alpaca/exchange.ts +++ b/src/alpaca/exchange.ts @@ -1,6 +1,7 @@ import Alpaca from '@alpacahq/alpaca-trade-api'; import { AlpacaPortfolioProvider } from './portfolio'; import { AlpacaQuoteProvider } from './quote'; +import { AlpacaActionProvider } from './actions'; import { Exchange } from '../interface/exchange'; import { PortfolioProvider } from '../interface/portfolio'; import { QuoteProvider } from '../interface/quote'; @@ -51,7 +52,7 @@ export class AlpacaExchange implements Exchange { this.portfolioProvider = new AlpacaPortfolioProvider(this.alpaca); this.quoteProvider = new AlpacaQuoteProvider(this.alpaca); - this.actionProvider = null!; + this.actionProvider = new AlpacaActionProvider(this.alpaca); this.name = 'Alpaca'; } diff --git a/src/index.ts b/src/index.ts index c85fec1..099cffe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,7 @@ export * from './interface/exchange'; export * from './interface/portfolio'; export * from './interface/quote'; export * from './interface/actions'; + +export * from './alpaca/exchange'; +export * from './alpaca/portfolio'; +export * from './alpaca/quote'; diff --git a/src/interface/actions.ts b/src/interface/actions.ts index 2fdad9c..91ca388 100644 --- a/src/interface/actions.ts +++ b/src/interface/actions.ts @@ -30,6 +30,11 @@ export class Action { */ readonly pricePerShare: number; + /** + * The timestamp of the action + */ + readonly timestamp: Date; + /** * Represents a user Action. * @constructor @@ -37,12 +42,14 @@ export class Action { * @param {number} quantity - The quantity of the asset being traded. * @param {ActionSide} side - The side of the trade (buy or sell). * @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.quantity = quantity; this.side = side; this.pricePerShare = pricePerShare; + this.timestamp = timestamp; } } @@ -94,13 +101,23 @@ export class ActionFetchOptions { * The date options for filtering actions. */ 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. */ -export interface ActionFetchResponse { - +export class ActionFetchResponse { /** * An array of `Action` objects. */ @@ -111,6 +128,17 @@ export interface ActionFetchResponse { * Returns a promise that resolves to an `ActionFetchResponse` object. */ readonly fetchNextPage?: () => Promise; + + /** + * 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) { + this.actions = actions; + this.fetchNextPage = fetchNextPage; + } } /** diff --git a/test/alpaca.test.ts b/test/alpaca.test.ts index a96b15d..9c23980 100644 --- a/test/alpaca.test.ts +++ b/test/alpaca.test.ts @@ -1,19 +1,63 @@ import { describe, expect, test } from '@jest/globals'; 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', () => { - test('portfolio fetch', () => { + test('portfolio 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); - 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_SECRET_KEY).toBeDefined(); 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); });