Iterating through array / chain of open and closed stock positions

Viewed 65

I have an array of stock trades that I want to iterate through and I want the output to be a final position. I am parsing a CSV which provides a list of buys and sells. I want to process each of these and understand what is remaining at the end.

By the end I want to have an array that contains multiple objects, each object describes the final position - I bought some twitter (1 trade - open), I sold the whole position (same trade now marked closed), I then bought some shop (1 trade - open), sold SOME of it (previous trade quantity updated and still open, new trade created with sold quantity and closed), then later I bought some more and then sold all (1 new trade, then close all trades)

Example from CSV

{
  Action: 'Buy',
  Symbol: 'SHOP',
  Quantity: '200',
  ...
},
{
  Action: 'Sell',
  Symbol: 'SHOP',
  Quantity: '200',
  ...
},
{
  Action: 'Buy',
  Symbol: 'SHOP',
  Quantity: '50',
  ...
},
{
  Action: 'Sell',
  Symbol: 'SHOP',
  Quantity: '25',
  ...
},
{
  Action: 'Sell',
  Symbol: 'SHOP',
  Quantity: '25',
  ...
},
{
  Action: 'Buy',
  Symbol: 'SHOP',
  Quantity: '1',
  ...
},

This would convert into:

{
  stockTicker: 'SHOP',
  positionStatus: 'CLOSED',
  quantity: 200,
  ...
},
{
  stockTicker: 'SHOP',
  positionStatus: 'CLOSED',
  quantity: 25,
  ...
},
{
  stockTicker: 'SHOP',
  positionStatus: 'CLOSED',
  quantity: 25,
  ...
},
{
  stockTicker: 'SHOP',
  positionStatus: 'OPEN',
  quantity: 1,
  ...
}

This is pretty straight forward if I create a recursive function. I can iterate through and find all the open positions, I can then iterate again and find all the quantities that match completely and close those trades, but then I need to keep iterating through and looking for partial quantity sales, and then how do I know when to stop? Because eventually there may be no more sells and there may only be open trades remaining - which is correct.

I'm not sure how or when I should decide to stop iterating and looking for matching positions.

2 Answers

You don't need a recursive function for this, you can construct the positions object with one iteration of your trades array.

trades.reduce(...) takes the trades array and constructs an object with keys being the unique symbols in trades, and values being the running total of how many units are held.

The Object.entries and .map portions convert that object to an array of objects for convenience, you might not need that.

const trades = [
{
  Action: 'Buy',
  Symbol: 'SHOP',
  Quantity: '200',
},
{
  Action: 'Sell',
  Symbol: 'SHOP',
  Quantity: '200',
},
{
  Action: 'Buy',
  Symbol: 'SHOP',
  Quantity: '50',
},
{
  Action: 'Sell',
  Symbol: 'SHOP',
  Quantity: '25',
},
{
  Action: 'Sell',
  Symbol: 'SHOP',
  Quantity: '25',
}
];

const positions = Object.entries(trades.reduce((acc, el) => {
  if (!acc[el.Symbol]) acc[el.Symbol] = 0;
  if (el.Action === "Sell") {
    acc[el.Symbol] -= Number(el.Quantity);
  } else {
    acc[el.Symbol] += Number(el.Quantity);
  }
  return acc;
}, {})).map(([Symbol, Quantity]) => ({Symbol, Quantity}));
console.log(positions);

The way I approached it was populating two arrays, one for buys and one for sells. Then looping over both arrays to close whichever positions are closed, and keep a running count of ones that are open but have partial shares sold. I also used the Array.some() operator to short circuit my code after the sell order has been fully executed. Note that this code should work for multiple stock symbols.

Initial Array:

trades = [{
  Action: 'Buy',
  Symbol: 'SHOP',
  Quantity: '200',
},{
  Action: 'Sell',
  Symbol: 'SHOP',
  Quantity: '200',
},{
  Action: 'Buy',
  Symbol: 'SHOP',
  Quantity: '50',
},{
  Action: 'Sell',
  Symbol: 'SHOP',
  Quantity: '25',
},{
  Action: 'Sell',
  Symbol: 'SHOP',
  Quantity: '25',
},{
  Action: 'Buy',
  Symbol: 'SHOP',
  Quantity: '1',
}];

Logic:

var buyTrades = trades
  .filter(trade => trade.Action === 'Buy')
  .map(trade => {
  return {
    stockTicker: trade.Symbol,
    positionStatus: 'OPEN',
    quantity: trade.Quantity,
    quantityLeft: Number(trade.Quantity)
  }
});

var sellTrades = trades
  .filter(trade => trade.Action === 'Sell')
  .map(trade => {
  return {
    stockTicker: trade.Symbol,
    quantity: parseInt(trade.Quantity)
  }
});

sellTrades.forEach(sellTrade => {
  var currentSellQuantity = sellTrade.quantity;
  buyTrades.some(buyTrade => {
    if (buyTrade.positionStatus === 'OPEN' && currentSellQuantity !== 0) {
      if (parseInt(buyTrade.quantityLeft) > currentSellQuantity) {
        buyTrade.quantityLeft = buyTrade.quantityLeft - currentSellQuantity;
        currentSellQuantity = 0;
      } else {
        buyTrade.positionStatus = 'CLOSED';
        currentSellQuantity = currentSellQuantity - buyTrade.quantityLeft;
        delete buyTrade.quantityLeft;
      }
    } 
    return currentSellQuantity === 0;
  });
});

console.log(buyTrades);
Related