Resolve "Implement action fetching for Alpaca" #31

Merged
clbertolini merged 12 commits from 14-implement-action-fetching-for-alpaca into main 2023-11-10 21:36:43 +00:00
6 changed files with 142 additions and 12 deletions

View File

@ -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",

54
src/alpaca/actions.ts Normal file
View 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;
}
}

View File

@ -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';
}

View File

@ -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';

View File

@ -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<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;
}
}
/**

View File

@ -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);
});