#' Entropy pooling program for blending views on scenarios with a prior scenario-probability distribution1#'2#' Entropy program will change the initial predictive distribution 'p' to a new set 'p_' that satisfies3#' specified moment conditions but changes other propoerties of the new distribution the least by4#' minimizing the relative entropy between the two distributions. Theoretical note: Relative Entropy (Kullback-Leibler information criterion KLIC) is an5#' asymmetric measure.6#'7#' We retrieve a new set of probabilities for the joint-scenarios using the Entropy pooling method8#' Of the many choices of 'p' that satisfy the views, we choose 'p' that minimize the entropy or distance of the new probability9#' distribution to the prior joint-scenario probabilities.10#'11#' We use Kullback-Leibler divergence or relative entropy dist(p,q): Sum across all scenarios [ p-t * ln( p-t / q-t ) ]12#' Therefore we define solution as p* = argmin (choice of p ) [ sum across all scenarios: p-t * ln( p-t / q-t) ],13#' such that 'p' satisfies views. The views modify the prior in a cohrent manner (minimizing distortion)14#' We forumulate the stress tests of the baseline scenarios as linear constraints on yet-to-be defined probabilities15#' Note that the numerical optimization acts on a very limited number of variables equal16#' to the number of views. It does not act directly on the very large number of variables17#' of interest, namely the probabilities of the Monte Carlo scenarios. This feature guarantees18#' the numerical feasability of entropy optimization.19#'20#' Note that new probabilities are generated in much the same way that the state-price density modifies21#' objective probabilities of pay-offs to risk-neutral probabilities in contingent-claims asset pricing22#'23#' Compute posterior (=change of measure) with Entropy Pooling, as described in24#'25#' @param p a vector of initial probabilities based on prior (reference model, empirical distribution, etc.). Sum of 'p' must be 126#' @param Aeq matrix consisting of equality constraints (paired with argument 'beq'). Denoted as 'H' in the Meucci paper. (denoted as 'H' in the "Meucci - Flexible Views Theory & Practice" paper formlua 86 on page 22)27#' @param beq vector corresponding to the matrix of equality constraints (paired with argument 'Aeq'). Denoted as 'h' in the Meucci paper28#' @param A matrix consisting of inequality constraints (paired with argument 'b'). Denoted as 'F' in the Meucci paper29#' @param b vector consisting of inequality constraints (paired with matrix A). Denoted as 'f' in the Meucci paper30#' @param verbose If TRUE, prints out additional information. Default FALSE.31#'32#' ' \deqn{ \tilde{p} \equiv argmin_{Fx \leq f, Hx \equiv h} \big\{ \sum_1^J x_{j} \big(ln \big( x_{j} \big) - ln \big( p_{j} \big) \big) \big\}33#' \\ \ell \big(x, \lambda, \nu \big) \equiv x' \big(ln \big(x\big) - ln \big(p\big) \big) + \lambda' \big(Fx - f\big) + \nu' \big(Hx - h\big)}34#' @return a list with35#' \describe{36#' \item{\code{p_}:}{ revised probabilities based on entropy pooling}37#' \item{\code{optimizationPerformance}:}{ a list with status of optimization,38#' value, number of iterations, and sum of probabilities}39#' }40#' @author Ram Ahluwalia \email{ram@@wingedfootcapital.com}41#' @references42#' A. Meucci - "Fully Flexible Views: Theory and Practice". See page 22 for illustration of numerical implementation43#' Symmys site containing original MATLAB source code \url{https://www.arpm.co/}44#' NLOPT open-source optimization site containing background on algorithms \url{https://nlopt.readthedocs.io/en/latest/}45#' We use the information-theoretic estimator of Kitamur and Stutzer (1997).46#' Reversing 'p' and 'p_' leads to the empirical likelihood" estimator of Qin and Lawless (1994).47#' See Robertson et al, "Forecasting Using Relative Entropy" (2002) for more theory48#' @export49EntropyProg = function( p , A = NULL , b = NULL , Aeq , beq, verbose=FALSE )50{51stopifnot("package:nloptr" %in% search() || requireNamespace("nloptr",quietly = TRUE) )5253if( is.vector(b) ) b = matrix(b, nrow=length(b))54if( is.vector(beq) ) beq = matrix(beq, nrow=length(beq))5556if( !length(b) ) A = matrix( ,nrow = 0, ncol = 0)57if( !length(b) ) b = matrix( ,nrow = 0, ncol = 0)5859# count the number of constraints60K_ = nrow( A ) # K_ is the number of inequality constraints in the matrix-vector pair A-b61K = nrow( Aeq ) # K is the number of equality views in the matrix-vector pair Aeq-beq6263# parameter checks64if ( K_ + K == 0 ) { stop( "at least one equality or inequality constraint must be specified")}65if ( ( ( .999999 < sum(p)) & (sum(p) < 1.00001) ) == FALSE ) { stop( "sum of probabilities from prior distribution must equal 1")}66if ( nrow(Aeq) != nrow(beq) ) { stop( "number of inequality constraints in matrix Aeq must match number of elements in vector beq") }67if ( nrow(A) != nrow(b) ) { stop( "number of equality constraints in matrix A must match number of elements in vector b") }6869# calculate derivatives of constraint matrices70A_ = t( A )71b_ = t( b )72Aeq_ = t( Aeq )73beq_ = t( beq )7475# starting guess for optimization search with length = to number of views76x0 = matrix( 0 , nrow = K_ + K , ncol = 1 )7778# set up print arguments for verbose79if(verbose){80check_derivatives_print = "all"81print_level = 282} else {83check_derivatives_print = "none"84print_level = 085}8687if ( !K_ ) # equality constraints only88{89# define maximum likelihood, gradient, and hessian functions for unconstrained and constrained optimization90eval_f_list = function( v ) # cost function for unconstrained optimization (no inequality constraints)91{92x = exp( log(p) - 1 - Aeq_ %*% v )93x = apply( cbind( x , 10^-32 ) , 1 , max ) # robustification94# L is the Lagrangian dual function (without inequality constraints). See formula 88 on p. 22 in "Meucci - Fully Flexible Views - Theory and Practice (2010)"95# t(x) is the derivative x'96# Aeq_ is the derivative of the Aeq matrix of equality constraints (denoted as 'H in the paper)97# beq_ is the transpose of the vector associated with Aeq equality constraints98# L= x' * ( log(x) - log(p) + Aeq_ * v ) - beq_ * v99# 1xJ * ( Jx1 - Jx1 + JxN+1 * N+1x1 ) - 1xN+1 * N+1x1100L = t(x) %*% ( log(x) - log(p) + Aeq_ %*% v ) - beq_ %*% v101mL = -L # take negative values since we want to maximize102103# evaluate gradient104gradient = beq - Aeq %*% x105106# evaluate Hessian107# We comment this out for now -- to be used when we find an optimizer that can utilize the Hessian in addition to the gradient108# H = ( Aeq %*% (( x %*% ones(1,K) ) * Aeq_) ) # Hessian computed by Chen Qing, Lin Daimin, Meng Yanyan, Wang Weijun109110return( list( objective = mL , gradient = gradient ) )111}112113# setup unconstrained optimization114start = Sys.time()115opts = list( algorithm = "NLOPT_LD_LBFGS" ,116xtol_rel = 1.0e-6 ,117check_derivatives = TRUE ,118check_derivatives_print = check_derivatives_print ,119print_level = print_level ,120maxeval = 1000 )121optimResult = nloptr::nloptr(x0 = x0, eval_f = eval_f_list , opts = opts )122end = Sys.time()123124if(verbose){125print( c("Optimization completed in ", end - start ))126}127128if ( optimResult$status < 0 ) {129print( c("Exit code " , optimResult$status ) )130stop( "Error: The optimizer did not converge" )131}132133# return results of optimization134v = optimResult$solution135p_ = exp( log(p) - 1 - Aeq_ %*% v )136optimizationPerformance = list( converged = (optimResult$status > 0) ,137ml = optimResult$objective ,138iterations = optimResult$iterations ,139sumOfProbabilities = sum( p_ ) )140}else # case inequality constraints are specified141{142# setup variables for constrained optimization143InqMat = -diag( 1 , K_ + K ) # -1 * Identity Matrix with dimension equal to number of constraints144InqMat = InqMat[ -c( K_+1:nrow( InqMat ) ) , ] # drop rows corresponding to equality constraints145InqVec = matrix( 0 , K_ , 1 )146147# define maximum likelihood, gradient, and hessian functions for constrained optimization148InqConstraint = function( x ) { return( InqMat %*% x ) } # function used to evalute A %*% x <= 0 or A %*% x <= c(0,0) if there is more than one inequality constraint149150jacobian_constraint = function( x ) { return( InqMat ) }151# Jacobian of the constraints matrix. One row per constraint, one column per control parameter (x1,x2)152# Turns out the Jacobian of the constraints matrix is always equal to InqMat153154nestedfunC = function( lv )155{156lv = as.matrix( lv )157l = lv[ 1:K_ , , drop = FALSE ] # inequality Lagrange multiplier158v = lv[ (K_+1):length(lv) , , drop = FALSE ] # equality lagrange multiplier159x = exp( log(p) - 1 - A_ %*% l - Aeq_ %*% v )160x = apply( cbind( x , 10^-32 ) , 1 , max )161162# L is the cost function used for constrained optimization163# L is the Lagrangian dual function with inequality constraints and equality constraints164L = t(x) %*% ( log(x) - log(p) ) + t(l) %*% (A %*% x-b) + t(v) %*% (Aeq %*% x-beq)165objective = -L # take negative values since we want to maximize166167# calculate the gradient168gradient = rbind( b - A%*%x , beq - Aeq %*% x )169170# compute the Hessian (commented out since no R optimizer supports use of Hessian)171# Hessian computed by Chen Qing, Lin Daimin, Meng Yanyan, Wang Weijun172#onesToK_ = array( rep( 1 , K_ ) ) ;onesToK = array( rep( 1 , K ) )173#x = as.matrix( x )174#H11 = A %*% ((x %*% onesToK_) * A_) ; H12 = A %*% ((x %*% onesToK) * Aeq_)175#H21 = Aeq %*% ((x %*% onesToK_) * A_) ; H22 = Aeq %*% ((x %*% onesToK) * Aeq_)176#H1 = cbind( H11 , H12 ) ; H2 = cbind( H21 , H22 ) ; H = rbind( H1 , H2 ) # Hessian for constrained optimization177return( list( objective = objective , gradient = gradient ) )178}179180# find minimum of constrained multivariate function181start = Sys.time()182# Note: other candidates for constrained optimization in library nloptr: NLOPT_LD_SLSQP, NLOPT_LD_MMA, NLOPT_LN_AUGLAG, NLOPT_LD_AUGLAG_EQ183# See NLOPT open-source site for more details: http://ab-initio.mit.edu/wiki/index.php/NLopt184local_opts <- list( algorithm = "NLOPT_LD_SLSQP",185xtol_rel = 1.0e-6 ,186check_derivatives = TRUE ,187check_derivatives_print = check_derivatives_print ,188eval_f = nestedfunC ,189eval_g_ineq = InqConstraint ,190eval_jac_g_ineq = jacobian_constraint )191optimResult = nloptr::nloptr( x0 = x0 ,192eval_f = nestedfunC ,193eval_g_ineq = InqConstraint ,194eval_jac_g_ineq = jacobian_constraint ,195opts = list( algorithm = "NLOPT_LD_AUGLAG" ,196local_opts = local_opts ,197print_level = print_level ,198maxeval = 1000 ,199check_derivatives = TRUE ,200check_derivatives_print = check_derivatives_print ,201xtol_rel = 1.0e-6 ))202end = Sys.time()203if(verbose){204print( c("Optimization completed in " , end - start ))205}206207if ( optimResult$status < 0 ) {208print( c("Exit code " , optimResult$status ) )209stop( "Error: The optimizer did not converge" )210}211212# return results of optimization213lv = matrix( optimResult$solution , ncol = 1 )214l = lv[ 1:K_ , , drop = FALSE ] # inequality Lagrange multipliers215v = lv[ (K_+1):nrow( lv ) , , drop = FALSE ] # equality Lagrange multipliers216p_ = exp( log(p) -1 - A_ %*% l - Aeq_ %*% v )217optimizationPerformance = list( converged = (optimResult$status > 0),218ml = optimResult$objective,219iterations = optimResult$iterations,220sumOfProbabilities = sum( p_ ))221}222223if(verbose) print( optimizationPerformance )224225if ( sum( p_ ) < .999 ) { stop( "Sum of revised probabilities is less than 1!" ) }226if ( sum( p_ ) > 1.001 ) { stop( "Sum of revised probabilities is greater than 1!" ) }227228return ( list ( p_ = p_ , optimizationPerformance = optimizationPerformance ) )229}230231232233#' Generates histogram234#'235#' @param X a vector containing the data points236#' @param p a vector containing the probabilities for each of the data points in X237#' @param nBins expected number of Bins the data set is to be broken down into238#' @param freq a boolean variable to indicate whether the graphic is a representation of frequencies239#'240#' @return a list with241#' f the frequency for each midpoint242#' x the midpoints of the nBins intervals243#'244#' @references245#' \url{https://www.arpm.co/}246#' See Meucci script pHist.m used for plotting247#' @author Ram Ahluwalia \email{ram@@wingedfootcapital.com} and Xavier Valls \email{flamejat@@gmail.com}248pHist = function( X , p , nBins, freq = FALSE )249{250if ( length( match.call() ) < 3 )251{252J = dim( X )[ 1 ]253nBins = round( 10 * log(J) )254}255256dist = hist( x = X , breaks = nBins , plot = FALSE );257n = dist$counts258x = dist$breaks259D = x[2] - x[1]260261N = length(x)262# np = zeros(N , 1)263np = matrix(0, nrow=N, ncol=1)264for (s in 1:N)265{266# The boolean Index is true is X is within the interval centered at x(s) and within a half-break distance267Index = ( X >= x[s] - D/2 ) & ( X <= x[s] + D/2 )268# np = new probabilities?269np[ s ] = sum( p[ Index ] )270f = np/D271}272273plot( x , f , type = "h", main = "Portfolio return distribution")274275return( list( f = f , x = x ) )276}277278279