1

I am trying to learn some SQL by combining it with my interest in finance. At the moment I am attempting to calculate some portfolio data based on a list of transactions (in SQL Server).

This is my table:

create table StockTransactions 
(
    TransactionID int,
    Instrument varchar(32),
    TransactionType varchar(32),
    Units int,
    Price float,
    TransactionSum as case
        when TransactionType = 'Buy' then (-(Units * Price))
        when TransactionType = 'Sell'  then (Units * Price)
        else 0
        end,
    PurchaseCost float
)

Sample data:

insert into StockTransactions (TransactionID, Instrument, TransactionType, Units, Price)
values 
(1, 'Apple', 'Buy', 10, 120),
(2, 'Microsoft', 'Buy', 20, 290),
(3, 'Apple', 'Buy', 10, 125),
(4, 'Apple', 'Sell', 5, 140),
(5, 'Apple', 'Buy', 10, 130),
(6, 'Apple', 'Sell', 10, 135)

Which results in this table:

TransactionID Instrument TransactionType Units Price TransactionSum PurchaseCost
1 Apple Buy 10 120 -1200 NULL
2 Microsoft Buy 20 290 -5800 NULL
3 Apple Buy 10 125 -1250 NULL
4 Apple Sell 5 140 700 NULL
5 Apple Buy 10 130 -1300 NULL
6 Apple Sell 10 135 1350 NULL

I am trying to calculate the values for the PurchaseCost column. The values I want are:

Row PurchaseCost
1 -1200
2 -5800
3 -2450
4 -1837.5
5 -3137.5
6 -2353.13

The logic is as follows. If a purchase has been made, the TransactionSum should be added to the latest previous PurchaseCost for that instrument. However, when a sale has been made a certain sum should be subtracted.

In transaction four I sell 5 of my 20 Apple shares. Therefor I'd like to subtract 1/4 of the previous PurchaseCost for a new PurchaseCost value of -1837.5.

In transaction six I sell 10 of my 25 Apple shares, and would then like to reduce the PurchaseCost to 2353.13 (60% of the previous value).

I can't seem to figure out how to do this by myself. Any ideas?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Lumac
  • 11
  • 1
  • 1
    FYI, for a column called `Price` the data type `float` is a poor choice. Monetary values should *never* be stored as a floating point number. – Thom A Sep 20 '21 at 15:34
  • It looks like `PurchaseCost` is a running total value. If so that doesn't belong in the table, and would be better off served in a `VIEW`. – Thom A Sep 20 '21 at 15:36
  • Maybe an answer could be derived from [this](https://stackoverflow.com/a/65943630/6748758) except it seems you're looking for FIFO instead of LIFO – SteveC Sep 20 '21 at 16:03
  • Your table needs a date as well (and likely a time component) - you should reconsider the accounting logic to deriving cost in order to calculate profit (or income or whatever term you want to apply). In the real world, people make corrections long to old transactions and add new transactions to old periods. Your current path is overly simplistic if this is intended as a real production-ready system. – SMor Sep 20 '21 at 16:39
  • Thank you for your suggestions! Larnu: I'll change the name of Price in my database and will look into views to see how they could of use in this scenario. SteveC: Actually in my country we use the average cost instead of FIFO or LIFO. So every purchase (as long as I have a position) will have an impact. (Which can be very annoying during tax season.) – Lumac Sep 20 '21 at 17:22
  • SMor: The table above is a bit simplified, I do have dates and a few other columns in my real database. I figured that it would be easier to refer to a TransactionID than a Date, so that's why I added that column. For corrections I probably would need to take Date into account somehow. I'm sure I have made, and will make, many odd choices when working on this project. I have some experience with SQL, but none at all with designing databases/tables, so this is an attempt to learn more. – Lumac Sep 20 '21 at 17:22

1 Answers1

0

You can create store procedure to calculate each row Here, the code to calculate and select using temporary table

IF OBJECT_ID('tempdb..#TempStock') IS NOT NULL DROP TABLE #TempStock

DECLARE @TransId int
DECLARE @TempInst varchar(32)
DECLARE @TempPurchase float
DECLARE @TempUnit int
DECLARE @TempTrans varchar(32)
DECLARE @LastPurchase float
DECLARE @LastUnits int

CREATE TABLE #TempStock (
    TransactionID int,
    Instrument varchar(32),
    Units int,
    PurchaseCost float
)

DECLARE MY_CURSOR CURSOR
FOR SELECT DISTINCT TransactionID 
FROM StockTransactions


OPEN MY_CURSOR
FETCH NEXT FROM MY_CURSOR INTO @TransId
WHILE @@FETCH_STATUS = 0
BEGIN 
    SELECT @TempInst = Instrument, @TempTrans = TransactionType, @TempUnit = Units, @TempPurchase = TransactionSum FROM StockTransactions
    WHERE TransactionID = @TransId

    SELECT  TOP 1 @LastPurchase = PurchaseCost, @LastUnits = Units
    FROM #TempStock
    WHERE Instrument = @TempInst
    ORDER BY TransactionID DESC
    
    IF @@ROWCOUNT = 0
    BEGIN
        SELECT @LastPurchase = 0, @LastUnits = 0 
    END

    IF @TempTrans = 'Buy'
    BEGIN
        INSERT INTO #TempStock (TransactionID, Instrument, Units, PurchaseCost)
        VALUES (
            @TransId,
            @TempInst,
            ISNULL(@LastUnits,0) + ISNULL(@TempUnit,0),
            ISNULL(@LastPurchase,0) + ISNULL(@TempPurchase,0)
        )
    END
    ELSE IF @TempTrans = 'Sell'
    BEGIN
        INSERT INTO #TempStock (TransactionID, Instrument, Units, PurchaseCost)
        VALUES (
            @TransId,
            @TempInst,
            ISNULL(@LastUnits,0) - ISNULL(@TempUnit,0),
            (ISNULL(@LastPurchase,0) * (ISNULL(@LastUnits,0) - ISNULL(@TempUnit,0))) / ISNULL(@LastUnits,0)
        )
    END
    
    FETCH NEXT FROM MY_CURSOR INTO @TransId
END
CLOSE MY_CURSOR
DEALLOCATE MY_CURSOR

SELECT TransactionID, PurchaseCost FROM #TempStock

DROP TABLE #TempStock

You can update the master table from that temporary table

  • Thank you for taking the time to write such a comprehensive answer! I'm learning a lot by trying to understand everything. I have also been playing around with it, using different test data. There seems to be a bug in there, it sometimes picks up PurchaseCost and Units from the wrong instrument. Your code works with my example transactions above, but if I were to insert another Apple transaction at #2 (and change all the following TransactionIDs to +1), the first Microsoft trade at #3 (previously at #2) will be a combination of #1 and #3, despite them not belonging to the same instrument. – Lumac Sep 21 '21 at 14:44
  • i've edited the answer to fix the bug. Just add rowcount to check #tempstock – Mohamad Ilham Sep 22 '21 at 02:39
  • That fixed the problem. Thank you again! – Lumac Sep 23 '21 at 09:02