Generating all realized and unrealized trades usin

2019-08-05 15:40发布

问题:

PROBLEM:

Given some positions in different financial securities, I would like to apply trades to them up to a given date. In doing so I would like to keep track which trades closed the position and opened a new one. The position is CLOSED when the Position Quantity becomes 0 after a trade is applied. A position is considered OPENED when the Position Quantity changes from 0 to something.


SETUP: SQL DEMO SOURCE

Let's say I have the following tables:

CREATE TABLE tPosition 
(
    SecNum INT,
    Position INT
)

CREATE TABLE tTrade
(
    TradeID INT IDENTITY(1,1),
    TradeDate DATETIME,
    SecNum INT,
    Quantity INT
)

And some sample data:

INSERT INTO tPosition (SecNum, Position) 
SELECT 1, 100
UNION
SELECT 2, 200
UNION
SELECT 3, -300

INSERT INTO tTrade (TradeID, TradeDate, SecNum, Quantity)
SELECT 1, '1/1/2016', 1, -50
UNION
SELECT 2, '1/2/2016', 1, -50
UNION
SELECT 3, '1/3/2016', 1, -50
UNION
SELECT 4, '1/4/2016', 1, 50
UNION
SELECT 6, '1/5/2016', 3, 200
UNION
SELECT 7, '1/5/2016', 3, 200;

SAMPLE SCENARIOS/CASES: SQL DEMO SOURCE

Without any trades my result would be (i.e. exactly the position table with 2 extra fields which will be useful later):

SecNum, Position, OpenedByTradeID, ClosedByTradeID
    1,     100,       NULL,           NULL
    2,     200,       NULL,           NULL
    3,     -300,      NULL,           NULL

So let's say I apply trades to the positions up to and including 1/1/2016. This trade with TradeID:1, will affect the position for securityID: 1 as 100+(-50)=50 so my result should be:

SecNum, Position, OpenedByTradeID, ClosedByTradeID
    1,     50,       NULL,         NULL
    2,     200,      NULL,         NULL
    3,     -300,     NULL,         NULL

The OpenedByTradeID and ClosedTradeID are still NULL because the position hasn't crossed 0 yet.


If I apply trades up to and including 1/2/2016 I should get:

 SecNum, Position, OpenedByTradeID, ClosedByTradeID
    1,      0,       NULL,             2
    2,     200,      NULL,             NULL
    3,     -300,     NULL,             NULL

Notice that the position has become 0 so we record the ClosedByTradeID with the tradeID that closed this position.


Up to and including 1/3/2016 I should get:

 SecNum, Position, OpenedByTradeID, ClosedByTradeID
    1,      0,       NULL,             2
    1,     -50,       3,               NULL
    2,     200,      NULL,             NULL
    3,     -300,     NULL,             NULL

Notice that a new position was opened in securityID:1 by the latest tradeID:3 so we mark the OpenedByTradeID column with TradeID:3


Up to and including 1/4/2016 I should get:

 SecNum, Position, OpenedByTradeID, ClosedByTradeID
    1,      0,       NULL,              2
    1,      0,       3,                 4
    2,     200,      NULL,              NULL
    3,     -300,     NULL,              NULL

Notice that the position has become 0 so we record the ClosedByTradeID with the tradeID that closed this position -- TradeID:4


Up to and including 1/5/2016 is an edge case. There are 2 things happening here: the application of the trade crosses the 0 mark so a new position needs to be formed AND 2 trades happen on the same day.

Applying:

  • tradeID:6 for securityID:3 would adjust the position to -100 (-300 + 200 = -100).
  • Then we need to apply tradeID:7 but that would cross 0 (-100 + 200 = 100 crossing from neg -> pos) so we need to apply the part before crossing 0 to close the position and then start a new position with the remaining amount.
  • Part of tradeID:7 [100] would adjust the position to 0 (-100 + 100 = 0) and close it (would be noted in ClosedByTradeID column) and then part of tradeID:7 [the remaining 100] would adjust the position to 100 (0 + 100 = 100) [would also get noted in OpenedByTradeID column].

Therefore, I should get:

 SecNum, Position, OpenedByTradeID, ClosedByTradeID
    1,      0,       NULL,              2
    1,      0,       3,                 4
    2,     200,      NULL,              NULL
    3,      0,       NULL,              7
    3,     100,       7,                NULL

This sounds like an islands problem but I just can't seem to figure out how to write this one.

回答1:

This is just a partial work using recursive. I couldnt finish, but maybe others can have ideas.

I start working with only SecNum = 1

SQL DEMO

WITH DirectReports (SecNum, Position, OpenedByTradeID, ClosedByTradeID, level_id, TradeID)
AS (
    SELECT SecNum, Position, NULL as OpenedByTradeID, NULL as ClosedByTradeID, 1, null as TradeID
    FROM tPosition
    WHERE SecNum = 1 
    UNION ALL
    SELECT D.SecNum, 
           Position + Quantity as Position, 
           CASE WHEN Position = 0 and Position + Quantity <> 0 
                THEN T.TradeID
                ELSE NULL
           END as OpenedByTradeID,
           CASE WHEN Position <> 0 and Position + Quantity = 0 
                THEN T.TradeID
                ELSE NULL
           END as ClosedByTradeID,
           level_id + 1 as level_id,
           T.TradeID

    FROM DirectReports D
    JOIN (SELECT  *,
                  ROW_NUMBER() OVER (partition by SecNum ORDER BY TradeDate) as rn
          FROM tTrade                              
         ) T
      ON D.SecNum = T.SecNum     
     AND D.level_id = T.rn
)   
SELECT *
FROM DirectReports

OUTPUT



回答2:

I think I got it to work using the WHILE loop approach. Maybe there is a more efficient way but I think this works...

DECLARE @result TABLE
(
    SecNum INT,
    Position INT,
    OpenedByTradeID INT,
    ClosedByTradeID INT
)

INSERT INTO @result(SecNum, Position)
SELECT SecNum, Position
FROM dbo.tPosition

SELECT *
FROM @result
ORDER BY SecNum

DECLARE @CurTradeID INT
SELECT @CurTradeID = MIN(TradeID) FROM dbo.tTrade
WHILE(@CurTradeID IS NOT NULL) 
BEGIN
     DECLARE @TradeQty INT, @TradeSecNum INT
     SELECT @TradeQty = Quantity, @TradeSecNum = SecNum FROM dbo.tTrade WHERE TradeID = @CurTradeID

     DECLARE @OldPos INT = (SELECT Position FROM @result WHERE ClosedByTradeID IS NULL AND SecNum = @TradeSecNum)

     -- IF THERE IS NO POSITION
     IF (@OldPos IS NULL)
        BEGIN
            INSERT INTO @result(SecNum, Position, OpenedByTradeID, ClosedByTradeID)
            SELECT @TradeSecNum, @TradeQty, @CurTradeID, NULL
        END

    -- IF THIS TRADE CLOSES THE POSITION
     ELSE IF (@OldPos + @TradeQty = 0) 
         BEGIN
            UPDATE @result 
            SET ClosedByTradeID = @CurTradeID, Position = Position + @TradeQty
            WHERE SecNum = @TradeSecNum AND ClosedByTradeID IS NULL
         END

     -- IF THIS TRADE MAKES THE POSITION CROSS THROUGH 0 i.e. IF TRADE MAKES POSITION CHANGE SIGN
     ELSE IF (SIGN(@OldPos + @TradeQty) <> SIGN(@OldPos))
        BEGIN
            DECLARE @RemainingAmt INT = @TradeQty + @OldPos

            UPDATE @result
            SET ClosedByTradeID = @CurTradeID, Position = 0
            WHERE SecNum = @TradeSecNum AND ClosedByTradeID IS NULL

            INSERT INTO @result(SecNum, Position, OpenedByTradeID, ClosedByTradeID)
            SELECT @TradeSecNum, @RemainingAmt, @CurTradeID, NULL
        END

    -- JUST UPDATE THE ACTIVE POSITION
     ELSE        
        BEGIN
            UPDATE @result 
            SET Position = Position + @TradeQty
            WHERE SecNum = @TradeSecNum AND ClosedByTradeID IS NULL
        END

    SELECT @CurTradeID = MIN(TradeID) FROM dbo.tTrade WHERE TradeID > @CurTradeID
END

SELECT *
FROM @result
ORDER BY SecNum

P.S.: I figured that I can make a temp table with the trades that I need before-hand and I can use that table instead of tTrade in my query above so I don't have to keep dealing with selecting the trades up to a particular date.