Search code examples
kdb

FIFO & Unrealised pnl in kdb/q


I'm using the solution reported here to compute fifo pnl. I would however also like to keep track of the unrealised pnl at the end of the day i.e mark to market the portfolio at the close price (given the transactions occur over multiple days and we know the close price).

t:([]id:1+til 6;date:(3#2024.01.02),3#2024.01.03; side:`b`s`b`s`s`b;qty:6 5 4 3 8 7;px:10 11 10 12 9 8f; closePx:13 13 13 14 14 14 );

The idea I have in mind is to run the fifo pnl daily with the caveat that the first "transaction" is the previous day end position to mimic an actual trading book. to compute fifo realised pnl:

t: select from t where date = 2024.01.02;
t:update pos:sums delta from update delta:qty*(1;-1)side=`s from t;
f:{signum[x]*x,{@[(-). z;x;:;abs[y]-sum z 1]}[y;x y]{(x;deltas y&sums x)}[abs where[signum[x]<>signum x y]#x;abs x y]};
t:update fifo:deltas[id!delta;f\[id!delta;id]] from t;
t: update realised_pnl:sum each(id!px)*/:fifo from t;

Then we can compute which transactions were matched and at what price the remaining transactions were executed and finally compute the unrealised PnL

t: update matched:sum each fifo@'/:id from t;
t: update outstanding: delta+matched from t;

eod: select px: outstanding wavg px, qty: sum outstanding, first closePx by date, side from t where outstanding>0;
eod: update unrealisedPnL: ?[side in `b; qty*(closePx-px);-1*qty*(closePx-px)] from eod;

What I'm struggling to do now is how to used this logic for the next day and so on. I'm also open to suggestions on how to keep track of unrealized PnL is some other ways


Solution

  • One possible approach (which only requires your qty/px/side fields) is to keep track of the cost basis (and thus cost avg) of your remaining/unrealised positions. Something like this:

    q)t:([]id:1+til 6;date:(3#2024.01.02),3#2024.01.03; side:`b`s`b`s`s`b;qty:6 5 4 3 8 7;px:6 11 10 12 9 8f; closePx:13 13 13 14 14 14 );
    q)t:update pos:sums delta from update delta:qty*(1;-1)side=`s from t;
    q)cb:{(count[x]+signum[first x]*y*signum z)#reverse[x],y#z};
    q)update costAvg:avg each abs costBasis from update costBasis:cb\["f"$();qty;px*(-1;1)side=`b]from t
    id date       side qty px closePx delta pos costBasis          costAvg
    ----------------------------------------------------------------------
    1  2024.01.02 b    6   6  13      6     6   6 6 6 6 6 6f       6
    2  2024.01.02 s    5   11 13      -5    1   ,6f                6
    3  2024.01.02 b    4   10 13      4     5   6 10 10 10 10f     9.2
    4  2024.01.03 s    3   12 14      -3    2   10 10f             10
    5  2024.01.03 s    8   9  14      -8    -6  -9 -9 -9 -9 -9 -9f 9
    6  2024.01.03 b    7   8  14      7     1   ,8f                8
    

    I've changed some of the numbers in your example to make it more representative. So then you have the avg (weighted) cost of your remanining positions which you can mark to the closePx to get unrealised pnl

    EDIT: If this is too memory hungry for larger quantities then you could use a custom representation of the cost basis to reduce memory footprint as follows:

    q)fmt:{count each group cb[key[x]where value x;y;z]};
    q)update costAvg:{avg abs key[x]where value x}each costBasis from update costBasis:fmt\[{x!x}"f"$();qty;px*(-1;1)side=`b]from t
    id date       side qty px closePx delta pos costBasis costAvg
    -------------------------------------------------------------
    1  2024.01.02 b    6   6  13      6     6   (,6f)!,6  6
    2  2024.01.02 s    5   11 13      -5    1   (,6f)!,1  6
    3  2024.01.02 b    4   10 13      4     5   6 10f!1 4 9.2
    4  2024.01.03 s    3   12 14      -3    2   (,10f)!,2 10
    5  2024.01.03 s    8   9  14      -8    -6  (,-9f)!,6 9
    6  2024.01.03 b    7   8  14      7     1   (,8f)!,1  8