From f602dfb4ac3c81c94c8a0a73cba10c68ab7db5a6 Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Fri, 13 Jan 2023 15:40:07 +0100 Subject: [PATCH 01/13] PlaceOrderButton: Prevent action on disabled btn --- src/components/molecules/PlaceOrderButton.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/molecules/PlaceOrderButton.tsx b/src/components/molecules/PlaceOrderButton.tsx index a6ec2bd..a619bcc 100644 --- a/src/components/molecules/PlaceOrderButton.tsx +++ b/src/components/molecules/PlaceOrderButton.tsx @@ -81,6 +81,10 @@ const PlaceOrderButton = ({ { + if (disabled) { + return; + } + setDisabled(true); upsertTrade(ticker, { ticker, From 5d2118493e6822efde6555229de0103c16a6c3ac Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Fri, 13 Jan 2023 15:40:22 +0100 Subject: [PATCH 02/13] tradesMeta: make comments into doc comments --- src/db/tradesMeta.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/db/tradesMeta.ts b/src/db/tradesMeta.ts index 4ce8438..71e884b 100644 --- a/src/db/tradesMeta.ts +++ b/src/db/tradesMeta.ts @@ -1,18 +1,26 @@ -// These are also mapped all the way to lowercased and sent to alpacaca (as prop "side") +/** + * These are also mapped all the way to lowercased and sent to alpacaca + * (as prop "side") + */ export enum TRADE_SIDE { BUY = "BUY", SELL = "SELL", } export enum TRADE_STATUS { - READY = "READY", // not even sent to Alpaca yet - ACTIVE = "ACTIVE", // not filled yet - PARTIALLY_FILLED = "PARTIALLY_FILLED", // not totally filled yet - FILLED = "FILLED", // "buy" or "sell" type process, should now be concidered done + /** not even sent to Alpaca yet */ + READY = "READY", + /** not filled yet */ + ACTIVE = "ACTIVE", + /** not totally filled yet */ + PARTIALLY_FILLED = "PARTIALLY_FILLED", + /** "buy" or "sell" type process, should now be concidered done */ + FILLED = "FILLED", OPEN = "OPEN", CLOSED = "CLOSED", CANCELLED = "CANCELLED", - TAKE_PROFIT = "TAKE PROFIT", // order is filled and later resulted in a take-profit order. + /** order is filled and later resulted in a take-profit order. */ + TAKE_PROFIT = "TAKE PROFIT", } export interface TradesDataType { @@ -22,7 +30,8 @@ export interface TradesDataType { price: number; quantity: number; created: number; - breakoutRef: string; // Important! Used as _ref + /** Important! Used as _ref */ + breakoutRef: string; alpacaOrderId?: string; userRef?: string; } From 94190c271b2301db361df29b6c35a7320b4157c4 Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Wed, 18 Jan 2023 11:15:36 +0100 Subject: [PATCH 03/13] Add alpaca-ts (Alpaca TS client) --- package.json | 3 ++- yarn.lock | 68 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ec3d267..e8c45bf 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "typescript": "4.8.4" }, "dependencies": { + "@master-chief/alpaca": "^6.3.20", "@polygon.io/client-js": "^6.0.6", "@sentry/nextjs": "^7.20.0", "@types/node-fetch": "^2.6.2", @@ -55,4 +56,4 @@ "usehooks-ts": "^2.9.1", "zustand": "^4.1.4" } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 46134bd..1db6221 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1132,6 +1132,28 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@master-chief/alpaca@^6.3.20": + version "6.3.20" + resolved "https://registry.yarnpkg.com/@master-chief/alpaca/-/alpaca-6.3.20.tgz#d934fe0c72e10437b1fec1947a689bd8fa1a4cbc" + integrity sha512-Nc9SmvwtnwVxFOVdRpKta2rkppMi0Ivl7bgO6vCWMh2taykH7404gmfYlS3uL/gduPDbWloqJ3TQHFo4F1d1FQ== + dependencies: + "@master-chief/map" "^0.0.6" + abort-controller "^3.0.0" + bottleneck "^2.19.5" + bufferutil "^4.0.3" + eventemitter3 "^4.0.7" + is-blob "^2.1.0" + isomorphic-unfetch "^3.1.0" + isomorphic-ws "^4.0.1" + qs "^6.9.6" + utf-8-validate "^5.0.4" + ws "^7.5.0" + +"@master-chief/map@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@master-chief/map/-/map-0.0.6.tgz#b0d59489448a70eec55fba38ff2f9f926153096f" + integrity sha512-QOLJmzRGkSbwFRgJ2y1Z8Jx1pIcrluWtHaxnpc14KJ6PbumJv/lM1g5Se+HtVbZSqZkQQ4l5Z2wG4uXuk520oQ== + "@next/env@12.3.4": version "12.3.4" resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.4.tgz#c787837d36fcad75d72ff8df6b57482027d64a47" @@ -2174,6 +2196,11 @@ bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bottleneck@^2.19.5: + version "2.19.5" + resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" + integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2235,7 +2262,7 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -bufferutil@^4.0.1: +bufferutil@^4.0.1, bufferutil@^4.0.3: version "4.0.7" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== @@ -3055,6 +3082,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -3695,6 +3727,11 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" +is-blob@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-blob/-/is-blob-2.1.0.tgz#e36cd82c90653f1e1b930f11baf9c64216a05385" + integrity sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw== + is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" @@ -3839,6 +3876,19 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isomorphic-unfetch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" + integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== + dependencies: + node-fetch "^2.6.1" + unfetch "^4.2.0" + +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" @@ -5290,6 +5340,13 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@^6.9.6: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + query-string@^7.0.1: version "7.1.3" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" @@ -6102,6 +6159,11 @@ underscore@~1.13.2: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== +unfetch@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" + integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -6149,7 +6211,7 @@ usehooks-ts@^2.9.1: resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.9.1.tgz#953d3284851ffd097432379e271ce046a8180b37" integrity sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA== -utf-8-validate@^5.0.2: +utf-8-validate@^5.0.2, utf-8-validate@^5.0.4: version "5.0.10" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== @@ -6335,7 +6397,7 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7.4.6: +ws@^7.4.6, ws@^7.5.0: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== From db291bf7f305b711250a00eef46a4d602c735983 Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Wed, 18 Jan 2023 12:17:56 +0100 Subject: [PATCH 04/13] Remove price-range check for buy orders Handled in alpaca with limit rules, and if needed should be set from UI when creating the order --- src/lib/tradesHandler.ts | 79 +++++++++++++++++------------------ src/services/alpacaService.ts | 30 +++++++------ 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/src/lib/tradesHandler.ts b/src/lib/tradesHandler.ts index c6bb1e8..eef0791 100644 --- a/src/lib/tradesHandler.ts +++ b/src/lib/tradesHandler.ts @@ -20,53 +20,38 @@ interface ExtendedTradesDataType extends TradesDataType { sold?: number; } -export const isPriceWithinBuyRange = ( - currentPrice: number, - targetPrice: number, -) => { - // Allow 1% range to trigger buy - return currentPrice > targetPrice * 1.0 && currentPrice < targetPrice * 1.01; -}; - export const triggerBuyOrders = async () => { // Get all "READY" orders: const trades = await getTradesByStatus(TRADE_STATUS.READY); - const promises: Promise[] = []; - trades.forEach((trade) => { - promises.push(getLastTradePrice(trade.ticker)); - }); - const marketPrices = await Promise.all(promises); - const AlpacaTradePromises: Promise[] = []; - trades.forEach((trade, i) => { + + trades.forEach((trade) => { const { price, ticker, quantity } = trade; - const marketPrice = marketPrices[i]; - if (marketPrice && isPriceWithinBuyRange(marketPrice, price)) { - // Send order to alpaca: - AlpacaTradePromises.push( - alpacaService - .postNewBuyOrder(ticker, price, quantity) - .then(async (result) => { - const placed = Date.parse(result.created_at); // result.created_at: '2022-12-05T11:02:02.058370387Z' - console.log("ALPACA ORDER DONE:", result); - await putTrade({ - ...trade, - status: TRADE_STATUS.ACTIVE, - alpacaOrderId: result.id, - placed, - }).catch((e) => { - console.log(e); - }); - }) - .catch((e) => { - console.log(e); - }), - ); - } + + AlpacaTradePromises.push( + alpacaService + .postBuyLimitOrder({ ticker, price, quantity }) + .then((result) => { + const placedTimestamp = Date.parse(result.created_at); + console.log("ALPACA ORDER DONE:", result); + + return putTrade({ + ...trade, + status: TRADE_STATUS.ACTIVE, + alpacaOrderId: result.id, + placed: placedTimestamp, + }); + }) + .catch((e) => { + console.log(e); + }), + ); }); + await Promise.all(AlpacaTradePromises); - return trades.map((t, i) => ({ ...t, marketPrice: marketPrices[i] })); + + return trades; }; export const deleteActiveOrder = async (orderId: string) => { @@ -138,6 +123,9 @@ export const triggerUpdateBuyOrders = async () => { }; export const triggerClearOldBuyOrders = async () => { + // TODO: Get cancelled orders from Alpaca instead and use that to + // update our stored orders? + // Get all "READY" and "ACTIVE" orders: const readyTrades = await getTradesByStatus(TRADE_STATUS.READY); const activeTrades = await getTradesByStatus(TRADE_STATUS.ACTIVE); @@ -165,14 +153,22 @@ export const isStopLossOrder = ( const lastTradePrice = trade.lastTradePrice; if (!lastTradePrice) return false; const movingAvg = trade.movingAvg10; + + // Stop loss case (1) if (trade.price - lastTradePrice >= stopLossLimit) return true; + + // Stop loss case (2) if (movingAvg && lastTradePrice <= movingAvg) return true; // ? <= or < ? + return false; }; -/* After 10% increase in value, we take profit */ +/** After 10% increase in value, we take profit */ const isTakeProfitOrder = (trade: ExtendedTradesDataType) => { - if (trade.status == TRADE_STATUS.TAKE_PROFIT) return false; + if (trade.status == TRADE_STATUS.TAKE_PROFIT) { + return false; + } + const lastTradePrice = trade.lastTradePrice; return lastTradePrice && trade.price * 1.1 <= lastTradePrice; }; @@ -195,6 +191,7 @@ const updateTrade = async (trade: ExtendedTradesDataType) => { const handleTakeProfitOrder = async (trade: ExtendedTradesDataType) => { try { + // TODO: Needed? 50% of 1 => 1 should solve it if (!(trade.quantity > 1)) { void updateTrade({ ...trade, diff --git a/src/services/alpacaService.ts b/src/services/alpacaService.ts index 1621a8d..04c4713 100644 --- a/src/services/alpacaService.ts +++ b/src/services/alpacaService.ts @@ -1,3 +1,5 @@ +import { RawOrder } from "@master-chief/alpaca/@types/entities"; +import { PlaceOrder } from "@master-chief/alpaca/@types/params"; import fetch, { BodyInit } from "node-fetch"; import { getISOStringForToday } from "../lib/helpers"; import { convertResult, handleResult } from "../util"; @@ -23,7 +25,7 @@ const getHoldingInTicker = async (ticker: string) => { }; /* Used for all orders (both with side "buy" and "sell") */ -const postOrder = async (body: BodyInit) => { +const postOrder = async (body: BodyInit): Promise => { try { const res = await fetch( `${brokerApiBaseUrl}/trading/accounts/${accountId}/orders`, @@ -128,21 +130,25 @@ export const takeProfitSellOrder = (symbol: string, totalQuantity: number) => { return postOrder(body); }; -export const postNewBuyOrder = async ( - ticker: string, - price: number, - quantity: number, -) => { - const body: BodyInit = JSON.stringify({ +export const postBuyLimitOrder = async ({ + ticker, + price, + quantity, +}: { + ticker: string; + price: number; + quantity: number; +}) => { + const bodyObject: PlaceOrder = { symbol: ticker, - qty: quantity, // TODO: UNDO OVERRIDING OF QUANTITY: quantity, - side: Side.BUY, - time_in_force: "day", type: "limit", limit_price: price, - }); + side: "buy", + time_in_force: "day", + qty: quantity, + }; - return postOrder(body); + return postOrder(JSON.stringify(bodyObject)); }; export const getOrders = async () => { From f05dc0faa23db38fc2658b02dfb2b9ccf394c7bf Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Wed, 18 Jan 2023 13:05:42 +0100 Subject: [PATCH 05/13] api/broker/orders: cleanup --- src/components/molecules/PlaceOrderButton.tsx | 2 +- src/pages/api/broker/orders/index.ts | 134 ++++++++---------- 2 files changed, 64 insertions(+), 72 deletions(-) diff --git a/src/components/molecules/PlaceOrderButton.tsx b/src/components/molecules/PlaceOrderButton.tsx index a619bcc..7e22734 100644 --- a/src/components/molecules/PlaceOrderButton.tsx +++ b/src/components/molecules/PlaceOrderButton.tsx @@ -92,7 +92,7 @@ const PlaceOrderButton = ({ status: TRADE_STATUS.READY, side: TRADE_SIDE.BUY, }); - /* Note: rn this will not place an actual order. See /api/broker/orders */ + void handleBuyOrder(ticker, buyPrice, quantity, breakoutRef); typeof onClick === "function" && onClick(); }} diff --git a/src/pages/api/broker/orders/index.ts b/src/pages/api/broker/orders/index.ts index b38605a..5bc7d24 100644 --- a/src/pages/api/broker/orders/index.ts +++ b/src/pages/api/broker/orders/index.ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { ResponseDataType } from "../../ResponseDataMeta"; +import { auth } from "../../../../auth/firebaseAdmin"; import { postTrade } from "../../../../db/tradesEntity"; -import { TRADE_STATUS, TRADE_SIDE } from "../../../../db/tradesMeta"; +import { TRADE_SIDE, TRADE_STATUS } from "../../../../db/tradesMeta"; import * as alpacaService from "../../../../services/alpacaService"; -import { auth } from "../../../../auth/firebaseAdmin"; +import { ResponseDataType } from "../../ResponseDataMeta"; interface ExtendedResponseDataType extends ResponseDataType { orders?: Record; @@ -16,78 +16,16 @@ export default async function handler( const { method } = req; try { - const { email } = await auth.verifyIdToken(req.cookies.idToken as string); - const responseData: ExtendedResponseDataType = { status: "INIT" }; + let responseData: ExtendedResponseDataType; switch (method) { case "GET": - await alpacaService - .getOrders() - .then((result) => { - responseData.status = "OK"; - responseData.orders = result; - }) - .catch((e) => { - responseData.status = "NOK"; - responseData.message = e.message; - }); + responseData = await getOrders(); break; case "POST": - const body = JSON.parse(req.body); - const { - ticker, - side, - status, - price, - quantity, - breakoutRef, - }: { - ticker: string; - side: TRADE_SIDE; - status: TRADE_STATUS; - price: number; - quantity: number; - breakoutRef: string; - } = body; - - responseData.status = "OK"; - await postTrade({ - ticker, - side, - status, - price, - quantity, - created: Date.now(), - breakoutRef, - userRef: email, - }).catch((e) => { - console.log(e); - responseData.status = "NOK"; - responseData.message = e.message; - }); - - // const created_at = Date.parse(result.created_at).toString(); // result.created_at: '2022-12-05T11:02:02.058370387Z' - // await alpacaService - // .postNewBuyOrder(ticker, price, quantity) - // .then(async (result) => { - // const alpacaOrderId = result.id; - // const created_at = Date.parse(result.created_at).toString(); // result.created_at: '2022-12-05T11:02:02.058370387Z' - // await handleSaveOrder( - // ticker, - // type, - // status, - // price, - // quantity, - // alpacaOrderId, - // created_at, - // breakoutRef, - // ); - // responseData.status = "OK"; - // }) - // .catch((e) => { - // console.log(e); - // responseData.status = "NOK"; - // responseData.message = e.message; - // }); + const { email } = await auth.verifyIdToken( + req.cookies.idToken as string, + ); + responseData = await createNewTrade(req, email); break; default: throw new Error(`Unsupported method: ${method as string}`); @@ -107,3 +45,57 @@ export default async function handler( }); } } + +async function createNewTrade(req: NextApiRequest, email?: string) { + const responseData: ExtendedResponseDataType = { status: "INIT" }; + const body = JSON.parse(req.body); + const { + ticker, + side, + status, + price, + quantity, + breakoutRef, + }: { + ticker: string; + side: TRADE_SIDE; + status: TRADE_STATUS; + price: number; + quantity: number; + breakoutRef: string; + } = body; + + responseData.status = "OK"; + + await postTrade({ + ticker, + side, + status, + price, + quantity, + created: Date.now(), + breakoutRef, + userRef: email, + }).catch((e) => { + console.log(e); + responseData.status = "NOK"; + responseData.message = e.message; + }); + + return responseData; +} + +async function getOrders() { + const responseData: ExtendedResponseDataType = { status: "INIT" }; + await alpacaService + .getOrders() + .then((result) => { + responseData.status = "OK"; + responseData.orders = result; + }) + .catch((e) => { + responseData.status = "NOK"; + responseData.message = e.message; + }); + return responseData; +} From 03933525f9a74fb8544fc30bc77239f08afff083 Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Wed, 18 Jan 2023 13:52:03 +0100 Subject: [PATCH 06/13] tradesHandler: Refactor triggerUpdateOpenBuyOrders --- src/lib/tradesHandler.ts | 86 +++++++++++-------- .../api/triggers/trigger-update-orders.ts | 4 +- src/services/alpacaService.ts | 5 +- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/src/lib/tradesHandler.ts b/src/lib/tradesHandler.ts index eef0791..71f6333 100644 --- a/src/lib/tradesHandler.ts +++ b/src/lib/tradesHandler.ts @@ -2,16 +2,15 @@ import { deleteTrade, getTradeByOrderId, getTradesByStatus, - postTrade, putTrade, } from "../db/tradesEntity"; -import { TradesDataType, TRADE_STATUS, TRADE_SIDE } from "../db/tradesMeta"; +import { TradesDataType, TRADE_STATUS } from "../db/tradesMeta"; +import { AlpacaOrderStatusType } from "../services/alpacaMeta"; +import * as alpacaService from "../services/alpacaService"; import { getLastTradePrice, getSimpleMovingAverage, } from "../services/polygonService"; -import * as alpacaService from "../services/alpacaService"; -import { AlpacaOrderStatusType } from "../services/alpacaMeta"; import { isToday } from "./helpers"; interface ExtendedTradesDataType extends TradesDataType { @@ -66,7 +65,7 @@ export const deleteActiveOrder = async (orderId: string) => { } }; -export const triggerUpdateBuyOrders = async () => { +export const triggerUpdateOpenBuyOrders = async () => { // Get all "ACTIVE" & "PARTIALLY_FILLED" orders: const activeTrades = await getTradesByStatus(TRADE_STATUS.ACTIVE); const partiallyFilledTrades = await getTradesByStatus( @@ -74,51 +73,62 @@ export const triggerUpdateBuyOrders = async () => { ); const orderIds = activeTrades.map(({ alpacaOrderId }) => alpacaOrderId); + + // TODO: Why today? Why not all by status open? const orders = await alpacaService.getTodaysOrders(); - const TradesPromises: Promise[] = []; + const updateTradesPromises: Promise[] = []; + activeTrades.forEach((trade) => { - const existingTrades = orders.find( - ({ id }: { id: string }) => id === trade.alpacaOrderId, - ); - if (existingTrades.status === AlpacaOrderStatusType.FILLED) { - TradesPromises.push( - putTrade({ - ...trade, - status: TRADE_STATUS.FILLED, - }).catch((e) => { - console.log(e); - }), - ); - } else if ( - existingTrades.status === AlpacaOrderStatusType.PARTIALLY_FILLED - ) { - TradesPromises.push( - putTrade({ - ...trade, - status: TRADE_STATUS.PARTIALLY_FILLED, - }).catch((e) => { - console.log(e); - }), + const alpacaOrder = orders.find(({ id }) => id === trade.alpacaOrderId); + + if (!alpacaOrder) { + console.error( + "Order " + trade.breakoutRef + " has no corresponding order in Alpaca", ); + + return; } + + let newStatus: TRADE_STATUS; + + // TODO: what about canceled orders? + if (alpacaOrder.status === AlpacaOrderStatusType.FILLED) { + newStatus = TRADE_STATUS.FILLED; + } else if (alpacaOrder.status === AlpacaOrderStatusType.PARTIALLY_FILLED) { + newStatus = TRADE_STATUS.PARTIALLY_FILLED; + } else { + return; + } + + updateTradesPromises.push( + putTrade({ ...trade, status: newStatus }).catch((e) => { + console.log(e); + }), + ); }); + partiallyFilledTrades.forEach((trade) => { - const existingTrades = orders.find( - ({ id }: { id: string }) => id === trade.alpacaOrderId, - ); - if (existingTrades.status === AlpacaOrderStatusType.FILLED) { - TradesPromises.push( - putTrade({ - ...trade, - status: TRADE_STATUS.FILLED, - }).catch((e) => { + const alpacaOrder = orders.find(({ id }) => id === trade.alpacaOrderId); + + if (!alpacaOrder) { + console.error( + "Order " + trade.breakoutRef + " has no corresponding order in Alpaca", + ); + + return; + } + + if (alpacaOrder.status === AlpacaOrderStatusType.FILLED) { + updateTradesPromises.push( + putTrade({ ...trade, status: TRADE_STATUS.FILLED }).catch((e) => { console.log(e); }), ); } }); - await Promise.all(TradesPromises); + + await Promise.all(updateTradesPromises); return { activeTrades, partiallyFilledTrades, orderIds, orders }; }; diff --git a/src/pages/api/triggers/trigger-update-orders.ts b/src/pages/api/triggers/trigger-update-orders.ts index ab4a923..35ccfee 100644 --- a/src/pages/api/triggers/trigger-update-orders.ts +++ b/src/pages/api/triggers/trigger-update-orders.ts @@ -1,5 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { triggerUpdateBuyOrders } from "../../../lib/tradesHandler"; +import { triggerUpdateOpenBuyOrders } from "../../../lib/tradesHandler"; type ResponseDataType = { status: string; @@ -16,7 +16,7 @@ export default async function handler( const responseData: ResponseDataType = { status: "INIT" }; switch (method) { case "GET": - await triggerUpdateBuyOrders() + await triggerUpdateOpenBuyOrders() .then((results) => { responseData.status = "OK"; responseData.meta = results; diff --git a/src/services/alpacaService.ts b/src/services/alpacaService.ts index 04c4713..ed3566a 100644 --- a/src/services/alpacaService.ts +++ b/src/services/alpacaService.ts @@ -1,9 +1,8 @@ -import { RawOrder } from "@master-chief/alpaca/@types/entities"; +import { Order, RawOrder } from "@master-chief/alpaca/@types/entities"; import { PlaceOrder } from "@master-chief/alpaca/@types/params"; import fetch, { BodyInit } from "node-fetch"; import { getISOStringForToday } from "../lib/helpers"; import { convertResult, handleResult } from "../util"; -import { Side } from "./alpacaMeta"; const { ALPACA_API_KEY_ID = "[NOT_DEFINED_IN_ENV]", @@ -167,7 +166,7 @@ export const getOrders = async () => { } }; -export const getTodaysOrders = async () => { +export const getTodaysOrders = async (): Promise => { try { const res = await fetch( `${brokerApiBaseUrl}/trading/accounts/${accountId}/orders?status=all&after=${getISOStringForToday()}`, From 2711bc819060179d85ff2973900bce4cf6baf6f8 Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Wed, 18 Jan 2023 14:36:42 +0100 Subject: [PATCH 07/13] tradesHandler: only clear BUY trades --- src/lib/tradesHandler.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/lib/tradesHandler.ts b/src/lib/tradesHandler.ts index 71f6333..9aa1130 100644 --- a/src/lib/tradesHandler.ts +++ b/src/lib/tradesHandler.ts @@ -4,7 +4,7 @@ import { getTradesByStatus, putTrade, } from "../db/tradesEntity"; -import { TradesDataType, TRADE_STATUS } from "../db/tradesMeta"; +import { TradesDataType, TRADE_SIDE, TRADE_STATUS } from "../db/tradesMeta"; import { AlpacaOrderStatusType } from "../services/alpacaMeta"; import * as alpacaService from "../services/alpacaService"; import { @@ -133,9 +133,6 @@ export const triggerUpdateOpenBuyOrders = async () => { }; export const triggerClearOldBuyOrders = async () => { - // TODO: Get cancelled orders from Alpaca instead and use that to - // update our stored orders? - // Get all "READY" and "ACTIVE" orders: const readyTrades = await getTradesByStatus(TRADE_STATUS.READY); const activeTrades = await getTradesByStatus(TRADE_STATUS.ACTIVE); @@ -143,12 +140,12 @@ export const triggerClearOldBuyOrders = async () => { // Delete all old ones: const promises: Promise[] = []; readyTrades.forEach((trade) => { - if (!isToday(trade.created)) { + if (!isToday(trade.created) && trade.side === TRADE_SIDE.BUY) { promises.push(deleteTrade(trade.breakoutRef)); } }); activeTrades.forEach((trade) => { - if (!isToday(trade.created)) { + if (!isToday(trade.created) && trade.side === TRADE_SIDE.BUY) { promises.push(deleteTrade(trade.breakoutRef)); } }); From 2c84a14e2e1b2046d6e3833dbf0eced6f3a4ad10 Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Wed, 18 Jan 2023 15:03:34 +0100 Subject: [PATCH 08/13] helpers: Add isValidSymbol --- src/lib/helpers.ts | 9 +++++++++ src/services/alpacaService.ts | 22 +++------------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 54c577b..015f0f3 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -71,3 +71,12 @@ export const getNewRunId = () => { nowHours, )}${addZero(nowMinutes)}${addZero(nowSeconds)}`; }; + +export function isValidSymbol(symbol: string): boolean { + return Boolean( + symbol && + typeof symbol === "string" && + symbol.length >= 1 && + symbol.length <= 5, + ); +} diff --git a/src/services/alpacaService.ts b/src/services/alpacaService.ts index ed3566a..14d20be 100644 --- a/src/services/alpacaService.ts +++ b/src/services/alpacaService.ts @@ -62,13 +62,7 @@ const getAssetByTicker = async (ticker: string) => { export const closeOpenPosition = async (symbol: string, percentage: string) => { try { - if ( - !symbol || - typeof symbol !== "string" || - symbol.length < 2 || - symbol.length > 5 || - !percentage - ) { + if (!percentage || !isValidSymbol(symbol)) { throw Error; } const res = await fetch( @@ -89,12 +83,7 @@ export const closeOpenPosition = async (symbol: string, percentage: string) => { /* Closes the position (sells 100%). */ export const stopLossSellOrder = async (symbol: string) => { - if ( - !symbol || - typeof symbol !== "string" || - symbol.length < 2 || - symbol.length > 5 - ) { + if (!isValidSymbol(symbol)) { throw Error; } @@ -104,12 +93,7 @@ export const stopLossSellOrder = async (symbol: string) => { /* This is triggered when price has went up with 10% or more. */ export const takeProfitSellOrder = (symbol: string, totalQuantity: number) => { - if ( - !symbol || - typeof symbol !== "string" || - symbol.length < 2 || - symbol.length > 5 - ) { + if (!isValidSymbol(symbol)) { throw Error; } From 3a9c41f6f657285672c56d09c6b9119b2e081336 Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Fri, 20 Jan 2023 09:44:39 +0100 Subject: [PATCH 09/13] Fix sell logic to put sell orders correctly Also fixes some other minor problems, naming, etc. --- src/lib/tradesHandler.ts | 24 +++++++++-------------- src/services/alpacaService.ts | 36 ++++++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/lib/tradesHandler.ts b/src/lib/tradesHandler.ts index 9aa1130..f1001c2 100644 --- a/src/lib/tradesHandler.ts +++ b/src/lib/tradesHandler.ts @@ -171,7 +171,8 @@ export const isStopLossOrder = ( }; /** After 10% increase in value, we take profit */ -const isTakeProfitOrder = (trade: ExtendedTradesDataType) => { +const isTakePartialProfit = (trade: ExtendedTradesDataType) => { + // TODO: Filter these out earlier if (trade.status == TRADE_STATUS.TAKE_PROFIT) { return false; } @@ -198,23 +199,13 @@ const updateTrade = async (trade: ExtendedTradesDataType) => { const handleTakeProfitOrder = async (trade: ExtendedTradesDataType) => { try { - // TODO: Needed? 50% of 1 => 1 should solve it - if (!(trade.quantity > 1)) { - void updateTrade({ - ...trade, - status: TRADE_STATUS.CLOSED, - sold: Date.now(), - }); - void alpacaService.stopLossSellOrder(trade.ticker); - return; - } const result = await alpacaService.takeProfitSellOrder( trade.ticker, trade.quantity, ); await putTrade({ ...depopulateTrade(trade), - quantity: trade.quantity - result.qty, + quantity: trade.quantity - parseInt(result.qty), status: TRADE_STATUS.TAKE_PROFIT, }); } catch (e) { @@ -231,14 +222,15 @@ export const performActions = ( trades.forEach((trade) => { const { ticker, breakoutRef } = trade; if (isStopLossOrder(trade, stopLossLimit)) { - void alpacaService.stopLossSellOrder(trade.ticker); + void alpacaService.stopLossSellOrder(trade.ticker, trade.quantity); messageArray.push(`Stop loss ${ticker}: breakoutRef: ${breakoutRef}`); + void updateTrade({ ...trade, - status: TRADE_STATUS.CLOSED, + status: TRADE_STATUS.CLOSED, // TODO: change to more specific sold: Date.now(), }); - } else if (isTakeProfitOrder(trade)) { + } else if (isTakePartialProfit(trade)) { void handleTakeProfitOrder(trade); messageArray.push(`Take profit ${ticker}: breakoutRef: ${breakoutRef}`); } @@ -269,7 +261,9 @@ export const triggerStopLossTakeProfit = async () => { populateTradesData(filledTrades), alpacaService.getPortfolioValue(), ]); + const stopLossLimit = balance * 0.005; // 0.5% of total value + return performActions(newFilledTrades, stopLossLimit); } catch (e) { console.log(e); diff --git a/src/services/alpacaService.ts b/src/services/alpacaService.ts index 14d20be..4984ce1 100644 --- a/src/services/alpacaService.ts +++ b/src/services/alpacaService.ts @@ -1,8 +1,9 @@ import { Order, RawOrder } from "@master-chief/alpaca/@types/entities"; import { PlaceOrder } from "@master-chief/alpaca/@types/params"; import fetch, { BodyInit } from "node-fetch"; -import { getISOStringForToday } from "../lib/helpers"; +import { getISOStringForToday, isValidSymbol } from "../lib/helpers"; import { convertResult, handleResult } from "../util"; +import { Side } from "./alpacaMeta"; const { ALPACA_API_KEY_ID = "[NOT_DEFINED_IN_ENV]", @@ -82,34 +83,47 @@ export const closeOpenPosition = async (symbol: string, percentage: string) => { }; /* Closes the position (sells 100%). */ -export const stopLossSellOrder = async (symbol: string) => { +export const stopLossSellOrder = async (symbol: string, quantity: number) => { if (!isValidSymbol(symbol)) { throw Error; } console.log(`Stop loss on ${symbol}`); - await deleteOrder(symbol); + + await postSellOrder({ symbol, quantity }); }; -/* This is triggered when price has went up with 10% or more. */ +/** Should sell 50% of position */ export const takeProfitSellOrder = (symbol: string, totalQuantity: number) => { if (!isValidSymbol(symbol)) { throw Error; } - // sell ~50%, floored value to prevent fractional trades. - let quantity = 0; - quantity = Math.floor(totalQuantity * 0.5); + // sell ~50%, ceiled value to prevent fractional trades. + const quantity = Math.ceil(totalQuantity * 0.5); + + console.log(`Take profit on ${symbol}`); + + return postSellOrder({ symbol, quantity }); +}; - const body: BodyInit = JSON.stringify({ - side: "sell", +const postSellOrder = ({ + symbol, + quantity, +}: { + symbol: string; + quantity: number; +}) => { + const params: PlaceOrder = { + side: Side.SELL, symbol: symbol, time_in_force: "day", qty: quantity, type: "market", - }); + }; + + const body: BodyInit = JSON.stringify(params); - console.log(`Take profit on ${symbol}`); return postOrder(body); }; From ae2deb78364c317a606993795a74a4c3c18cb8b4 Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Fri, 20 Jan 2023 10:55:53 +0100 Subject: [PATCH 10/13] Change buy breakout order to use stop order --- src/lib/tradesHandler.ts | 2 +- src/services/alpacaService.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/tradesHandler.ts b/src/lib/tradesHandler.ts index f1001c2..72b7e10 100644 --- a/src/lib/tradesHandler.ts +++ b/src/lib/tradesHandler.ts @@ -30,7 +30,7 @@ export const triggerBuyOrders = async () => { AlpacaTradePromises.push( alpacaService - .postBuyLimitOrder({ ticker, price, quantity }) + .postBuyBreakoutOrder({ ticker, price, quantity }) .then((result) => { const placedTimestamp = Date.parse(result.created_at); console.log("ALPACA ORDER DONE:", result); diff --git a/src/services/alpacaService.ts b/src/services/alpacaService.ts index 4984ce1..a723a0d 100644 --- a/src/services/alpacaService.ts +++ b/src/services/alpacaService.ts @@ -127,7 +127,7 @@ const postSellOrder = ({ return postOrder(body); }; -export const postBuyLimitOrder = async ({ +export const postBuyBreakoutOrder = async ({ ticker, price, quantity, @@ -138,8 +138,8 @@ export const postBuyLimitOrder = async ({ }) => { const bodyObject: PlaceOrder = { symbol: ticker, - type: "limit", - limit_price: price, + type: "stop", + stop_price: price, side: "buy", time_in_force: "day", qty: quantity, From 16c7853a2667a21b31925d3fcf71f0eb3df58b82 Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Fri, 20 Jan 2023 11:20:21 +0100 Subject: [PATCH 11/13] getTradesByStatus: Add support for multiple statuses --- src/db/tradesEntity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/db/tradesEntity.ts b/src/db/tradesEntity.ts index 9971899..a164210 100644 --- a/src/db/tradesEntity.ts +++ b/src/db/tradesEntity.ts @@ -51,9 +51,9 @@ export async function deleteTrade(ref: string) { return; } -export async function getTradesByStatus(status: TRADE_STATUS) { +export async function getTradesByStatus(...status: TRADE_STATUS[]) { const query = db.collection("trades"); - const results = await query.where("status", "==", status).get(); + const results = await query.where("status", "in", status).get(); if (results.size === 0) { return []; } From cf9f0a339d7792b0758c6b5f3652706e63879bea Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Fri, 20 Jan 2023 11:24:06 +0100 Subject: [PATCH 12/13] Update take partial profit check Only perform once, then revert to regular stop-loss procedure --- src/db/tradesMeta.ts | 14 +++++++++++++- src/lib/tradesHandler.ts | 18 +++++++++++------- src/services/alpacaService.ts | 5 ++++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/db/tradesMeta.ts b/src/db/tradesMeta.ts index 71e884b..6d61272 100644 --- a/src/db/tradesMeta.ts +++ b/src/db/tradesMeta.ts @@ -10,17 +10,29 @@ export enum TRADE_SIDE { export enum TRADE_STATUS { /** not even sent to Alpaca yet */ READY = "READY", - /** not filled yet */ + + /** not filled yet; can be cancelled or dead for some other reason in Alpaca */ ACTIVE = "ACTIVE", + /** not totally filled yet */ PARTIALLY_FILLED = "PARTIALLY_FILLED", + /** "buy" or "sell" type process, should now be concidered done */ FILLED = "FILLED", OPEN = "OPEN", CLOSED = "CLOSED", CANCELLED = "CANCELLED", + /** order is filled and later resulted in a take-profit order. */ TAKE_PROFIT = "TAKE PROFIT", + + // TODO: use these + /** order is filled and later resulted in a stop-loss (1) order. */ + STOP_LOSS_1 = "STOP_LOSS_1", + /** order is filled and later resulted in a stop-loss (2) order. */ + STOP_LOSS_2 = "STOP_LOSS_2", + /** order is filled and later resulted in a take partial profit order. */ + TAKE_PARTIAL_PROFIT = "TAKE_PARTIAL_PROFIT", } export interface TradesDataType { diff --git a/src/lib/tradesHandler.ts b/src/lib/tradesHandler.ts index 72b7e10..383c7cb 100644 --- a/src/lib/tradesHandler.ts +++ b/src/lib/tradesHandler.ts @@ -172,8 +172,9 @@ export const isStopLossOrder = ( /** After 10% increase in value, we take profit */ const isTakePartialProfit = (trade: ExtendedTradesDataType) => { - // TODO: Filter these out earlier - if (trade.status == TRADE_STATUS.TAKE_PROFIT) { + if (trade.status === TRADE_STATUS.TAKE_PARTIAL_PROFIT) { + // We only want to do this once; next sell will be a stop-loss to + // sell 100% return false; } @@ -197,16 +198,16 @@ const updateTrade = async (trade: ExtendedTradesDataType) => { } }; -const handleTakeProfitOrder = async (trade: ExtendedTradesDataType) => { +const handleTakePartialProfitOrder = async (trade: ExtendedTradesDataType) => { try { - const result = await alpacaService.takeProfitSellOrder( + const result = await alpacaService.takePartialProfitSellOrder( trade.ticker, trade.quantity, ); await putTrade({ ...depopulateTrade(trade), quantity: trade.quantity - parseInt(result.qty), - status: TRADE_STATUS.TAKE_PROFIT, + status: TRADE_STATUS.TAKE_PARTIAL_PROFIT, }); } catch (e) { console.log(e); @@ -231,7 +232,7 @@ export const performActions = ( sold: Date.now(), }); } else if (isTakePartialProfit(trade)) { - void handleTakeProfitOrder(trade); + void handleTakePartialProfitOrder(trade); messageArray.push(`Take profit ${ticker}: breakoutRef: ${breakoutRef}`); } }); @@ -255,7 +256,10 @@ async function populateTradesData(trades: TradesDataType[]) { export const triggerStopLossTakeProfit = async () => { try { - const filledTrades = await getTradesByStatus(TRADE_STATUS.FILLED); + const filledTrades = await getTradesByStatus( + TRADE_STATUS.FILLED, + TRADE_STATUS.TAKE_PARTIAL_PROFIT, + ); const [newFilledTrades, balance] = await Promise.all([ populateTradesData(filledTrades), diff --git a/src/services/alpacaService.ts b/src/services/alpacaService.ts index a723a0d..5cb7f80 100644 --- a/src/services/alpacaService.ts +++ b/src/services/alpacaService.ts @@ -94,7 +94,10 @@ export const stopLossSellOrder = async (symbol: string, quantity: number) => { }; /** Should sell 50% of position */ -export const takeProfitSellOrder = (symbol: string, totalQuantity: number) => { +export const takePartialProfitSellOrder = ( + symbol: string, + totalQuantity: number, +) => { if (!isValidSymbol(symbol)) { throw Error; } From 2c450252b4200ae6fe9bf8a659fcc875f220434a Mon Sep 17 00:00:00 2001 From: Daniel Molin Date: Fri, 20 Jan 2023 11:38:57 +0100 Subject: [PATCH 13/13] tradesHandler: Add stop-loss case (2), isToday check closes #185 --- src/lib/tradesHandler.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib/tradesHandler.ts b/src/lib/tradesHandler.ts index 383c7cb..86858ef 100644 --- a/src/lib/tradesHandler.ts +++ b/src/lib/tradesHandler.ts @@ -161,12 +161,20 @@ export const isStopLossOrder = ( if (!lastTradePrice) return false; const movingAvg = trade.movingAvg10; + // All of these should sell 100% + // Stop loss case (1) if (trade.price - lastTradePrice >= stopLossLimit) return true; - // Stop loss case (2) - if (movingAvg && lastTradePrice <= movingAvg) return true; // ? <= or < ? + if (!isToday(trade.created)) { + // Stop loss case (2) + if (lastTradePrice <= trade.price) return true; + + // Take profit (1) + if (movingAvg && lastTradePrice <= movingAvg) return true; + } + // TODO: return specific stoploss type return false; };