Skip to content

Instantly share code, notes, and snippets.

@jaymon0703
Created March 9, 2017 19:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jaymon0703/f889b3099cf069fe2d11d5dbf1393d7b to your computer and use it in GitHub Desktop.
Save jaymon0703/f889b3099cf069fe2d11d5dbf1393d7b to your computer and use it in GitHub Desktop.
perTradeStats with added columns Init.Qty and End.Qty
#' calculate trade statistics for round turn trades.
#'
#' One 'trade' is defined as a series of transactions which make up a 'round turn'.
#' It may contain many transactions. This function reports statistics on these
#' round turn trades which may be used on their own or which are also used
#' by other functions, including \code{\link{tradeStats}} and \code{\link{tradeQuantiles}}
#'
#' @details
#' Additional methods of determining 'round turns' are also supported.
#'
#' \strong{Supported Methods for \code{tradeDef}:}
#' \describe{
#' \item{\code{flat.to.flat}}{From the initial transaction that moves the
#' position away from zero to the last transaction that flattens the position
#' make up one round turn trade for the purposes of 'flat to flat' analysis.}
#' \item{\code{flat.to.reduced}}{The \emph{flat.to.reduced} method starts the
#' round turn trade at the same point as \emph{flat.to.flat}, at the first
#' transaction which moves the position from zero to a new open position. The
#' end of each round turn is described by transactions which move the position
#' closer to zero, regardless of any other transactions which may have
#' increased the position along the way.}
#' \item{\code{increased.to.reduced}}{The \emph{increased.to.reduced} method
#' is appropriate for analyzing round turns in a portfolio which is rarely
#' flat, or which regularly adds to and reduces positions. Every transaction
#' which moves the position closer to zero (reduced position) will close a
#' round turn. this closing transaction will be paired with one or more
#' transaction which move the position further from zero to locate the
#' initiating transactions. \code{acfifo} is an alias for this method.}
#' }
#'
#' As with the rest of \code{blotter}, \code{perTradeStats} uses average cost
#' accounting. For the purposes of round turns, the average cost in force is
#' the average cost of the open position at the time of the closing transaction.
#'
#' Note that a trade that is open at the end of the measured period will
#' be marked to the timestamp of the end of the series.
#' If that trade is later closed, the stats for it will likely change.
#' This is 'mark to market' for the open position, and corresponds to
#' most trade accounting systems and risk systems in including the open
#' position in reporting.
#'
#' @param Portfolio string identifying the portfolio
#' @param Symbol string identifying the symbol to examin trades for. If missing, the first symbol found in the \code{Portfolio} portfolio will be used
#' @param includeOpenTrade whether to process only finished trades, or the last trade if it is still open, default TRUE
#' @param tradeDef string, one of 'flat.to.flat', 'flat.to.reduced', 'increased.to.reduced' or 'acfifo'. See Details.
#' @param \dots any other passthrough parameters
#' @author Brian G. Peterson, Jasen Mackie, Jan Humme
#' @references Tomasini, E. and Jaekle, U. \emph{Trading Systems - A new approach to system development and portfolio optimisation} (ISBN 978-1-905641-79-6)
#' @return
#' A \code{data.frame} containing:
#'
#' \describe{
#' \item{Start}{the \code{POSIXct} timestamp of the start of the trade}
#' \item{End}{the \code{POSIXct} timestamp of the end of the trade, when flat}
#' \item{Init.Qty}{the initiating quantity on opening the trade}
#' \item{Init.Pos}{the initial position on opening the trade}
#' \item{Max.Pos}{the maximum (largest) position held during the open trade}
#' \item{End.Qty}{the remaining quantity held after closing the trade}
#' \item{Num.Txns}{ the number of transactions included in this trade}
#' \item{Max.Notional.Cost}{ the largest notional investment cost of this trade}
#' \item{Net.Trading.PL}{ net trading P&L in the currency of \code{Symbol}}
#' \item{MAE}{ Maximum Adverse Excursion (MAE), in the currency of \code{Symbol}}
#' \item{MFE}{ Maximum Favorable Excursion (MFE), in the currency of \code{Symbol}}
#' \item{Pct.Net.Trading.PL}{ net trading P&L in percent of invested \code{Symbol} price gained or lost}
#' \item{Pct.MAE}{ Maximum Adverse Excursion (MAE), in percent}
#' \item{Pct.MFE}{ Maximum Favorable Excursion (MFE), in percent}
#' \item{tick.Net.Trading.PL}{ net trading P&L in ticks}
#' \item{tick.MAE}{ Maximum Adverse Excursion (MAE) in ticks}
#' \item{tick.MFE}{ Maximum Favorable Excursion (MFE) in ticks}
#' }
#' @seealso \code{\link{chart.ME}} for a chart of MAE and MFE derived from this function,
#' and \code{\link{tradeStats}} for a summary view of the performance, and
#' \code{\link{tradeQuantiles}} for round turns classified by quantile.
#' @export
perTradeStats <- function(Portfolio, Symbol, includeOpenTrade=TRUE, tradeDef="flat.to.flat", ...) {
portf <- .getPortfolio(Portfolio)
if(missing(Symbol)) Symbol <- ls(portf$symbols)[[1]]
posPL <- portf$symbols[[Symbol]]$posPL
instr <- getInstrument(Symbol)
tick_value <- instr$multiplier*instr$tick_size
tradeDef <- match.arg(tradeDef, c("flat.to.flat","flat.to.reduced","increased.to.reduced","acfifo"))
if(tradeDef=='acfifo') tradeDef<-'increased.to.reduced'
trades <- list()
switch(tradeDef,
flat.to.flat = {
# identify start and end for each trade, where end means flat position
trades$Start <- which(posPL$Pos.Qty!=0 & lag(posPL$Pos.Qty)==0)
trades$End <- which(posPL$Pos.Qty==0 & lag(posPL$Pos.Qty)!=0)
},
flat.to.reduced = {
# find all transactions that bring position closer to zero ('trade ends')
decrPos <- diff(abs(posPL$Pos.Qty)) < 0
# find all transactions that open a position ('trade starts')
initPos <- posPL$Pos.Qty!=0 & lag(posPL$Pos.Qty)==0
# 'trades' start when we open a position, so determine which starts correspond to each end
# add small amount to Start index, so starts will always occur before ends in StartEnd
Start <- xts(initPos[initPos,which.i=TRUE],index(initPos[initPos])+1e-5)
End <- xts(decrPos[decrPos,which.i=TRUE],index(decrPos[decrPos]))
StartEnd <- merge(Start,End)
StartEnd$Start <- na.locf(StartEnd$Start)
StartEnd <- StartEnd[!is.na(StartEnd$End),]
# populate trades list
trades$Start <- drop(coredata(StartEnd$Start))
trades$End <- drop(coredata(StartEnd$End))
# add extra 'trade start' if there's an open trade, so 'includeOpenTrade' logic will work
if(last(posPL)[,"Pos.Qty"] != 0)
trades$Start <- c(trades$Start, last(trades$Start))
},
increased.to.reduced = {
# find all transactions that bring position closer to zero ('trade ends')
decrPos <- diff(abs(posPL$Pos.Qty)) < 0
decrPosCount <- ifelse(diff(abs(posPL$Pos.Qty)) < 0,-1,0)
decrPosCount <- ifelse(decrPosCount[-1] == 0, 0, cumsum(decrPosCount[-1]))
decrPosQty <- ifelse(diff(abs(posPL$Pos.Qty)) < 0, diff(abs(posPL$Pos.Qty)),0)
decrPosQtyCum <- ifelse(decrPosQty[-1] == 0, 0, cumsum(decrPosQty[-1])) #subset for the leading NA
# find all transactions that take position further from zero ('trade starts')
incrPos <- diff(abs(posPL$Pos.Qty)) > 0
incrPosCount <- ifelse(diff(abs(posPL$Pos.Qty)) > 0,1,0)
incrPosCount <- ifelse(incrPosCount[-1] == 0, 0, cumsum(incrPosCount[-1]))
incrPosQty <- ifelse(diff(abs(posPL$Pos.Qty)) > 0, diff(abs(posPL$Pos.Qty)),0)
incrPosQtyCum <- ifelse(incrPosQty[-1] == 0, 0, cumsum(incrPosQty[-1])) #subset for the leading NA
df <- cbind(incrPosCount, incrPosQty, incrPosQtyCum, decrPosCount, decrPosQty, decrPosQtyCum)[-1]
names(df) <- c("incrPosCount", "incrPosQty", "incrPosQtyCum", "decrPosCount", "decrPosQty", "decrPosQtyCum")
consol <- cbind(incrPosQtyCum,decrPosQtyCum)
names(consol)<-c('incrPosQtyCum','decrPosQtyCum')
consol$decrPosQtyCum<- -consol$decrPosQtyCum
consol$incrPosQtyCum[consol$incrPosQtyCum==0]<-NA
consol$decrPosQtyCum[consol$decrPosQtyCum==0]<-NA
idx <- findInterval(na.omit(consol$decrPosQtyCum),na.omit(consol$incrPosQtyCum))
#consol <- cbind(na.omit(consol$incrPosQtyCum), na.omit(consol$decrPosQtyCum), idx)
# populate trades list
idx <- idx[!is.na(idx)] # remove NAs from idx vector
idx <- idx[-length(idx)] # remove last element...see description ***TODO: add description with example dataset?
idx <- idx + 1 # +1 as findInterval() finds the lower bound of the range...see description ***TODO: add description with example dataset?
trades$Start[1] <- first(which(consol$incrPosQtyCum != "NA"))
trades$End <- which(consol$decrPosQtyCum != "NA")
trades$Start[2:length(trades$End)] <- which(consol$incrPosQtyCum != "NA")[idx]
# now add 1 to idx for missing initdate from incr/decrPosQtyCum - adds consistency with falt.to.reduced and flat.to.flat
trades$Start <- trades$Start + 1
trades$End <- trades$End + 1
# add extra 'trade start' if there's an open trade, so 'includeOpenTrade' logic will work
if(last(posPL)[,"Pos.Qty"] != 0)
trades$Start <- c(trades$Start, last(trades$Start))
}
)
# if the last trade is still open, adjust depending on whether wants open trades or not
if(length(trades$Start)>length(trades$End))
{
if(includeOpenTrade)
trades$End <- c(trades$End,nrow(posPL))
else
trades$Start <- head(trades$Start, -1)
}
# pre-allocate trades list
N <- length(trades$End)
trades <- c(trades, list(
Init.Qty = numeric(N),
Init.Pos = numeric(N),
Max.Pos = numeric(N),
End.Qty = numeric(N),
Num.Txns = integer(N),
Max.Notional.Cost = numeric(N),
Net.Trading.PL = numeric(N),
MAE = numeric(N),
MFE = numeric(N),
Pct.Net.Trading.PL = numeric(N),
Pct.MAE = numeric(N),
Pct.MFE = numeric(N),
tick.Net.Trading.PL = numeric(N),
tick.MAE = numeric(N),
tick.MFE = numeric(N)))
# calculate information about each trade
# but first create txn.qty vector for computing new columns "Init.Qty" and "End.Qty"
txn.qty <- diff(posPL$Pos.Qty)
for(i in 1:N)
{
timespan <- seq.int(trades$Start[i], trades$End[i])
trade <- posPL[timespan]
n <- nrow(trade)
# calculate cost basis, PosPL, Pct.PL, tick.PL columns
Pos.Qty <- trade[,"Pos.Qty"] # avoid repeated subsetting
Pos.Cost.Basis <- cumsum(trade[,"Txn.Value"])
Pos.PL <- trade[,"Pos.Value"]-Pos.Cost.Basis
Pct.PL <- Pos.PL/abs(Pos.Cost.Basis) # broken for last timestamp (fixed below)
Tick.PL <- Pos.PL/abs(Pos.Qty)/tick_value # broken for last timestamp (fixed below)
Max.Pos.Qty.loc <- which.max(abs(Pos.Qty)) # find max position quantity location
# position sizes
trades$Init.Pos[i] <- Pos.Qty[1]
trades$Max.Pos[i] <- Pos.Qty[Max.Pos.Qty.loc]
# initiating and ending quantities
trades$Init.Qty[i] <- txn.qty[timespan][1]
trades$End.Qty[i] <- Pos.Qty[length(trade[,1])]
# count number of transactions
trades$Num.Txns[i] <- sum(trade[,"Txn.Value"]!=0)
# investment
trades$Max.Notional.Cost[i] <- Pos.Cost.Basis[Max.Pos.Qty.loc]
# cash P&L
trades$Net.Trading.PL[i] <- Pos.PL[n]
trades$MAE[i] <- min(0,Pos.PL)
trades$MFE[i] <- max(0,Pos.PL)
# percentage P&L
Pct.PL[n] <- Pos.PL[n]/abs(trades$Max.Notional.Cost[i])
trades$Pct.Net.Trading.PL[i] <- Pct.PL[n]
trades$Pct.MAE[i] <- min(0,Pct.PL)
trades$Pct.MFE[i] <- max(0,Pct.PL)
# tick P&L
# Net.Trading.PL/position/tick value = ticks
Tick.PL[n] <- Pos.PL[n]/abs(trades$Max.Pos[i])/tick_value
trades$tick.Net.Trading.PL[i] <- Tick.PL[n]
trades$tick.MAE[i] <- min(0,Tick.PL)
trades$tick.MFE[i] <- max(0,Tick.PL)
}
trades$Start <- index(posPL)[trades$Start]
trades$End <- index(posPL)[trades$End]
return(as.data.frame(trades))
} # end fn perTradeStats
#' quantiles of per-trade stats
#'
#' The quantiles of your trade statistics get to the heart of quantitatively
#' setting rational stops and possibly even profit taking targets
#' for a trading strategy or system.
#' When applied to theoretical trades from a backtest, they may help to adjust
#' parameters prior to trying the strategy with real money.
#' When applied to real historical trades, they should help in examining what
#' is working and where there is room for improvement in a trading system
#' or strategy.
#'
#' This function will use the \code{\link{quantile}} function to calculate
#' quantiles of per-trade net P&L, MAE, and MFE using the output from
#' \code{\link{perTradeStats}}. These quantiles are chosen by the \code{probs}
#' parameter and will be calculated for one or all of
#' 'cash','percent',or 'tick', controlled by the \code{scale} argument.
#' Quantiles will be calculated separately for trades that end positive (gains)
#' and trades that end negative (losses), and will be denoted
#' 'pos' and 'neg',respectively.
#'
#' Additionally, this function will return the MAE with respect to
#' the maximum cumulative P&L achieved for each \code{scale} you request.
#' Tomasini&Jaekle recommend plotting MAE or MFE with respect to cumulative P&L
#' and choosing a stop or profit target in the 'stable region'. The reported
#' max should help the user to locate the stable region, perhaps mechanically.
#' There is room for improvement here, but this should give the user
#' information to work with in addition to the raw quantiles.
#' For example, it may make more sense to use the max of a loess or
#' kernel or other non-linear fit as the target point.
#'
#' @param Portfolio string identifying the portfolio
#' @param Symbol string identifying the symbol to examin trades for. If missing, the first symbol found in the \code{Portfolio} portfolio will be used
#' @param \dots any other passthrough parameters
#' @param scale string specifying 'cash', or 'percent' for percentage of investment, or 'tick'
#' @param probs vector of probabilities for \code{quantile}
#' @author Brian G. Peterson
#' @references Tomasini, E. and Jaekle, U. \emph{Trading Systems - A new approach to system development and portfolio optimisation} (ISBN 978-1-905641-79-6)
#' @seealso \code{\link{tradeStats}}
#' @export
tradeQuantiles <- function(Portfolio, Symbol, ..., scale=c('cash','percent','tick'),probs=c(.5,.75,.9,.95,.99,1))
{
trades <- perTradeStats(Portfolio, Symbol, ...)
#order them by increasing MAE and decreasing P&L (to resolve ties)
trades <- trades[with(trades, order(-Pct.MAE, -Pct.Net.Trading.PL)), ]
#we could argue that we need three separate sorts, but we'll come back to that if we need to
trades$Cum.Pct.PL <- cumsum(trades$Pct.Net.Trading.PL) #NOTE: this is adding simple returns, so not perfect, but gets the job done
trades$Cum.PL <- cumsum(trades$Net.Trading.PL)
trades$Cum.tick.PL <- cumsum(trades$tick.Net.Trading.PL)
# example plot
# plot(-trades$Pct.MAE,trades$Cum.Pct.PL,type='l')
#TODO: put this into a chart. fn
post <- trades[trades$Net.Trading.PL>0,]
negt <- trades[trades$Net.Trading.PL<0,]
ret<-NULL
for (sc in scale){
switch(sc,
cash = {
posq <- quantile(post$Net.Trading.PL,probs=probs)
names(posq)<-paste('posPL',names(posq))
negq <- -1*quantile(abs(negt$Net.Trading.PL),probs=probs)
names(negq)<-paste('negPL',names(negq))
posMFEq <-quantile(post$MFE,probs=probs)
names(posMFEq) <- paste('posMFE',names(posMFEq))
posMAEq <--1*quantile(abs(post$MAE),probs=probs)
names(posMAEq) <- paste('posMAE',names(posMAEq))
negMFEq <-quantile(negt$MFE,probs=probs)
names(negMFEq) <- paste('negMFE',names(negMFEq))
negMAEq <--1*quantile(abs(negt$MAE),probs=probs)
names(negMAEq) <- paste('negMAE',names(negMAEq))
MAEmax <- trades[which(trades$Cum.PL==max(trades$Cum.PL)),]$MAE
names(MAEmax)<-'MAE~max(cumPL)'
ret<-c(ret,posq,negq,posMFEq,posMAEq,negMFEq,negMAEq,MAEmax)
},
percent = {
posq <- quantile(post$Pct.Net.Trading.PL,probs=probs)
names(posq)<-paste('posPctPL',names(posq))
negq <- -1*quantile(abs(negt$Pct.Net.Trading.PL),probs=probs)
names(negq)<-paste('negPctPL',names(negq))
posMFEq <-quantile(post$Pct.MFE,probs=probs)
names(posMFEq) <- paste('posPctMFE',names(posMFEq))
posMAEq <--1*quantile(abs(post$Pct.MAE),probs=probs)
names(posMAEq) <- paste('posPctMAE',names(posMAEq))
negMFEq <-quantile(negt$Pct.MFE,probs=probs)
names(negMFEq) <- paste('negPctMFE',names(negMFEq))
negMAEq <--1*quantile(abs(negt$Pct.MAE),probs=probs)
names(negMAEq) <- paste('negPctMAE',names(negMAEq))
MAEmax <- trades[which(trades$Cum.Pct.PL==max(trades$Cum.Pct.PL)),]$Pct.MAE
names(MAEmax)<-'%MAE~max(cum%PL)'
ret<-c(ret,posq,negq,posMFEq,posMAEq,negMFEq,negMAEq,MAEmax)
},
tick = {
posq <- quantile(post$tick.Net.Trading.PL,probs=probs)
names(posq)<-paste('posTickPL',names(posq))
negq <- -1*quantile(abs(negt$tick.Net.Trading.PL),probs=probs)
names(negq)<-paste('negTickPL',names(negq))
posMFEq <-quantile(post$tick.MFE,probs=probs)
names(posMFEq) <- paste('posTickMFE',names(posMFEq))
posMAEq <--1*quantile(abs(post$tick.MAE),probs=probs)
names(posMAEq) <- paste('posTickMAE',names(posMAEq))
negMFEq <-quantile(negt$tick.MFE,probs=probs)
names(negMFEq) <- paste('negTickMFE',names(negMFEq))
negMAEq <--1*quantile(abs(negt$tick.MAE),probs=probs)
names(negMAEq) <- paste('negTickMAE',names(negMAEq))
MAEmax <- trades[which(trades$Cum.tick.PL==max(trades$Cum.tick.PL)),]$tick.MAE
names(MAEmax)<-'tick.MAE~max(cum.tick.PL)'
ret<-c(ret,posq,negq,posMFEq,posMAEq,negMFEq,negMAEq,MAEmax)
}
) #end scale switch
} #end for loop
#return a single column for now, could be multiple column if we looped on Symbols
ret<-t(t(ret))
colnames(ret)<-paste(Portfolio,Symbol,sep='.')
ret
}
###############################################################################
# Blotter: Tools for transaction-oriented trading systems development
# for R (see http://r-project.org/)
# Copyright (c) 2008-2015 Peter Carl and Brian G. Peterson
#
# This library is distributed under the terms of the GNU Public License (GPL)
# for full details see the file COPYING
#
# $Id$
#
###############################################################################
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment