Home > OS >  Find portfolio balance efficiently (remove nested query)
Find portfolio balance efficiently (remove nested query)

Time:06-16

I could not find an issue like this one on SO. I have seen the following:

None of them help me with the issue I have.

Lets say that I have a system that keeps track of my portfolio of shares. I have two main tables. one with the buy and sell orders that I have made, and the other with share prices for many shares over multiple years.

I am looking for an efficient script that gives me the overall portfolio dollar value for each order I have made over time. The current implementation I am using is far too slow: it executes a sub-query for every order I have. I can sometimes have tens of thousands of orders in the table.

The SQL

I am running this on an Oracle database:

-- Will not have prices on all days (eg weekends and public holidays)
-- One price per day at most for a given ticker
-- This table has hundreds of tickers and hundreds of entries for each ticker
with prices as (
select 'AAPL' ticker, date('2022-01-01') dt, 1.0 price union
select 'AAPL' ticker, date('2022-01-02') dt, 1.1 price union
select 'AAPL' ticker, date('2022-01-03') dt, 1.2 price union
select 'AAPL' ticker, date('2022-01-04') dt, 1.3 price union
select 'AAPL' ticker, date('2022-01-05') dt, 1.1 price union
select 'AAPL' ticker, date('2022-01-06') dt, 1.0 price union
select 'AAPL' ticker, date('2022-01-07') dt, 1.1 price union
select 'GOOG' ticker, date('2022-01-01') dt, 10.3 price union
select 'GOOG' ticker, date('2022-01-02') dt, 10.5 price union
select 'GOOG' ticker, date('2022-01-03') dt, 9.2 price union
select 'GOOG' ticker, date('2022-01-04') dt, 10.1 price union
select 'GOOG' ticker, date('2022-01-05') dt, 11.1 price union
select 'GOOG' ticker, date('2022-01-06') dt, 10.0 price union
select 'GOOG' ticker, date('2022-01-07') dt, 10.1 price union
select 'MSFT' ticker, date('2022-01-01') dt, 5.0 price union
select 'MSFT' ticker, date('2022-01-02') dt, 5.1 price union
select 'MSFT' ticker, date('2022-01-03') dt, 4.2 price union
select 'MSFT' ticker, date('2022-01-04') dt, 6.3 price union
select 'MSFT' ticker, date('2022-01-05') dt, 5.1 price union
select 'MSFT' ticker, date('2022-01-06') dt, 4.9 price union
select 'MSFT' ticker, date('2022-01-07') dt, 5.3 price
)

-- Can have more than 1 order on the same day and same ticker
-- Can have multiple orders for multiple tickers on the same day
-- This table can have tens of thousands of orders but usually less than 20 tickers
, orders as (
select 'AAPL' ticker, date('2022-01-02') dt, 'Buy' type, 1000 shares union
select 'GOOG' ticker, date('2022-01-02') dt, 'Buy' type, 100 shares union
select 'AAPL' ticker, date('2022-01-04') dt, 'Sell' type, -100 shares union
select 'AAPL' ticker, date('2022-01-04') dt, 'Sell' type, -50 shares union
select 'AAPL' ticker, date('2022-01-05') dt, 'Sell' type, -100 shares union
select 'GOOG' ticker, date('2022-01-05') dt, 'Buy' type, 1 shares
)

, summary as (
select
o.ticker,
o.dt,
p.price share_price,
sum(o.shares) order_shares,
sum(o.shares * p.price) order_dollars,
sum(sum(o.shares)) over(partition by o.ticker order by o.dt) balance_shares,
sum(sum(o.shares)) over(partition by o.ticker order by o.dt) * p.price balance_dollars,
(
select sum(o1.shares * p1.price)
from orders o1
inner join prices p1
on p1.ticker = o1.ticker
and p1.dt = o.dt
where o1.dt <= o.dt
) portfolio_balance_dollars
from orders o
inner join prices p on p.ticker = o.ticker and p.dt = o.dt
group by o.ticker, o.dt, p.price
order by o.dt, o.ticker, p.price
)

