// Copyright 2015 Ivan Popivanov
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package net.tradelib.core;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.logging.Logger;
import com.google.common.collect.TreeBasedTable;
public class Portfolio {
public static final String DEFAULT_NAME = "DefaultPortfolio";
private static final Logger logger = Logger.getLogger(Portfolio.class.getName());
private class Transaction {
public LocalDateTime ts;
public long quantity;
public double price;
public double value;
public double averageCost;
public long positionQuantity;
public double positionAverageCost;
public double grossPnl;
public double netPnl;
public double fees;
public Transaction(LocalDateTime ldt, long q, double p, double f) {
ts = ldt;
quantity = q;
price = p;
fees = f;
value = 0.0;
averageCost = 0.0;
positionQuantity = 0;
positionAverageCost = 0.0;
grossPnl = 0.0;
netPnl = 0.0;
}
public Transaction(LocalDateTime ldt) {
ts = ldt;
quantity = 0;
price = 0.0;
fees = 0.0;
value = 0.0;
averageCost = 0.0;
positionQuantity = 0;
positionAverageCost = 0.0;
grossPnl = 0.0;
netPnl = 0.0;
}
}
// 'Long.Value', 'Short.Value', 'Net.Value', 'Gross.Value', 'Period.Realized.PL', 'Period.Unrealized.PL', 'Gross.Trading.PL', 'Txn.Fees', 'Net.Trading.PL'
private class Summary {
public LocalDateTime ts; // the timestamp for this summary
public double longValue = 0.0;
public double shortValue = 0.0;
public double netValue = 0.0;
public double grossValue = 0.0;
public double txnFees = 0.0;
public double realizedPnl = 0.0; // period realized PnL
public double unrealizedPnl = 0.0; // period unrealized PnL
public double grossPnl = 0.0; // gross trading PnL
public double netPnl = 0.0; // net trading PnL
public Summary(LocalDateTime ts) {
this.ts = ts;
}
}
private TreeMap<LocalDateTime, Summary> summaries;
private class InstrumentData {
public ArrayList<Transaction> transactions;
public ArrayList<PositionPnl> positionPnls;
// The last processed transaction
public int lastTxn;
// The last prices seen for this instrument. Needed to maintain
// the running position PnL. A price must be recorded before a
// transaction occurs.
public double lastPrice;
// The PnL for this instrument - computed accumulatively.
public Pnl pnl;
public InstrumentData() {
transactions = new ArrayList<Transaction>();
transactions.add(new Transaction(LocalDateTime.MIN));
lastTxn = 0;
lastPrice = Double.NaN;
pnl = new Pnl();
positionPnls = new ArrayList<PositionPnl>();
positionPnls.add(new PositionPnl());
}
}
String name_;
HashMap<String, InstrumentData> instrumentMap;
public Portfolio(String name) {
this.name_ = name;
this.instrumentMap = new HashMap<String, InstrumentData>();
this.summaries = new TreeMap<LocalDateTime, Portfolio.Summary>();
}
public Portfolio() {
this.name_ = DEFAULT_NAME;
this.instrumentMap = new HashMap<String, InstrumentData>();
this.summaries = new TreeMap<LocalDateTime, Portfolio.Summary>();
}
public void addInstrument(Instrument i) {
InstrumentData icb = instrumentMap.get(i.getSymbol());
if(icb == null) {
instrumentMap.put(i.getSymbol(), new InstrumentData());
}
}
public void addTransaction(Instrument i, LocalDateTime ldt, long q, double p, double f) {
InstrumentData id = instrumentMap.get(i.getSymbol());
if(id == null) {
id = new InstrumentData();
instrumentMap.put(i.getSymbol(), id);
}
ArrayList<Transaction> instrumentTransactions = id.transactions;
assert ldt.isAfter(instrumentTransactions.get(instrumentTransactions.size()-1).ts) :
"Transactions must be added in chronological order [" +
ldt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:mm nnn")) + " " +
instrumentTransactions.get(instrumentTransactions.size()-1).ts.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:mm nnn")) + "]";
Transaction transaction = new Transaction(ldt, q, p, f);
long ppq = instrumentTransactions.get(instrumentTransactions.size()-1).positionQuantity;
if(ppq != 0 && ppq != -transaction.quantity && Long.signum(ppq + transaction.quantity) != Long.signum(ppq)) {
// Split the transaction into two, first add the zero-ing transaction
double perUnitFee = transaction.fees / Math.abs(transaction.quantity);
addTransaction(i, transaction.ts, -ppq, transaction.price, perUnitFee * Math.abs(ppq));
// Adjust the inputs to reflect what's left to transact, increase the date
// time by a microsecond to keep the uniqueness in the transaction set.
transaction.ts = transaction.ts.plusNanos(1000);
transaction.quantity += ppq;
ppq = 0;
transaction.fees = perUnitFee * Math.abs(transaction.quantity);
}
// Transaction value, gross of fees
transaction.value = transaction.quantity * transaction.price * i.getBpv();
// Transaction average cost
transaction.averageCost = transaction.value / (transaction.quantity * i.getBpv());
// Calculate the new quantity for this position
transaction.positionQuantity = ppq + transaction.quantity;
// Previous position average cost
double ppac = instrumentTransactions.get(instrumentTransactions.size()-1).positionAverageCost;
// Calculate position average cost
if (transaction.positionQuantity == 0) {
transaction.positionAverageCost = 0.0;
} else if(Math.abs(ppq) > Math.abs(transaction.positionQuantity)) {
transaction.positionAverageCost = ppac;
} else {
transaction.positionAverageCost = (ppq * ppac * i.getBpv() + transaction.value) /
(transaction.positionQuantity * i.getBpv());
}
// Calculate PnL
if(Math.abs(ppq) < Math.abs(transaction.positionQuantity) || ppq == 0) {
transaction.grossPnl = 0.0;
} else {
transaction.grossPnl = transaction.quantity * i.getBpv() * (ppac - transaction.averageCost);
}
transaction.netPnl = transaction.grossPnl + transaction.fees;
// logger_.info("Portfolio: adding transaction at " + transaction.ts.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) +
// ", previous transaction at " + transactions.get(transactions.size()-1).ts.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
assert transaction.ts.isAfter(instrumentTransactions.get(instrumentTransactions.size()-1).ts) :
"Transactions must be added in chronological order [" +
transaction.ts.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:mm nnn")) + " " +
instrumentTransactions.get(instrumentTransactions.size()-1).ts.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:mm nnn")) + "]";
instrumentTransactions.add(transaction);
}
public void addTransaction(Execution e) {
addTransaction(e.getInstrument(), e.getDateTime(), e.getQuantity(), e.getPrice(), e.getFees());
}
public void markSummary(PositionPnl posPnl) {
Summary ss = summaries.get(posPnl.ts);
if(ss == null) {
ss = new Summary(posPnl.ts);
}
ss.grossValue += Math.abs(posPnl.positionValue);
ss.netValue += posPnl.positionValue;
if(posPnl.positionValue > 0.0) ss.longValue += posPnl.positionValue;
else if(posPnl.positionValue < 0.0) ss.shortValue += posPnl.positionValue;
ss.realizedPnl += posPnl.realizedPnl;
ss.unrealizedPnl += posPnl.unrealizedPnl;
ss.grossPnl += posPnl.grossPnl;
ss.netPnl += posPnl.netPnl;
ss.txnFees += posPnl.fees;
summaries.put(posPnl.ts, ss);
}
public List<PositionPnl> mark(Instrument instrument, LocalDateTime ts, double price) {
InstrumentData idata = instrumentMap.get(instrument.getSymbol());
if(idata == null) {
idata = new InstrumentData();
instrumentMap.put(instrument.getSymbol(), idata);
}
if(idata.transactions.size() == 1) {
// No real transactions, record the price and return
idata.lastPrice = price;
return null;
}
if(Double.isNaN(idata.lastPrice)) {
throw new IllegalStateException(
"Portfolio::mark must be called with a valid price before recording a transaction!");
}
ArrayList<PositionPnl> result = new ArrayList<PositionPnl>();
List<Transaction> txns = idata.transactions;
PositionPnl lastPnl = idata.positionPnls.get(idata.positionPnls.size() - 1);
// Only the last transaction can have the same timestamp as the price.
int txnId = idata.lastTxn + 1;
while(txnId < txns.size() && idata.transactions.get(txnId).ts.isBefore(ts)) {
Transaction txn = txns.get(txnId);
PositionPnl posPnl = new PositionPnl(txn.ts);
posPnl.positionQuantity = txn.positionQuantity;
posPnl.positionAverageCost = txn.positionAverageCost;
posPnl.transactionValue = txn.value;
posPnl.fees = txn.fees;
// Use the previous price
posPnl.positionValue = instrument.getBpv() * posPnl.positionQuantity * idata.lastPrice;
posPnl.grossPnl = posPnl.positionValue - lastPnl.positionValue - posPnl.transactionValue;
posPnl.realizedPnl = txn.grossPnl;
posPnl.unrealizedPnl = posPnl.grossPnl - txn.grossPnl;
posPnl.netPnl = posPnl.grossPnl + txn.fees;
// Accumulate the unrealized PnL
idata.pnl.add(posPnl.realizedPnl, posPnl.unrealizedPnl);
assert posPnl.ts.isAfter(lastPnl.ts);
idata.positionPnls.add(posPnl);
result.add(posPnl);
lastPnl = posPnl;
markSummary(posPnl);
++txnId;
}
// Process the price
if(txnId < txns.size() && idata.transactions.get(txnId).ts.equals(ts)) {
// A transaction with a same timestamp as the price
Transaction txn = txns.get(txnId);
PositionPnl posPnl = new PositionPnl(txn.ts);
// The current time is both in the price list and in the transaction list
posPnl.positionQuantity = txn.positionQuantity;
posPnl.positionAverageCost = txn.positionAverageCost;
posPnl.transactionValue = txn.value;
posPnl.fees = txn.fees;
posPnl.positionValue = instrument.getBpv() * posPnl.positionQuantity * price;
posPnl.grossPnl = posPnl.positionValue - lastPnl.positionValue - posPnl.transactionValue;
posPnl.realizedPnl = txn.grossPnl;
posPnl.unrealizedPnl = posPnl.grossPnl - txn.grossPnl;
posPnl.netPnl = posPnl.grossPnl + txn.fees;
// Accumulate the unrealized PnL
idata.pnl.add(posPnl.realizedPnl, posPnl.unrealizedPnl);
assert posPnl.ts.isAfter(lastPnl.ts);
idata.positionPnls.add(posPnl);
result.add(posPnl);
lastPnl = posPnl;
markSummary(posPnl);
++txnId;
} else {
// Create an entry based on the price itself
PositionPnl posPnl = new PositionPnl(ts);
// No position change, only mark for the price
posPnl.positionQuantity = lastPnl.positionQuantity;
posPnl.positionAverageCost = lastPnl.positionAverageCost;
posPnl.transactionValue = 0.0;
posPnl.fees = 0.0;
posPnl.positionValue = instrument.getBpv() * posPnl.positionQuantity * price;
posPnl.grossPnl = posPnl.positionValue - lastPnl.positionValue;
posPnl.realizedPnl = 0.0;
posPnl.unrealizedPnl = posPnl.grossPnl;
posPnl.netPnl = posPnl.grossPnl;
// Accumulate the unrealized PnL
idata.pnl.add(posPnl.realizedPnl, posPnl.unrealizedPnl);
assert posPnl.ts.isAfter(lastPnl.ts);
idata.positionPnls.add(posPnl);
result.add(posPnl);
lastPnl = posPnl;
markSummary(posPnl);
}
// Update the price
idata.lastPrice = price;
idata.lastTxn = txnId - 1;
// Sometimes we split transactions adding a nanosecond to the price. Thus,
// we may have a situation where there are transactions left. We will process
// them next time (alternatively, we may want to continue here).
return result;
}
public void updatePnl(Instrument instrument, TimeSeries<Double> prices) {
InstrumentData id = instrumentMap.get(instrument.getSymbol());
List<Transaction> transactions = id.transactions;
PositionPnl lastPnl = id.positionPnls.get(id.positionPnls.size() - 1);
// We only process prices after the last PnL
int priceId = 0;
while(priceId < prices.size() && prices.getTimestamp(priceId).isBefore(lastPnl.ts)) {
++priceId;
}
// Find qualifying transactions
int txnId = 0;
while(txnId < transactions.size() && !transactions.get(txnId).ts.isAfter(lastPnl.ts)) {
++txnId;
}
boolean hasPrices = priceId < prices.size();
boolean hasTxns = txnId < transactions.size();
while(hasPrices || hasTxns) {
if(hasPrices && hasTxns && prices.getTimestamp(priceId).equals(transactions.get(txnId).ts)) {
Transaction txn = transactions.get(txnId);
PositionPnl ppnl = new PositionPnl(txn.ts);
// The current time is both in the price list and in the transaction list
ppnl.positionQuantity = txn.positionQuantity;
ppnl.positionAverageCost = txn.positionAverageCost;
ppnl.transactionValue = txn.value;
ppnl.fees = txn.fees;
ppnl.positionValue = instrument.getBpv() * ppnl.positionQuantity * prices.get(priceId);
ppnl.grossPnl = ppnl.positionValue - lastPnl.positionValue - ppnl.transactionValue;
ppnl.realizedPnl = txn.grossPnl;
ppnl.unrealizedPnl = ppnl.grossPnl - txn.grossPnl;
ppnl.netPnl = ppnl.grossPnl + txn.fees;
// Accumulate the unrealized PnL
id.pnl.add(ppnl.realizedPnl, ppnl.unrealizedPnl);
id.positionPnls.add(ppnl);
lastPnl = ppnl;
++priceId;
++txnId;
hasPrices = priceId < prices.size();
hasTxns = txnId < transactions.size();
} else if(!hasTxns || prices.getTimestamp(priceId).isBefore(transactions.get(txnId).ts)) {
PositionPnl ppnl = new PositionPnl(prices.getTimestamp(priceId));
// No position change, only mark for the price
ppnl.positionQuantity = lastPnl.positionQuantity;
ppnl.positionAverageCost = lastPnl.positionAverageCost;
ppnl.transactionValue = 0.0;
ppnl.fees = 0.0;
ppnl.positionValue = instrument.getBpv() * ppnl.positionQuantity * prices.get(priceId);
ppnl.grossPnl = ppnl.positionValue - lastPnl.positionValue;
ppnl.realizedPnl = 0.0;
ppnl.unrealizedPnl = ppnl.grossPnl;
ppnl.netPnl = ppnl.grossPnl;
id.positionPnls.add(ppnl);
lastPnl = ppnl;
++priceId;
hasPrices = priceId < prices.size();
} else {
Transaction txn = transactions.get(txnId);
PositionPnl ppnl = new PositionPnl(transactions.get(txnId).ts);
ppnl.positionQuantity = txn.positionQuantity;
ppnl.positionAverageCost = txn.positionAverageCost;
ppnl.transactionValue = txn.value;
ppnl.fees = txn.fees;
// Use the previous price
ppnl.positionValue = instrument.getBpv() * ppnl.positionQuantity * prices.get(priceId - 1);
ppnl.grossPnl = ppnl.positionValue - lastPnl.positionValue - ppnl.transactionValue;
ppnl.realizedPnl = txn.grossPnl;
ppnl.unrealizedPnl = ppnl.grossPnl - txn.grossPnl;
ppnl.netPnl = ppnl.grossPnl + txn.fees;
// Accumulate the unrealized PnL
id.pnl.add(ppnl.realizedPnl, ppnl.unrealizedPnl);
id.positionPnls.add(ppnl);
lastPnl = ppnl;
++txnId;
hasTxns = txnId < transactions.size();
}
}
}
private InstrumentData getInstrumentData(Instrument instrument) {
return instrumentMap.get(instrument.getSymbol());
}
/**
* @brief Obtains the PnL series for an instrument
*
* This call is to be used together with "mark"
*
* @param instrument
* @return The PnL series
*/
Series getPnlSeries(Instrument instrument) {
List<PositionPnl> positionPnls = getInstrumentData(instrument).positionPnls;
Series ss = new Series(1);
// Skip the first PositionPnl - it's artificial
for(int ii = 1; ii < positionPnls.size(); ++ii) {
PositionPnl ppnl = positionPnls.get(ii);
ss.append(ppnl.ts, ppnl.netPnl);
}
return ss;
}
/**
* @brief Computes the PnL for an instrument
*
* Computes the PnL for the specified instrument using the specified prices.
*
* @param[in] instrument - the instrument
* @param[in] prices - the price series for the PnL computation
*/
TimeSeries<Double> getPnlOld(Instrument instrument, TimeSeries<Double> prices)
{
TimeSeries<Double> pnl = new TimeSeries<Double>();
ArrayList<Transaction> transactions = instrumentMap.get(instrument.getSymbol()).transactions;
// Handle the trivial case of no transactions.
if(transactions.size() <= 1) {
pnl.addAll(prices.getTimestamps(), 0.0);
return pnl;
}
// Find the start
int currentTransaction = 1;
int ii = 0;
while(ii < prices.size() && prices.getTimestamp(ii).isBefore(transactions.get(currentTransaction).ts)) {
++ii;
}
// Set the pnl to 0 from beginning of time to the first transaction
pnl.addAll(prices.getTimestamps(ii), 0.0);
if(ii == prices.size()) return pnl;
double previousPositionValue = 0.0;
while(ii < prices.size() && currentTransaction < transactions.size()) {
Transaction transaction = transactions.get(currentTransaction);
// System.out.println("> " + prices.getTimestamp(ii).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + " " + prices.getTimestamp(ii).getNano() + " nanos, " + Integer.toString(ii));
// System.out.println(transaction.ts.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + " " +
// transaction.ts.getNano() + " nanos, id=" + Integer.toString(currentTransaction) + ", quantity = " +
// transaction.quantity);
if(prices.getTimestamp(ii).equals(transaction.ts)) {
// The current time is both in the price list and in the transaction list
double transactionValue = transaction.value;
double positionValue = instrument.getBpv() * transaction.positionQuantity * prices.get(ii);
double pnlValue = positionValue - previousPositionValue - transactionValue + transaction.fees;
pnl.add(prices.getTimestamp(ii), pnlValue);
++ii;
++currentTransaction;
previousPositionValue = positionValue;
} else if(prices.getTimestamp(ii).isBefore(transaction.ts)) {
// Only in the price list - use the previous position
double positionValue = transactions.get(currentTransaction-1).positionQuantity * instrument.getBpv() * prices.get(ii).doubleValue();
pnl.add(prices.getTimestamp(ii), positionValue - previousPositionValue);
++ii;
previousPositionValue = positionValue;
} else {
if(ii > 0) {
// Only in the transaction list - use the previous price
double positionValue = transaction.positionQuantity * instrument.getBpv() * prices.get(ii-1);
double pnlValue = positionValue - previousPositionValue - transaction.value;
pnl.add(transaction.ts, pnlValue);
previousPositionValue = positionValue;
} else {
// No "previous" price - no pnl
pnl.add(transaction.ts, 0.0);
}
++currentTransaction;
}
}
while(ii < prices.size()) {
double positionValue = transactions.get(currentTransaction - 1).positionQuantity * instrument.getBpv() * prices.get(ii).doubleValue();
pnl.add(prices.getTimestamp(ii), positionValue - previousPositionValue);
++ii;
previousPositionValue = positionValue;
}
return pnl;
}
public PositionPnl getPositionPnl(Instrument instrument) {
InstrumentData id = instrumentMap.get(instrument.getSymbol());
return id.positionPnls.get(id.positionPnls.size() - 1);
}
/**
* @brief Computes the PnL for the current position
*
* Computes the PnL for the current position and the specified price. Undefined
* behavior if a position doesn't exist.
*
* A position starts with the first transaction which sets a quantity different than 0.
*
* Uses the gross PnL for all transactions part of this trade.
*
* @param[in] instrument the instrument
* @param[in] price the price to compute the PnL
*/
public Pnl getPositionPnl(Instrument instrument, Double price) {
ArrayList<Transaction> instrumentTransactions = instrumentMap.get(instrument.getSymbol()).transactions;
int ii = instrumentTransactions.size() - 1;
Transaction transaction = instrumentTransactions.get(ii);
assert transaction.positionQuantity != 0 : "Must not be called without a position";
Pnl pnl = new Pnl();
pnl.unrealized = instrument.getBpv() * transaction.positionQuantity * (price.doubleValue() - transaction.positionAverageCost);
// Add all realized pnl
while(transaction.positionQuantity != 0) {
pnl.realized += transaction.grossPnl;
--ii;
transaction = instrumentTransactions.get(ii);
}
return pnl;
}
/**
* @brief Closes the position for a given instrument.
*
* Adds a transaction to close the position for an instrument. This is useful
* at the end of a backtest, in order to include an open position into the
* PnL computations.
*
* @param instrument The instrument.
* @param ldt The timestamp to use to close the position.
* @param price The transaction price.
* @param fees The transaction fees.
*/
public void closePosition(Instrument instrument, LocalDateTime ldt, double price, double fees) {
InstrumentData icb = instrumentMap.get(instrument.getSymbol());
if(icb == null) return;
ArrayList<Transaction> instrumentTransactions = icb.transactions;
if(instrumentTransactions == null || instrumentTransactions.size() <= 1) return;
Transaction transaction = instrumentTransactions.get(instrumentTransactions.size()-1);
if(transaction.quantity != 0) addTransaction(instrument, ldt, -transaction.quantity, price, fees);
}
public TreeBasedTable<LocalDateTime, String, Double> summarize() {
TreeBasedTable<LocalDateTime, String, Double> summary = TreeBasedTable.create();
for(HashMap.Entry<String, InstrumentData> ee : instrumentMap.entrySet()) {
for(PositionPnl pp : ee.getValue().positionPnls) {
// Update Gross.Value
Double grossValue = summary.get(pp.ts, "Gross.Value");
if(grossValue == null) {
summary.put(pp.ts, "Gross.Value", Math.abs(pp.positionValue));
} else {
summary.put(pp.ts, "Gross.Value", grossValue + Math.abs(pp.positionValue));
}
// Update Net.Value
Double netValue = summary.get(pp.ts, "Net.Value");
if(netValue == null) {
summary.put(pp.ts, "Net.Value", pp.positionValue);
} else {
summary.put(pp.ts, "Net.Value", netValue + pp.positionValue);
}
// Update Long.Value
if(pp.positionValue > 0.0) {
Double longValue = summary.get(pp.ts, "Long.Value");
if(longValue == null) {
summary.put(pp.ts, "Long.Value", pp.positionValue);
} else {
summary.put(pp.ts, "Long.Value", longValue + pp.positionValue);
}
}
// Update Short.Value
if(pp.positionValue < 0.0) {
Double shortValue = summary.get(pp.ts, "Short.Value");
if(shortValue == null) {
summary.put(pp.ts, "Short.Value", pp.positionValue);
} else {
summary.put(pp.ts, "Short.Value", shortValue + pp.positionValue);
}
}
// 'Long.Value', 'Short.Value', 'Net.Value', 'Gross.Value', 'Period.Realized.PL', 'Period.Unrealized.PL', 'Gross.Trading.PL', 'Txn.Fees', 'Net.Trading.PL'
for(String column : Arrays.asList("Period.Realized.PL", "Period.Unrealized.PL", "Gross.Trading.PL", "Txn.Fees", "Net.Trading.PL")) {
double value = 0.0;
switch(column) {
case "Period.Realized.PL":
value = pp.realizedPnl;
break;
case "Period.Unrealized.PL":
value = pp.unrealizedPnl;
break;
case "Gross.Trading.PL":
value = pp.grossPnl;
break;
case "Txn.Fees":
value = pp.fees;
break;
case "Net.Trading.PL":
value = pp.netPnl;
break;
}
Double summaryValue = summary.get(pp.ts, column);
if(summaryValue == null) {
summary.put(pp.ts, column, value);
} else {
summary.put(pp.ts, column, summaryValue + value);
}
}
}
}
return summary;
}
/**
* @brief Computes statistics for each trade.
*
* @param[in] instrument the instrument
*/
public List<Trade> getTrades(Instrument instrument) {
List<Trade> list = new ArrayList<Trade>();
InstrumentData icb = instrumentMap.get(instrument.getSymbol());
if(icb == null) return list;
ArrayList<Transaction> transactions = icb.transactions;
if(transactions == null) return list;
int ii = 0;
// Position at the first non-zero quantity
while(true) {
if(ii == transactions.size()) return list; // No meaningful transactions
if(transactions.get(ii).positionQuantity != 0) break; // Found a real transaction
++ii;
}
int jj = ii;
for(++jj; jj < transactions.size() && transactions.get(jj).positionQuantity != 0; ++jj) {
}
if(jj != transactions.size()) ++jj;
// [ii, jj) contains all transactions participating in the current trade
while(true) {
double positionCostBasis = 0.0;
int kk = jj - 1;
Trade ts = new Trade();
ts.start = transactions.get(ii).ts;
ts.end = transactions.get(kk).ts;
ts.initialPosition = transactions.get(ii).quantity;
ts.maxPosition = 0;
ts.numTransactions = 0;
ts.maxNotionalCost = 0.0;
ts.fees = 0.0;
for(int ll = ii; ll < jj; ++ll) {
Transaction transaction = transactions.get(ll);
if(transaction.value != 0.0) ++ts.numTransactions;
positionCostBasis = positionCostBasis + transaction.value;
ts.fees += transaction.fees;
if(Math.abs(transaction.positionQuantity) > Math.abs(ts.maxPosition)) {
ts.maxPosition = transaction.positionQuantity;
ts.maxNotionalCost = positionCostBasis;
}
}
double positionValue = transactions.get(kk).positionQuantity * instrument.getBpv() * transactions.get(kk).price;
ts.pnl = positionValue - positionCostBasis;
ts.pctPnl = ts.pnl/Math.abs(ts.maxNotionalCost);
ts.tickPnl = 0.0;
list.add(ts);
// Advance to the next trade
if(jj == transactions.size()) break;
ii = jj;
for(++jj; jj < transactions.size() && transactions.get(jj).positionQuantity != 0; ++jj) {
}
if(jj != transactions.size()) {
++jj;
} else {
// The last trade is still open - don't add it to the list
break;
}
}
return list;
}
public TradingResults getTradingResults(Instrument instrument) {
TradingResults tr = new TradingResults();
tr.pnl = getPnlSeries(instrument);
tr.stats = getTrades(instrument);
TradeSummaryBuilder shorts = new TradeSummaryBuilder(tr.pnl);
TradeSummaryBuilder longs = new TradeSummaryBuilder(tr.pnl);
TradeSummaryBuilder all = new TradeSummaryBuilder(tr.pnl);
for(Trade ts : tr.stats) {
if(ts.initialPosition > 0) {
all.add(ts);
longs.add(ts);
} else if(ts.initialPosition < 0) {
all.add(ts);
shorts.add(ts);
}
}
tr.all = all.summarize();
tr.longs = longs.summarize();
tr.shorts = shorts.summarize();
return tr;
}
public Iterable<String> symbols() {
return instrumentMap.keySet();
}
public Series getSummary() {
Series result = new Series(9);
for(Summary ss : summaries.values()) {
result.append(ss.ts, ss.longValue, ss.shortValue, ss.netValue,
ss.grossValue, ss.txnFees, ss.realizedPnl, ss.unrealizedPnl,
ss.grossPnl, ss.netPnl);
}
result.setNames("long.value", "short.value", "net.value", "gross.value",
"fees", "realized.pnl", "unrealized.pnl",
"gross.pnl", "net.pnl");
return result;
}
public Series getPositionPnls(Instrument instrument) {
Series result = new Series(9);
List<PositionPnl> posPnls = getInstrumentData(instrument).positionPnls;
// Skip the first PositionPnl - it's artificial
for(int ii = 1; ii < posPnls.size(); ++ii) {
PositionPnl posPnl = posPnls.get(ii);
result.append(posPnl.ts, posPnl.positionQuantity, posPnl.positionValue,
posPnl.positionAverageCost, posPnl.transactionValue,
posPnl.realizedPnl, posPnl.unrealizedPnl,
posPnl.grossPnl, posPnl.netPnl, posPnl.fees);
}
result.setNames("quantity", "value", "avg.cost", "txn.value",
"realized.pnl", "unrealized.pnl", "gross.pnl", "net.pnl", "fees");
return result;
}
/**
* Obtains the accumulative PnL for this instrument. That's the call to use
* to report open equity PnL.
*
* @param instrument
* @return
*/
public Pnl getPnl(Instrument instrument) {
return getInstrumentData(instrument).pnl;
}
}