package com.activequant.backtesting;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.log4j.Logger;
import com.activequant.domainmodel.trade.event.OrderEvent;
import com.activequant.domainmodel.trade.event.OrderFillEvent;
import com.activequant.domainmodel.trade.order.OrderSide;
import com.activequant.timeseries.TSContainer2;
import com.activequant.timeseries.TypedColumn;
/**
* http://www.interactivebrokers.com/de/accounts/fees/commission.php?ib_entity=
* de
*
* Makes a flat commission structure - (no three tier structure).
*
* Highly FX specific at the moment.
*
* @author GhostRider
*
*/
public class IBFXFeeCalculator implements IFeeCalculator {
private Map<String, Double> conversionSheet = new HashMap<String, Double>();
private String accountBaseCurrency = "USD";
private double minimumPerOrder = 2.5;
private double commissionBps = 0.2; //
private double tickSizeAcctCurrency = 0.0001;
private TSContainer2 feeSeries = new TSContainer2("FEES", new ArrayList<String>(), new ArrayList<TypedColumn>());
private Map<String, Double> runningPositions = new HashMap<String, Double>();
private Map<String, Double> avgEntryPrice = new HashMap<String, Double>();
private Logger log = Logger.getLogger(IBFXFeeCalculator.class);
// very dirty and against engineering ethics: nonreusable code below.
private final List<String> rows = new ArrayList<String>();
private DecimalFormat dcf = new DecimalFormat("#.######");
public IBFXFeeCalculator() {
// dump a row.
String row = "REFORDERID;TS_IN_NANOSECONDS;INSTID;";
row += "SIDE;Q;PX;CONVERSION_RATE_TO_USD;TRADED_VAL_IN_QUOTEE;TRADED_VAL_IN_USD;COMMISSION;AVGPX;CURRENTPOS;CLOSEDPNL;";
rows.add(row);
}
public void updateRefRate(String id, Double ref) {
conversionSheet.put(id, ref);
}
public Double getCurrentPos(String tid){
if(runningPositions.containsKey(tid))
return runningPositions.get(tid);
return 0.0;
}
public Double getAvgPx(String tid){
if(avgEntryPrice.containsKey(tid))
return avgEntryPrice.get(tid);
return 0.0;
}
@Override
public void track(OrderEvent orderEvent) {
if (orderEvent instanceof OrderFillEvent) {
//
OrderFillEvent ofe = (OrderFillEvent) orderEvent;
//
String tid = ofe.getOptionalInstId();
double volume = ofe.getFillAmount();
if (volume == 0.0)
return;
//
double execPrice = ofe.getFillPrice();
//
double tradedValueInQuotee = volume * execPrice;
//
if(tid.startsWith("PI_"))
tid = tid.substring(3);
String base = tid.substring(0, 3);
String quotee = tid.substring(3);
//
tid="PI_" + tid;
// doing equally weighted average pricing.
Double currentPos = getCurrentPos(tid);
Double avgPx = getAvgPx(tid);
//
double signedVolume = volume;
if(ofe.getSide().equals(OrderSide.SELL)){
signedVolume = - signedVolume;
}
log.info(tid+": signed volume: " + signedVolume);
//
Double closingTradePnl = 0.0;
if(Math.signum(signedVolume) == Math.signum(currentPos)){
// increase of position
Double newPos = currentPos + signedVolume;
Double newAvgPx = Math.abs(((currentPos * avgPx) + (signedVolume*execPrice))/newPos);
avgPx = newAvgPx;
currentPos = newPos;
//
this.avgEntryPrice.put(tid, newAvgPx);
this.runningPositions.put(tid, currentPos);
//
log.info("Increase of position. New avg entry price and running position for " + tid+": " + newAvgPx+"/"+currentPos);
}
else
{
if(currentPos!=0.0){
// decrease of position.
// By using equally weighted inventory (contrary to FIFO and LIFO), we can keep the average price constant.
Double newPos = currentPos + signedVolume;
currentPos = newPos;
this.runningPositions.put(tid, currentPos);
if(Math.signum(signedVolume)==1.0){
// means we were in a short position and are reducing it.
closingTradePnl = (avgPx - execPrice) * volume;
}
else{
// means we were in a long position and are reducing it.
closingTradePnl = (execPrice - avgPx) * volume;
}
log.info("Decrease of position. new avg entry price and running position for " + tid+": " + avgPx+"/"+currentPos);
}
else{
currentPos = signedVolume;
avgPx = execPrice;
this.avgEntryPrice.put(tid, avgPx);
this.runningPositions.put(tid, currentPos);
log.info("New avg entry price and running position for " + tid+": " + avgPx+"/"+currentPos);
}
}
//
double conversionRate = 1.0;
if (base.equals("USD")) {
conversionRate = 1.0 / execPrice;
} else if (quotee.equals("USD")) {
conversionRate = 1.0;
} else {
conversionRate = getConversionRate(base, quotee, execPrice);
}
//
double tradedValueInUsd = conversionRate * tradedValueInQuotee;
//
double commission = Math.max((0.2 * tickSizeAcctCurrency * tradedValueInUsd), 2.50);
// track it.
Double existingFees = (Double) feeSeries.getValue(tid, ofe.getTimeStamp());
if(existingFees==null)existingFees = 0.0;
feeSeries.setValue(tid, ofe.getTimeStamp(), commission+existingFees);
// dump a row.
String row = ofe.getRefOrderId() + ";" + ofe.getTimeStamp().getNanoseconds() + ";"
+ ofe.getOptionalInstId() + ";";
row += ofe.getSide() + ";" + dcf.format(ofe.getFillAmount()) + ";" + dcf.format(ofe.getFillPrice()) + ";";
row += dcf.format(conversionRate) + ";" + dcf.format(tradedValueInQuotee) + ";"
+ dcf.format(tradedValueInUsd) + ";" + dcf.format(commission)+";"+dcf.format(avgPx)+";"+dcf.format(currentPos)+";"+dcf.format(closingTradePnl);
rows.add(row);
}
}
/**
* iterates over the quote sheets to find the first matching pair to convert
* based on base or quotee to USD.
*
* @param base
* @param quotee
*/
private double getConversionRate(String base, String quotee, Double refQuote) {
//
double ret = 1.0;
Iterator<Entry<String, Double>> it = conversionSheet.entrySet().iterator();
while (it.hasNext()) {
Entry<String, Double> entry = it.next();
String pair = entry.getKey();
Double rate = entry.getValue();
String _base = pair.substring(0, 3);
String _quotee = pair.substring(3);
if (_base.equals("USD")) {
// ok, possible target ...
if (_quotee.equals(base)) {
return (1.0 / rate) / refQuote;
} else if (_quotee.equals(quotee)) {
return 1.0 / rate;
}
} else if (_quotee.equals("USD")) {
// ok, possible target ...
if (_base.equals(base)) {
return rate / refQuote;
} else if (_base.equals(quotee)) {
return rate;
}
}
}
return ret;
}
@Override
public TSContainer2 feesSeries() {
return feeSeries;
}
public List<String> getRows() {
return rows;
}
}