select s1.*
from summary s1

Giving an output of

ticker  dt          share_price  order_shares  order_dollars  balance_shares  balance_dollars  portfolio_balance_dollars
------  ----------  -----------  ------------  -------------  --------------  ---------------  -------------------------
AAPL    2022-01-02  1.1          1000          1100.0         1000            1100.0           2150.0                   
GOOG    2022-01-02  10.5         100           1050.0         100             1050.0           2150.0                   
AAPL    2022-01-04  1.3          -150          -195.0         850             1105.0           2115.0                   
AAPL    2022-01-05  1.1          -100          -110.0         750             825.0            1946.1                   
GOOG    2022-01-05  11.1         1             11.1           101             1121.1           1946.1                   

I am looking for a faster way to get portfolio_balance_dollars. One with less runtime complexity.

Alternative

An efficient answer for the above would be first prize. But instead of looking up the share prices of all the relevant shares on each order, it also suffices to use the latest share price for the relevant share found within summary itself. This is fine because there are orders taking place quite often. So the following output will also be good enough:

ticker  dt          share_price  order_shares  order_dollars  balance_shares  balance_dollars  portfolio_balance_dollars
------  ----------  -----------  ------------  -------------  --------------  ---------------  -------------------------
AAPL    2022-01-02  1.1          1000          1100.0         1000            1100.0           2150.0                   
GOOG    2022-01-02  10.5         100           1050.0         100             1050.0           2150.0                   
AAPL    2022-01-04  1.3          -150          -195.0         850             1105.0           2155.0                   
AAPL    2022-01-05  1.1          -100          -110.0         750             825.0            1946.1                   
GOOG    2022-01-05  11.1         1             11.1           101             1121.1           1946.1                   

That is instead of multiplying GOOG share balance with the GOOG share price at 2022-01-04, we instead multiply the share amount by the share price at 2022-01-02 to get a balance of 2155 for 2022-01-02. So it takes the latest available share price shown in summary itself.

db<>fiddle

CodePudding user response:

You can use:

SELECT *
FROM   (
  SELECT t.*,
         SUM(balance_dollars) OVER (PARTITION BY dt)
           AS portfolio_balance_dollars
  FROM   (
    SELECT t.ticker,
           o.dt,
           p.price AS share_price,
           o.shares AS order_shares,
           o.shares * p.price AS order_dollars,
           SUM(o.shares) OVER (PARTITION BY t.ticker ORDER BY o.dt)
             AS balance_shares,
           SUM(o.shares) OVER (PARTITION BY t.ticker ORDER BY o.dt)
             * p.price AS balance_dollars
    FROM   ( SELECT DISTINCT ticker FROM orders ) t
           LEFT OUTER JOIN (
             SELECT ticker,
                    dt,
                    SUM(shares) AS shares
             FROM   orders
             GROUP BY ticker, dt
           ) o
           PARTITION BY (o.dt)
           ON (t.ticker = o.ticker)
           CROSS JOIN LATERAL (
             SELECT p.*
             FROM   prices p
             WHERE  p.ticker = t.ticker
             AND    p.dt <= o.dt
             ORDER BY p.dt DESC
             FETCH FIRST ROW ONLY
           ) p
    ORDER BY
           o.dt,
           t.ticker,
           share_price
  ) t
)
WHERE  order_shares IS NOT NULL;

Which, for the sample data, outputs:

TICKER DT SHARE_PRICE ORDER_SHARES ORDER_DOLLARS BALANCE_SHARES BALANCE_DOLLARS PORTFOLIO_BALANCE_DOLLARS
AAPL 02-JAN-22 1.1 1000 1100 1000 1100 2150
GOOG 02-JAN-22 10.5 100 1050 100 1050 2150
AAPL 04-JAN-22 1.3 -150 -195 850 1105 2115
AAPL 05-JAN-22 1.1 -100 -110 750 825 1946.1
GOOG 05-JAN-22 11.1 1 11.1 101 1121.1 1946.1

db<>fiddle here

  • Related