How Fill Order Affects Average-Cost Realized P&L
Summary
The document compares two ways to calculate position and profit-and-loss figures from stock or futures fills. One method aggregates buys and sells to derive average prices and realized P&L; the other updates an average open price and realized P&L one fill at a time. For the example sequence, the methods report different realized amounts and different average open prices while leaving the same net position.
A follow-up demonstrates that adding a final fill to close the remaining position makes both calculations agree on total realized P&L. This points to a key distinction: an open position’s average-cost basis and the allocation of realized P&L can depend on the accounting convention, while the total after flattening can reconcile. The thread links to an external discussion but does not explain alternative lot-matching rules, fees, contract multipliers, or a universal reporting standard. Its example is a useful prompt to define the P&L convention and compare calculations on fully closed trades.
Key ideas
- Aggregating fills and processing them sequentially can produce different open-position average prices and realized P&L.
- The example shows that both methods retain the same net position despite their differing intermediate results.
- Closing the remaining position makes the two calculations agree on total realized P&L in the example.
- A P&L system should specify its cost-basis and fill-accounting conventions.
Tags
Full text
# Robust way to calculate P&L for stocks/futures trading
# Robust way to calculate P&L for stocks/futures trading
I have implemented a class in C# that will calculate P&L based on this description from TT. However it worries me that it gives different results for the same fills, if you apply the "fill download" calculations vs if you simply add fills (example below).
Is there a clear reference resource for P&L calculation? Google has failed me significantly on this one. I am including the code in case this can give an insight to a bug.
```
public sealed class ProfitLoss
{
public double Realized { get; private set; }
public int Net { get; private set; }
public double AverageOpenPrice { get; private set; }
public ProfitLoss()
{
}
public ProfitLoss(IEnumerable<Tuple<int, double>> initial)
{
int buyQuantity = 0, sellQuantity = 0;
double averageBuyPrice = 0, averageSellPrice = 0;
foreach (var fill in initial)
{
if (fill.Item1 > 0)
{
buyQuantity += fill.Item1;
averageBuyPrice += fill.Item1 * fill.Item2;
}
else if (fill.Item1 < 0)
{
int absQuantity = Math.Abs(fill.Item1);
sellQuantity += absQuantity;
averageSellPrice += absQuantity * fill.Item2;
}
}
if (buyQuantity > 0)
averageBuyPrice /= buyQuantity;
if (sellQuantity > 0)
averageSellPrice /= sellQuantity;
Net = buyQuantity - sellQuantity;
AverageOpenPrice = Net > 0 ? averageBuyPrice : averageSellPrice;
Realized = (averageSellPrice - averageBuyPrice) * Math.Min(buyQuantity, sellQuantity);
}
public void AddFill(int quantity, double price)
{
if (quantity == 0)
throw new ArgumentOutOfRangeException(nameof(quantity), "Quantity must be non-zero.");
if (Math.Sign(Net) != Math.Sign(quantity))
{
int absNet = Math.Abs(Net);
int absQuantity = Math.Abs(quantity);
if (absNet == absQuantity) // flat
{
Realized += (price - AverageOpenPrice) * Net;
AverageOpenPrice = 0;
}
else if (absNet > absQuantity) // decrease
{
Realized += (price - AverageOpenPrice) * -quantity;
}
else // reverse
{
Realized += (price - AverageOpenPrice) * Net;
AverageOpenPrice = price;
}
}
else // increase position
{
AverageOpenPrice = (Net * AverageOpenPrice + quantity * price) / (Net + quantity);
}
Net += quantity;
}
public double FloatingForTheoriticalExit(double exitPrice)
{
return (exitPrice - AverageOpenPrice) * Net;
}
}
```
Example of difference in calculation:
```
new ProfitLoss(new[] {
Tuple.Create(12, 100.0),
Tuple.Create(17, 99.0),
Tuple.Create(-9, 101.0),
Tuple.Create(-4, 105.0),
Tuple.Create(3, 103.0)
}).Dump();
```
Yields:
```
Realized 32.2499999999999
Net 19
AverageOpenPrice 99.75
```
But
```
var calc = new ProfitLoss();
calc.AddFill(12, 100);
calc.AddFill(17, 99);
calc.AddFill(-9, 101);
calc.AddFill(-4, 105);
calc.AddFill(3, 103);
calc.Dump();
```
Yields:
```
Realized 36.6206896551725
Net 19
AverageOpenPrice 99.9800362976407
```
## Answer by georgiosd (score 1)
https://quant.stackexchange.com/a/24371
After @AlexC's suggestion I tried adding fills to flatten the net positioning and the two calculations do indeed return the same output in terms of Realized PnL:
```
new ProfitLoss(new[] {
new Fill(12, 100.0),
new Fill(17, 99.0),
new Fill(-9, 101.0),
new Fill(-4, 105.0),
new Fill(3, 103.0),
new Fill(-19, 100)
}).Dump();
var calc = new ProfitLoss();
calc.AddFill(12, 100);
calc.AddFill(17, 99);
calc.AddFill(-9, 101);
calc.AddFill(-4, 105);
calc.AddFill(3, 103);
calc.AddFill(-19, 100);
calc.Dump();
```
Both of the above yield a Realized PnL of 37.Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.