diff --git a/.eslintrc.js b/.eslintrc.js index bd783a6..cc54a78 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,136 +1,137 @@ module.exports = { - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: "latest", // Allows the use of modern ECMAScript features - sourceType: "module", // Allows for the use of imports - project: ["./tsconfig.json", "./tsconfig.test.json"] - }, - extends: ["plugin:@typescript-eslint/recommended"], // Uses the linting rules from @typescript-eslint/eslint-plugin - env: { - node: true, // Enable Node.js global variables - }, - rules: { - "array-callback-return": "error", - "no-await-in-loop": "error", - "no-cond-assign": [ "error", "always" ], - "no-constant-binary-expression": "error", - "no-constant-condition": "error", - "no-constructor-return": "error", - "no-duplicate-imports": "error", - "no-promise-executor-return": "error", - "no-self-compare": "error", - "no-undef": [ "error", { "typeof": true } ], - "no-unmodified-loop-condition": "error", - "no-unreachable-loop": "error", - "no-unused-private-class-members": "error", - "require-atomic-updates": "error", - - "accessor-pairs": "error", - "block-scoped-var": "error", - "curly": [ "error", "all" ], - "default-case": "error", - "default-case-last": "error", - "default-param-last": "error", - "dot-notation": "error", - "eqeqeq": "error", - "func-style": [ "error", "expression" ], - "grouped-accessor-pairs": [ "error", "getBeforeSet" ], - "no-caller": "error", - "no-console": "error", - "no-eq-null": "error", - "no-eval": "error", - "no-extend-native": "error", - "no-extra-label": "error", - "no-implicit-coercion": "error", - "no-implicit-globals": "error", - "no-implied-eval": "error", - "no-inline-comments": "error", - "no-invalid-this": "error", - "no-iterator": "error", - "no-label-var": "error", - "no-labels": "error", - "no-lone-blocks": "error", - "no-lonely-if": "error", - "no-loop-func": "error", - "no-magic-numbers": "error", - "no-mixed-operators": "error", - "no-multi-assign": "error", - "no-multi-str": "error", - "no-nested-ternary": "error", - "no-new": "error", - "no-new-func": "error", - "no-new-wrappers": "error", - "no-nonoctal-decimal-escape": "error", - "no-object-constructor": "error", - "no-octal-escape": "error", - "no-param-reassign": "error", - "no-plusplus": "error", - "no-proto": "error", - "no-return-assign": "error", - "no-script-url": "error", - "no-sequences": "error", - "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", - "no-useless-call": "error", - "no-useless-catch": "error", - "no-useless-computed-key": "error", - "no-useless-concat": "error", - "no-useless-constructor": "error", - "no-useless-rename": "error", - "no-var": "error", - "no-void": "error", - "one-var": [ "error", "never" ], - "one-var-declaration-per-line": [ "error", "always" ], - "prefer-arrow-callback": "error", - "prefer-const": "error", - "prefer-exponentiation-operator": "error", - "prefer-named-capture-group": "error", - "prefer-numeric-literals": "error", - "prefer-object-has-own": "error", - "prefer-object-spread": "error", - "prefer-promise-reject-errors": "error", - "prefer-regex-literals": "error", - "prefer-rest-params": "error", - "prefer-spread": "error", - "prefer-template": "error", - "quote-props": [ "error", "as-needed" ], - "radix": "error", - "require-await": "error", - "require-unicode-regexp": "error", - "symbol-description": "error", - "yoda": "error", - - "array-bracket-newline": [ "error", "consistent" ], - "array-bracket-spacing": [ "error", "always", { "singleValue": false } ], - "array-element-newline": [ "error", "consistent" ], - "arrow-parens": "error", - "arrow-spacing": "error", - "block-spacing": "error", - "computed-property-spacing": "error", - "eol-last": "error", - "generator-star-spacing": [ "error", { "before": false, "after": true } ], - "key-spacing": "error", - "linebreak-style": "error", - "lines-around-comment": [ "error", { "allowBlockStart": true }], - "max-len": [ "error", { "code": 120 } ], - "new-parens": "error", - "no-multi-spaces": "error", - "no-multiple-empty-lines": "error", - "no-trailing-spaces": "error", - "no-whitespace-before-property": "error", - "nonblock-statement-body-position": [ "error", "below" ], - "object-curly-newline": [ "error", { "ObjectExpression": { "consistent": true }, "ObjectPattern": { "consistent": true } } ], - "operator-linebreak": "error", - "padded-blocks": [ "error", "never" ], - "space-in-parens": "error", - "switch-colon-spacing": [ "error", { "after": false, "before": false } ], - "template-curly-spacing": "error", - "wrap-iife": "error", - "yield-star-spacing": [ "error", "after" ], - } - }; \ No newline at end of file + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: "latest", // Allows the use of modern ECMAScript features + sourceType: "module", // Allows for the use of imports + project: ["./tsconfig.json", "./tsconfig.test.json"] + }, + extends: ["plugin:@typescript-eslint/recommended"], // Uses the linting rules from @typescript-eslint/eslint-plugin + env: { + node: true, // Enable Node.js global variables + }, + rules: { + "array-callback-return": "error", + "no-await-in-loop": "error", + "no-cond-assign": ["error", "always"], + "no-constant-binary-expression": "error", + "no-constant-condition": "error", + "no-constructor-return": "error", + "no-duplicate-imports": "error", + "no-promise-executor-return": "error", + "no-self-compare": "error", + "no-undef": ["error", { "typeof": true }], + "no-unmodified-loop-condition": "error", + "no-unreachable-loop": "error", + "no-unused-private-class-members": "error", + "require-atomic-updates": "error", + + "accessor-pairs": "error", + "block-scoped-var": "error", + "curly": ["error", "all"], + "default-case": "error", + "default-case-last": "error", + "default-param-last": "error", + "dot-notation": "error", + "eqeqeq": "error", + "func-style": ["error", "expression"], + "grouped-accessor-pairs": ["error", "getBeforeSet"], + "no-caller": "error", + "no-console": "error", + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-label": "error", + "no-implicit-coercion": "error", + "no-implicit-globals": "error", + "no-implied-eval": "error", + "no-inline-comments": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-lonely-if": "error", + "no-loop-func": "error", + "no-magic-numbers": "error", + "no-mixed-operators": "error", + "no-multi-assign": "error", + "no-multi-str": "error", + "no-nested-ternary": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-nonoctal-decimal-escape": "error", + "no-object-constructor": "error", + "no-octal-escape": "error", + "no-param-reassign": "error", + "no-plusplus": "error", + "no-proto": "error", + "no-return-assign": "error", + "no-script-url": "error", + "no-sequences": "error", + "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", + "no-useless-call": "error", + "no-useless-catch": "error", + "no-useless-computed-key": "error", + "no-useless-concat": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-var": "error", + "no-void": "error", + "one-var": ["error", "never"], + "one-var-declaration-per-line": ["error", "always"], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-exponentiation-operator": "error", + "prefer-named-capture-group": "error", + "prefer-numeric-literals": "error", + "prefer-object-has-own": "error", + "prefer-object-spread": "error", + "prefer-promise-reject-errors": "error", + "prefer-regex-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "quote-props": ["error", "as-needed"], + "semi": ["error", "always"], + "radix": "error", + "require-await": "error", + "require-unicode-regexp": "error", + "symbol-description": "error", + "yoda": "error", + + "array-bracket-newline": ["error", "consistent"], + "array-bracket-spacing": ["error", "always", { "singleValue": false }], + "array-element-newline": ["error", "consistent"], + "arrow-parens": "error", + "arrow-spacing": "error", + "block-spacing": "error", + "computed-property-spacing": "error", + "eol-last": "error", + "generator-star-spacing": ["error", { "before": false, "after": true }], + "key-spacing": "error", + "linebreak-style": "error", + "lines-around-comment": ["error", { "allowBlockStart": true }], + "max-len": ["error", { "code": 120 }], + "new-parens": "error", + "no-multi-spaces": "error", + "no-multiple-empty-lines": "error", + "no-trailing-spaces": "error", + "no-whitespace-before-property": "error", + "nonblock-statement-body-position": ["error", "below"], + "object-curly-newline": ["error", { "ObjectExpression": { "consistent": true }, "ObjectPattern": { "consistent": true } }], + "operator-linebreak": "error", + "padded-blocks": ["error", "never"], + "space-in-parens": "error", + "switch-colon-spacing": ["error", { "after": false, "before": false }], + "template-curly-spacing": "error", + "wrap-iife": "error", + "yield-star-spacing": ["error", "after"], + } +}; \ No newline at end of file diff --git a/.gitignore b/.gitignore index fe72787..f9890c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/** dist/** -docs/** \ No newline at end of file +docs/** +.env \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index 6bd32a9..4557437 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -8,6 +8,7 @@ const config: Config.InitialOptions = }, collectCoverageFrom: ['src/*.ts'], + verbose: false, } export default config \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b2a5521..fbd190a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "GPL-3.0-or-later", "dependencies": { + "@alpacahq/alpaca-trade-api": "^3.0.1", + "dotenv": "^16.3.1", "winston": "^3.10.0" }, "devDependencies": { @@ -34,6 +36,35 @@ "node": ">=0.10.0" } }, + "node_modules/@alpacahq/alpaca-trade-api": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@alpacahq/alpaca-trade-api/-/alpaca-trade-api-3.0.1.tgz", + "integrity": "sha512-FOsgA4jREAArWbb7nuKTo2yiUdb8+/4vboyTX2SRJSi+oYBaFrZV+hMNx7fGWtuLuKGRKEdK8Nc+8MFjwcbz4g==", + "dependencies": { + "axios": "^0.21.1", + "dotenv": "^6.2.0", + "events": "^3.2.0", + "just-extend": "^4.1.0", + "lodash": "^4.17.19", + "minimist": "^1.2.6", + "msgpack5": "^5.3.2", + "nats": "^1.4.9", + "urljoin": "^0.1.5", + "ws": "^7.4.3" + }, + "engines": { + "node": ">=14.x", + "npm": ">=6" + } + }, + "node_modules/@alpacahq/alpaca-trade-api/node_modules/dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==", + "engines": { + "node": ">=6" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -662,9 +693,9 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", - "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", @@ -1822,6 +1853,14 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1944,6 +1983,35 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2019,6 +2087,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2346,6 +2437,17 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.544", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.544.tgz", @@ -2565,6 +2667,14 @@ "node": ">=0.10.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2613,6 +2723,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/extend": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-2.0.2.tgz", + "integrity": "sha512-AgFD4VU+lVLP6vjnlNfF7OeInLTyeyckCNPEsuxz1vi786UuK/nk6ynPuhn/h+Ju9++TQyr5EpLRI14fc1QtTQ==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2747,6 +2862,25 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2918,6 +3052,25 @@ "node": ">=10.17.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3770,6 +3923,11 @@ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", "dev": true }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" + }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -3836,6 +3994,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -3970,11 +4133,48 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/msgpack5": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-5.3.2.tgz", + "integrity": "sha512-e9jz+6InQIUb2cGzZ/Mi+rQBs1KFLby+gNi+22VwQ1pnC9ieZjysKGmRMjdlf6IyjsltbgY4tGoHwHmyS7l94A==", + "dependencies": { + "bl": "^4.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.0", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/nats": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/nats/-/nats-1.4.12.tgz", + "integrity": "sha512-Jf4qesEF0Ay0D4AMw3OZnKMRTQm+6oZ5q8/m4gpy5bTmiDiK6wCXbZpzEslmezGpE93LV3RojNEG6dpK/mysLQ==", + "dependencies": { + "nuid": "^1.1.4", + "ts-nkeys": "^1.0.16" + }, + "bin": { + "node-pub": "examples/node-pub", + "node-reply": "examples/node-reply", + "node-req": "examples/node-req", + "node-sub": "examples/node-sub" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4014,6 +4214,14 @@ "node": ">=8" } }, + "node_modules/nuid": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/nuid/-/nuid-1.1.6.tgz", + "integrity": "sha512-Eb3CPCupYscP1/S1FQcO5nxtu6l/F3k0MQ69h7f5osnsemVk5pkc8/5AyalVT+NCfra9M71U8POqF6EZa6IHvg==", + "engines": { + "node": ">= 8.16.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4864,6 +5072,14 @@ } } }, + "node_modules/ts-nkeys": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/ts-nkeys/-/ts-nkeys-1.0.16.tgz", + "integrity": "sha512-1qrhAlavbm36wtW+7NtKOgxpzl+70NTF8xlz9mEhiA5zHMlMxjj3sEVKWm3pGZhHXE0Q3ykjrj+OSRVaYw+Dqg==", + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -4907,6 +5123,11 @@ } } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5037,6 +5258,14 @@ "punycode": "^2.1.0" } }, + "node_modules/urljoin": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/urljoin/-/urljoin-0.1.5.tgz", + "integrity": "sha512-OSGi+PS3zxk8XfQ+7buaupOdrW9P9p+V9rjxGzJaYEYDe/B2rv3WJCupq5LNERW4w4kWxsduUUrhCxZZiQ2udw==", + "dependencies": { + "extend": "~2.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5168,6 +5397,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index beece7a..b2da7b3 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "typescript": "^5.2.2" }, "dependencies": { + "@alpacahq/alpaca-trade-api": "^3.0.1", + "dotenv": "^16.3.1", "winston": "^3.10.0" } } diff --git a/src/alpaca/exchange.ts b/src/alpaca/exchange.ts new file mode 100644 index 0000000..70269f3 --- /dev/null +++ b/src/alpaca/exchange.ts @@ -0,0 +1,51 @@ +import Alpaca from '@alpacahq/alpaca-trade-api'; +import { AlpacaPortfolioProvider } from './portfolio'; +import { AlpacaQuoteProvider } from './quote'; +import { Exchange } from '../interface/exchange'; +import { PortfolioProvider } from '../interface/portfolio'; +import { QuoteProvider } from '../interface/quote'; + +/** + * Exchange implementation for Alpaca. + */ +export class AlpacaExchange implements Exchange { + /** + * The Alpaca API client. + */ + readonly alpaca: Alpaca; + + /** + * The portfolio provider for the exchange. + */ + readonly portfolioProvider: PortfolioProvider; + + /** + * The quote provider for the exchange. + */ + readonly quoteProvider: QuoteProvider; + + /** + * The name of the exchange. + */ + readonly name: string; + + /** + * Creates an instance of the Alpaca exchange. + * @constructor + * @param {string} keyId - The API key ID for the Alpaca exchange. + * @param {string} secretKey - The secret key for the Alpaca exchange. + * @param {boolean} paper - Whether to use the paper trading environment or not. + */ + constructor(keyId: string, secretKey: string, paper: boolean) { + this.alpaca = new Alpaca({ + keyId: keyId, + secretKey: secretKey, + paper: paper + }); + + this.portfolioProvider = new AlpacaPortfolioProvider(this.alpaca); + this.quoteProvider = new AlpacaQuoteProvider(this.alpaca); + + this.name = 'Alpaca'; + } +} diff --git a/src/alpaca/portfolio.ts b/src/alpaca/portfolio.ts new file mode 100644 index 0000000..728513f --- /dev/null +++ b/src/alpaca/portfolio.ts @@ -0,0 +1,63 @@ +import Alpaca from '@alpacahq/alpaca-trade-api'; +import { PortfolioProvider, Portfolio, Position } from '../interface/portfolio'; + +/** + * The position object returned by Alpaca. + * @see https://alpaca.markets/docs/api-references/trading-api/positions/#properties + */ +class AlpacaPosition { + asset_id!: string; + symbol!: string; + exchange!: string; + asset_class!: string; + avg_entry_price!: string; + qty!: string; + qty_available!: string; + side!: string; + market_value!: string; + cost_basis!: string; + unrealized_pl!: string; + unrealized_plpc!: string; + unrealized_intraday_pl!: string; + unrealized_intraday_plpc!: string; + current_price!: string; + lastday_price!: string; + change_today!: string; + asset_marginable!: string; +} + +/** + * Provides a portfolio using the Alpaca API client. + */ +export class AlpacaPortfolioProvider implements PortfolioProvider { + /** + * The Alpaca API client. + */ + readonly alpaca: Alpaca; + + /** + * Fetches the portfolio. + */ + readonly fetchPortfolio = (): Promise => { + return (this.alpaca.getPositions() as Promise).then((positions) => { + return new Portfolio(positions.map((position) => { + return new Position( + position.symbol, + parseInt(position.qty, 10), + parseFloat(position.market_value), + parseFloat(position.cost_basis), + parseFloat(position.market_value) + ); + })); + }); + }; + + /** + * Creates a new AlpacaPortfolioProvider instance. + * @constructor + * @param {Alpaca} alpaca - The Alpaca API client. + */ + constructor(alpaca: Alpaca) { + this.alpaca = alpaca; + } +} diff --git a/src/alpaca/quote.ts b/src/alpaca/quote.ts new file mode 100644 index 0000000..574a106 --- /dev/null +++ b/src/alpaca/quote.ts @@ -0,0 +1,41 @@ +import Alpaca from '@alpacahq/alpaca-trade-api'; +import { QuoteProvider, Quote } from '../interface/quote'; + +/** + * Provides quotes using the Alpaca API. + */ +export class AlpacaQuoteProvider implements QuoteProvider { + /** + * The Alpaca API client. + */ + readonly alpaca: Alpaca; + + /** + * Fetches a quote for the given stock symbol. + * @param symbol The stock symbol to fetch the quote for. + * @returns A Promise that resolves to a Quote object. + */ + readonly fetchQuote = (symbol: string): Promise => { + return this.alpaca.getLatestQuote(symbol).then((quote) => { + return new Quote( + quote.Symbol, + quote.AskPrice, + quote.AskSize, + quote.BidPrice, + quote.BidSize, + new Date(Date.parse(quote.Timestamp)) + ); + }).catch((err) => { + return err; + }); + }; + + /** + * Creates a new AlpacaQuoteProvider instance. + * @constructor + * @param {Alpaca} alpaca - The Alpaca API client. + */ + constructor(alpaca: Alpaca) { + this.alpaca = alpaca; + } +} diff --git a/src/index.ts b/src/index.ts index 8679f1c..45a1724 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,4 +9,4 @@ export { PortfolioProvider, Quote, QuoteProvider -} \ No newline at end of file +}; diff --git a/src/interface/exchange.ts b/src/interface/exchange.ts index a396215..cb659fb 100644 --- a/src/interface/exchange.ts +++ b/src/interface/exchange.ts @@ -20,4 +20,4 @@ export interface Exchange { * The name of the exchange. */ readonly name: string; -} \ No newline at end of file +} diff --git a/src/interface/portfolio.ts b/src/interface/portfolio.ts index f65e153..1b06baa 100644 --- a/src/interface/portfolio.ts +++ b/src/interface/portfolio.ts @@ -3,67 +3,46 @@ */ export class Position { /** - * The price of the last trade made for this position. - */ - readonly lastTrade: number; - - /** - * The date and time of the last trade made for this position. - */ - readonly lastTradeTime: Date; - - /** - * The change in the position value. - */ - readonly change: number; - - /** - * The percentage change in the position value. - */ - readonly changePercent: number; - - /** - * The earnings per share of the position. - */ - readonly earningsPerShare: number; - - /** - * The market capitalization of the position. - */ - readonly marketCap: number; - - /** - * The symbol of the position. + * The symbol name of the asset */ readonly symbol: string; /** - * Represents a position in a portfolio. - * @constructor - * @param {number} lastTrade - The price of the last trade made for this position. - * @param {Date} lastTradeTime - The date and time of the last trade made for this position. - * @param {number} change - The change in the position value. - * @param {number} changePercent - The percentage change in the position value. - * @param {number} earningsPerShare - The earnings per share of the position. - * @param {number} marketCap - The market capitalization of the position. - * @param {string} symbol - The symbol of the position. + * The total number of shares, not including open orders */ - constructor( - lastTrade: number, - lastTradeTime: Date, - change: number, - changePercent: number, - earningsPerShare: number, - marketCap: number, - symbol: string - ) { - this.lastTrade = lastTrade; - this.lastTradeTime = lastTradeTime; - this.change = change; - this.changePercent = changePercent; - this.earningsPerShare = earningsPerShare; - this.marketCap = marketCap; + readonly quantity: number; + + /** + * The total dollar amount of the position + */ + readonly marketValue: number; + + /** + * The total cost basis + */ + readonly costBasis: number; + + /** + * The current asset price per share + */ + readonly pricePerShare: number; + + + /** + * Creates a new Portfolio object. + * @constructor + * @param {string} symbol - The symbol of the asset in the portfolio. + * @param {number} quantity - The quantity of the asset in the portfolio. + * @param {number} marketValue - The market value of the asset in the portfolio. + * @param {number} costBasis - The cost basis of the asset in the portfolio. + * @param {number} pricePerShare - The price per share of the asset in the portfolio. + */ + constructor(symbol: string, quantity: number, marketValue: number, costBasis: number, pricePerShare: number) { this.symbol = symbol; + this.quantity = quantity; + this.marketValue = marketValue; + this.costBasis = costBasis; + this.pricePerShare = pricePerShare; } } diff --git a/src/interface/quote.ts b/src/interface/quote.ts index 62b32e0..4449bb8 100644 --- a/src/interface/quote.ts +++ b/src/interface/quote.ts @@ -2,52 +2,20 @@ * Represents a stock quote. */ export class Quote { - /** - * The name of the company associated with this quote. - */ - readonly companyName: string; - - /** - * The earnings per share of a company. - */ - readonly earningsPerShare: number; - - /** - * The estimated earnings for a stock. - */ - readonly estimatedEarnings: number; - - /** - * The price of the last trade for the security. - */ - readonly lastTrade: number; - - /** - * The symbol of the financial instrument being quoted. - */ readonly symbol: string; + readonly askPrice: number; + readonly askSize: number; + readonly bidPrice: number; + readonly bidSize: number; + readonly timeStamp: Date; - /** - * Represents a quote for a particular stock. - * @constructor - * @param {string} companyName - The name of the company associated with the stock. - * @param {number} earningsPerShare - The earnings per share for the stock. - * @param {number} estimatedEarnings - The estimated earnings for the stock. - * @param {number} lastTrade - The last trade price for the stock. - * @param {string} symbol - The symbol for the stock. - */ - constructor( - companyName: string, - earningsPerShare: number, - estimatedEarnings: number, - lastTrade: number, - symbol: string - ) { - this.companyName = companyName; - this.earningsPerShare = earningsPerShare; - this.estimatedEarnings = estimatedEarnings; - this.lastTrade = lastTrade; + constructor(symbol: string, askPrice: number, askSize: number, bidPrice: number, bidSize: number, timeStamp: Date) { this.symbol = symbol; + this.askPrice = askPrice; + this.askSize = askSize; + this.bidPrice = bidPrice; + this.bidSize = bidSize; + this.timeStamp = timeStamp; } } diff --git a/test/alpaca.test.ts b/test/alpaca.test.ts new file mode 100644 index 0000000..a96b15d --- /dev/null +++ b/test/alpaca.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from '@jest/globals'; +import 'dotenv/config'; +import { AlpacaExchange } from '../src/alpaca/exchange'; + +describe('Alpaca Tests', () => { + test('portfolio fetch', () => { + 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(); + }); + + test('quote fetch', () => { + 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(); + }); +}); diff --git a/test/basic.test.ts b/test/basic.test.ts deleted file mode 100644 index 3d83b4e..0000000 --- a/test/basic.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {describe, expect, test} from '@jest/globals'; - -describe('sum module', () => { - test('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); \ No newline at end of file