Tuesday 5 December 2017

Candlestick Plotting Function Submitted for Inclusion in Octave Financial Package

I have today submitted an improved version of my basic candlestick plotting function, candle.m ( see previous post ) for inclusion in the Octave Financial package. As I am not sure when, or even if, it will be accepted, I provide a copy of it below.
## Copyright (C) 2017 dekalog
## 
## This program is free software; you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 3 of the License, or
## (at your option) any later version.
## 
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
## 
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see .

## -*- texinfo -*- 
## @deftypefn {Function File} {@var{retval} =} candle (@var{highprices}, @var{lowprices}, @var{closeprices}, @var{openprices})
## @deftypefnx {Function File} {@var{retval} =} candle (@var{highprices}, @var{lowprices}, @var{closeprices}, @var{openprices}, @var{color})
## @deftypefnx {Function File} {@var{retval} =} candle (@var{highprices}, @var{lowprices}, @var{closeprices}, @var{openprices}, @var{color}, @var{dates})
## @deftypefnx {Function File} {@var{retval} =} candle (@var{highprices}, @var{lowprices}, @var{closeprices}, @var{openprices}, @var{color}, @var{dates}, @var{dateform})
##
## Plot the @var{highprices}, @var{lowprices}, @var{closeprices} and @var{openprices} of a security as a candlestick chart.
##
## HighPrices - High prices for a security. A column vector.
##
## LowPrices - Low prices for a security. A column vector.
##
## ClosePrices - Close prices for a security. A column vector.
##
## OpenPrices - Open prices for a security. A column vector.
##
## Color - (optional) Candlestick color is specified as a case insensitive four 
## character row vector, e.g. "brwk". The characters that are accepted are 
## k, b, c, r, m, w, g and y for black, blue, cyan, red, magenta, white, green 
## and yellow respectively. Default colors are "brwk" applied in order to bars 
## where the closing price is greater than the opening price, bars where the 
## closing price is less than the opening price, the chart background color and
## the candlestick wicks. If fewer than four colors are specified, they are
## applied in turn in the above order with default colors for unspecified colors. 
## For example, user supplied colors "gm" will plot green upbars and magenta
## downbars with a default white background and black wicks. If the user 
## specified color for background is black, without specifying the wick color, 
## e.g. "gmk", the default wick color is white. All other choices for background 
## color will default to black for wicks. If all four colors are user specified,
## those colors will be used. Doji bars and single price bars, e.g. open = high 
## = low = close, are plotted with the color for wicks, with single price bars 
## being plotted as points/dots.
##
## Dates - (Optional) Dates for user specified x-axis tick labels. Dates can be 
## a serial date number column (see datenum), a datevec matrix (See datevec)
## or a character vector of dates. If specified as either a datenum or a datevec,
## the Dateform argument is required. If the Dates argument is supplied, the 
## Color argument must also be explicitly specified.
##
## Dateform - (Optional) Either a Date character string or a single integer code
## number used to format the x-axis tick labels (See datestr). Only required if 
## Dates is specified as a serial date number column (See datenum) or a datevec 
## matrix (See datevec). 
##
## @seealso{datenum, datestr, datevec, highlow, bolling, dateaxis, movavg, 
## pointfig}
## @end deftypefn

## Author: dekalog 
## Created: 2017-12-05

function candle ( varargin )  
  
if ( nargin < 4 || nargin > 7 )
  print_usage ();
elseif ( nargin == 4 )
  HighPrices = varargin{1}; LowPrices = varargin{2}; ClosePrices = varargin{3}; 
  OpenPrices = varargin{4}; color = "brwk";
elseif ( nargin == 5 )
  HighPrices = varargin{1}; LowPrices = varargin{2}; ClosePrices = varargin{3}; 
  OpenPrices = varargin{4}; color = varargin{5};
elseif ( nargin == 6 )
  HighPrices = varargin{1}; LowPrices = varargin{2}; ClosePrices = varargin{3}; 
  OpenPrices = varargin{4}; color = varargin{5}; dates = varargin{6};
elseif ( nargin == 7 )
  HighPrices = varargin{1}; LowPrices = varargin{2}; ClosePrices = varargin{3}; 
  OpenPrices = varargin{4}; color = varargin{5}; dates = varargin{6}; 
  dateform = varargin{7};
endif
   
if ( ! isnumeric ( HighPrices ) || ! isnumeric ( LowPrices ) || ...
  ! isnumeric ( ClosePrices ) || ! isnumeric ( OpenPrices ) ) # one of OHLC is not numeric
  error ("candle: The inputs for HighPrices, LowPrices, ClosePrices and OpenPrices must be numeric vectors.");  
endif

if ( size ( OpenPrices ) != size ( HighPrices ) )
  error ("candle: OpenPrices and HighPrices vectors are different lengths.");
endif

if ( size ( HighPrices ) != size ( LowPrices ) )
  error ("candle: HighPrices and LowPrices vectors are different lengths.");
endif 

if ( size ( LowPrices ) != size ( ClosePrices ) )
  error ("candle: LowPrices and ClosePrices vectors are different lengths.");
endif

if ( size ( ClosePrices, 1 ) == 1 && size ( ClosePrices, 2 ) > 1 ) # ohlc inputs are row vectors, so transpose them
  OpenPrices = OpenPrices'; HighPrices = HighPrices'; LowPrices = LowPrices'; 
  ClosePrices = ClosePrices';
  warning ("candle: The HighPrices, LowPrices, ClosePrices and OpenPrices should be column vectors. They have been transposed.");
endif
 
## check the user input Color argument, if it's character row vector
if ( ( nargin >= 5 && ischar ( color ) ) && size ( color, 1 ) == 1 )
  
  if ( size ( color, 2 ) == 1 )                      # only one color has been user specified
    color = [ tolower( color ) "rwk" ];              # so add default colors for down bars, background and wicks
  
  elseif ( size ( color, 2 ) == 2 )                  # two colors have been user specified
    color = [ tolower( color ) "wk" ];               # so add default colors for background and wicks
  
  elseif ( size ( color, 2 ) == 3 )                  # three colors have been user specified
  
    if ( color ( 3 ) == "k" || color ( 3 ) == "K" )  # if user selected background is black
     color = [ tolower( color ) "w" ];               # set wicks to default white
 
    else
     color = [ tolower( color ) "k" ];               # else default black wicks
 
    endif
  
  elseif ( size ( color, 2 ) >= 4 )                  # all four colors have been user specified, extra character inputs ignored  
  color = tolower( color );                          # correct in case user input contains upper case e.g. "BRWK" 
  
  endif 

elseif ( nargin >= 5 && ! ischar ( color ) )          # the user input for color is not a charcter vector

warning ("candle: The fifth input argument, Color, should be a character row vector for Color.\nThe chart has been plotted with default colors.");
color = "brwk"; 

elseif ( ischar ( color ) && size ( color, 1 ) != 1 ) # user input is more than one row of characters - a date character array by mistake?

warning ("candle: Color is not a single row character vector. Possibly a column Dates character vector?\nThe chart has been plotted with default colors.");
color = "brwk";

endif                                                 # end of nargin >= 5 && ischar ( color ) ) && size ( color, 1 ) == 1 if statement
  
wicks = HighPrices .- LowPrices;
body = ClosePrices .- OpenPrices;
up_down = sign ( body );
body_width = 20;
wick_width = 1;
doji_size = 10;
one_price_size = 15;

hold on;

## first, plot the chart background color
plot ( HighPrices, color( 3 ), LowPrices, color( 3 ) );
fill ( [ min( xlim ) max( xlim ) max( xlim ) min( xlim ) ], ...
       [ min( ylim ) min( ylim ) max( ylim ) max( ylim ) ], color( 3 ) );

## plot the wicks
x = ( 1 : length ( ClosePrices ) );                                           # the x-axis
idx = x;
high_nan = nan ( size ( HighPrices ) ); high_nan( idx ) = HighPrices;         # highs
low_nan = nan ( size ( LowPrices ) ); low_nan( idx ) = LowPrices;             # lows
x = reshape ( [ x; x; nan( size ( x ) ) ], [], 1 );
y = reshape ( [ high_nan(:)'; low_nan(:)'; nan( 1 , length ( HighPrices ) ) ], ...
              [] , 1 );
plot ( x, y, color( 4 ), "linewidth", wick_width );                           # plot wicks

## plot the up bar bodies 
x = ( 1 : length ( ClosePrices ) );                                           # the x-axis
idx = ( up_down == 1 ); idx = find ( idx );                                   # index by condition close > open
high_nan = nan ( size ( HighPrices ) ); high_nan( idx ) = ClosePrices( idx ); # body highs
low_nan = nan ( size ( LowPrices ) ); low_nan( idx ) = OpenPrices( idx );     # body lows
x = reshape ( [ x; x; nan( size ( x ) ) ], [], 1 );
y = reshape ( [ high_nan(:)'; low_nan(:)'; nan( 1, length ( HighPrices ) ) ], ...
              [], 1 );
plot ( x, y, color( 1 ), "linewidth", body_width );                           # plot bodies for up bars
  
## plot the down bar bodies
x = ( 1 : length ( ClosePrices ) );                                           # the x-axis
idx = ( up_down == -1 ); idx = find ( idx );                                  # index by condition close < open
high_nan = nan ( size ( HighPrices ) ); high_nan( idx ) = OpenPrices( idx );  # body highs
low_nan = nan ( size ( LowPrices ) ); low_nan( idx ) = ClosePrices( idx );    # body lows
x = reshape ( [ x; x; nan( size ( x ) ) ], [], 1 );
y = reshape ( [ high_nan(:)'; low_nan(:)'; nan( 1, length ( HighPrices ) ) ], ...
              [], 1 );
plot ( x, y, color( 2 ), "linewidth", body_width );                           # plot bodies for down bars

## plot special cases
## doji bars
doji_bar = ( HighPrices > LowPrices ) .* ( ClosePrices == OpenPrices ); ...
             doji_ix = find ( doji_bar ); 
             
if ( length ( doji_ix ) >= 1 )
  x = ( 1 : length ( ClosePrices ) );                                         # the x-axis  
  plot ( x( doji_ix ), ClosePrices( doji_ix ), [ "+" char ( color( 4 ) ) ], ...
        "markersize", doji_size );                                            # plot the open/close as horizontal dash  
endif

## OpenPrices == HighPrices == LowPrices == ClosePrices
one_price = ( HighPrices == LowPrices ) .* ( ClosePrices == OpenPrices ) .* ...
            ( OpenPrices == HighPrices ); one_price_ix = find ( one_price ); 
            
if ( length ( one_price_ix ) >= 1 )
  x = ( 1 : length ( ClosePrices ) );                                         # the x-axis  
  plot ( x( one_price_ix ), ClosePrices( one_price_ix ), ...
          [ "." char ( color( 4 ) ) ], "markersize", one_price_size );        # plot as a point/dot  
endif  
  
hold off;

## now add the x-axis tick labels, if the user has supplied the correct arguments

if ( nargin == 6 && isnumeric ( dates ) ) 
  error ("candle: If the sixth input argument, Dates, is a serial date number column (See datenum) or a datevec matrix (See datevec), Dateform input is required.\nThe chart has been plotted without x-axis dates.");
endif

if ( nargin == 6 && ischar ( dates ) )                                        # user has given a character vector of dates for dates
  
if ( size ( dates, 1 ) != size ( ClosePrices, 1 ) )
  error ("candle: The sixth input argument, Dates, and the OHLC prices vectors are different lengths.\nThe chart has been plotted without x-axis dates.");
else

ticks = cellstr ( dates ) ;
ax = "x" ;
xticks = 1 : length ( ClosePrices );
h = gca ();
set ( h, "xtick", xticks );
set ( h, [ ax "ticklabel" ], ticks );

endif
  
endif  
  
if ( nargin == 7 && isnumeric ( dates ) )                              # input arguments 6 and 7 assumed to be a dates vector and dateform respectively
  
if ( size ( dates, 1 ) != size ( ClosePrices, 1 ) )
  error ("candle: The sixth input argument, Dates, and the OHLC prices vectors are different lengths.\nThe chart has been plotted without x-axis dates.");
endif

if ( size ( dates, 2 ) == 1 )                                          # user has given a possible serial date number column for dates
 
is_monotonically_increasing = sum ( dates == cummax ( dates ) ) / size ( dates, 1 );

if ( is_monotonically_increasing != 1 )  
  error ("candle: Dates does not appear to be a serial date number column as it is not monotonically increasing.\nThe chart has been plotted without x-axis dates.");  
endif

if ( isnumeric ( dateform ) && ( dateform < 0 || dateform > 31 ) )     
  error ("candle: Dateform integer code number is out of bounds (See datestr).\nThe chart has been plotted without x-axis dates."); 
endif

if ( isnumeric ( dateform ) && rem ( dateform, 1 ) > 0 )
  error ("candle: Dateform code number should be an integer 0 - 31 (See datestr).\nThe chart has been plotted without x-axis dates.");
endif      

ticks = datestr ( dates, dateform );
ticks = mat2cell ( ticks, ones ( size ( ticks, 1 ), 1 ), size ( ticks, 2 ) );  
ax = "x";
xticks = 1 : length ( ClosePrices );
h = gca ();
set ( h, "xtick", xticks );
set ( h, [ ax "ticklabel" ], ticks );

elseif ( size ( dates, 2 ) == 6 )                                      # user has given a possible datevec matrix for dates

if ( isnumeric ( dateform ) && ( dateform < 0 || dateform > 31 ) )     
  error ("candle: Dateform integer code number is out of bounds (See datestr).\nThe chart has been plotted without x-axis dates."); 
endif

if ( isnumeric ( dateform ) && rem ( dateform, 1 ) > 0 )
  error ("candle: Dateform code number should be an integer 0 - 31 (See datestr).\nThe chart has been plotted without x-axis dates.");
endif 

ticks = datestr ( dates, dateform );
ticks = mat2cell ( ticks, ones ( size ( ticks, 1 ), 1 ), size ( ticks, 2 ) );
ax = "x";
xticks = 1 : length ( ClosePrices );
h = gca ();
set ( h, "xtick", xticks );
set ( h, [ ax "ticklabel" ], ticks );

else 
  error ("candle: The numerical Dates input is neither a single column serial date number nor a six column datevec format.\nThe chart has been plotted without x-axis dates."); 
  
endif

endif # end of ( nargin == 7 && isnumeric ( dates ) ) if statement 

endfunction

%!demo 1
%! Open = [ 1292.4; 1291.7; 1291.8; 1292.2; 1291.5; 1291.0; 1291.0; 1291.5; 1291.7; 1291.5; 1290.7 ];
%! High = [ 1292.6; 1292.1; 1292.5; 1292.3; 1292.2; 1292.2; 1292.7; 1292.4; 1292.3; 1292.1; 1292.9 ];
%! Low = [ 1291.3; 1291.3; 1291.7; 1291.1; 1290.7; 1290.2; 1290.3; 1291.1; 1291.2; 1290.5; 1290.4 ];
%! Close = [ 1291.8; 1291.7; 1292.2; 1291.5; 1291.0; 1291.1; 1291.5; 1291.7; 1291.6; 1290.8; 1292.8 ];
%! graphics_toolkit('fltk'); candle( High, Low, Close, Open );
%! title("default plot.");

%!demo 2
%! Open = [ 1292.4; 1291.7; 1291.8; 1292.2; 1291.5; 1291.0; 1291.0; 1291.5; 1291.7; 1291.5; 1290.7 ];
%! High = [ 1292.6; 1292.1; 1292.5; 1292.3; 1292.2; 1292.2; 1292.7; 1292.4; 1292.3; 1292.1; 1292.9 ];
%! Low = [ 1291.3; 1291.3; 1291.7; 1291.1; 1290.7; 1290.2; 1290.3; 1291.1; 1291.2; 1290.5; 1290.4 ];
%! Close = [ 1291.8; 1291.7; 1292.2; 1291.5; 1291.0; 1291.1; 1291.5; 1291.7; 1291.6; 1290.8; 1292.8 ];
%! graphics_toolkit('fltk'); candle( High, Low, Close, Open, 'brk' );
%! title("default plot with user selected black background");

%!demo 3
%! Open = [ 1292.4; 1291.7; 1291.8; 1292.2; 1291.5; 1291.0; 1291.0; 1291.5; 1291.7; 1291.5; 1290.7 ];
%! High = [ 1292.6; 1292.1; 1292.5; 1292.3; 1292.2; 1292.2; 1292.7; 1292.4; 1292.3; 1292.1; 1292.9 ];
%! Low = [ 1291.3; 1291.3; 1291.7; 1291.1; 1290.7; 1290.2; 1290.3; 1291.1; 1291.2; 1290.5; 1290.4 ];
%! Close = [ 1291.8; 1291.7; 1292.2; 1291.5; 1291.0; 1291.1; 1291.5; 1291.7; 1291.6; 1290.8; 1292.8 ];
%! graphics_toolkit('fltk'); candle( High, Low, Close, Open, 'brkg' );
%! title("default color candlestick bodies and user selected background and wick colors");

%!demo 4
%! Open = [ 1292.4; 1291.7; 1291.8; 1292.2; 1291.5; 1291.0; 1291.0; 1291.5; 1291.7; 1291.5; 1290.7 ];
%! High = [ 1292.6; 1292.1; 1292.5; 1292.3; 1292.2; 1292.2; 1292.7; 1292.4; 1292.3; 1292.1; 1292.9 ];
%! Low = [ 1291.3; 1291.3; 1291.7; 1291.1; 1290.7; 1290.2; 1290.3; 1291.1; 1291.2; 1290.5; 1290.4 ];
%! Close = [ 1291.8; 1291.7; 1292.2; 1291.5; 1291.0; 1291.1; 1291.5; 1291.7; 1291.6; 1290.8; 1292.8 ];
%! graphics_toolkit('fltk'); candle( High, Low, Close, Open, 'gmby' );
%! title("all four colors being user selected");

%!demo 5
%! Open = [ 1292.4; 1291.7; 1291.8; 1292.2; 1291.5; 1291.0; 1291.0; 1291.5; 1291.7; 1291.5; 1290.7 ];
%! High = [ 1292.6; 1292.1; 1292.5; 1292.3; 1292.2; 1292.2; 1292.7; 1292.4; 1292.3; 1292.1; 1292.9 ];
%! Low = [ 1291.3; 1291.3; 1291.7; 1291.1; 1290.7; 1290.2; 1290.3; 1291.1; 1291.2; 1290.5; 1290.4 ];
%! Close = [ 1291.8; 1291.7; 1292.2; 1291.5; 1291.0; 1291.1; 1291.5; 1291.7; 1291.6; 1290.8; 1292.8 ];
%! datenum_vec = [ 7.3702e+05; 7.3702e+05 ;7.3702e+05; 7.3702e+05; 7.3702e+05; 7.3702e+05; 7.3702e+05; ...
%! 7.3702e+05; 7.3702e+05; 7.3702e+05; 7.3702e+05 ];
%! graphics_toolkit('fltk'); candle( High, Low, Close, Open, 'brwk', datenum_vec, "yyyy-mm-dd HH:MM" );
%! title("default plot with datenum dates and character dateform arguments");

%!demo 6
%! Open = [ 1292.4; 1291.7; 1291.8; 1292.2; 1291.5; 1291.0; 1291.0; 1291.5; 1291.7; 1291.5; 1290.7 ];
%! High = [ 1292.6; 1292.1; 1292.5; 1292.3; 1292.2; 1292.2; 1292.7; 1292.4; 1292.3; 1292.1; 1292.9 ];
%! Low = [ 1291.3; 1291.3; 1291.7; 1291.1; 1290.7; 1290.2; 1290.3; 1291.1; 1291.2; 1290.5; 1290.4 ];
%! Close = [ 1291.8; 1291.7; 1292.2; 1291.5; 1291.0; 1291.1; 1291.5; 1291.7; 1291.6; 1290.8; 1292.8 ];
%! datenum_vec = [ 7.3702e+05; 7.3702e+05 ;7.3702e+05; 7.3702e+05; 7.3702e+05; 7.3702e+05; 7.3702e+05; ...
%! 7.3702e+05; 7.3702e+05; 7.3702e+05; 7.3702e+05 ];
%! graphics_toolkit('fltk'); candle( High, Low, Close, Open, 'brk', datenum_vec, 31 );
%! title("default plot with user selected black background with datenum dates and integer dateform arguments");

%!demo 7
%! Open = [ 1292.4; 1291.7; 1291.8; 1292.2; 1291.5; 1291.0; 1291.0; 1291.5; 1291.7; 1291.5; 1290.7 ];
%! High = [ 1292.6; 1292.1; 1292.5; 1292.3; 1292.2; 1292.2; 1292.7; 1292.4; 1292.3; 1292.1; 1292.9 ];
%! Low = [ 1291.3; 1291.3; 1291.7; 1291.1; 1290.7; 1290.2; 1290.3; 1291.1; 1291.2; 1290.5; 1290.4 ];
%! Close = [ 1291.8; 1291.7; 1292.2; 1291.5; 1291.0; 1291.1; 1291.5; 1291.7; 1291.6; 1290.8; 1292.8 ];
%! datenum_vec = [ 7.3702e+05; 7.3702e+05 ;7.3702e+05; 7.3702e+05; 7.3702e+05; 7.3702e+05; 7.3702e+05; ...
%! 7.3702e+05; 7.3702e+05; 7.3702e+05; 7.3702e+05 ];
%! datevec_vec = datevec( datenum_vec );
%! graphics_toolkit('fltk'); candle( High, Low, Close, Open, 'brwk', datevec_vec, 21 );
%! title("default plot with datevec dates and integer dateform arguments");

%!demo 8
%! Open = [ 1292.4; 1291.7; 1291.8; 1292.2; 1291.5; 1291.0; 1291.0; 1291.5; 1291.7; 1291.5; 1290.7 ];
%! High = [ 1292.6; 1292.1; 1292.5; 1292.3; 1292.2; 1292.2; 1292.7; 1292.4; 1292.3; 1292.1; 1292.9 ];
%! Low = [ 1291.3; 1291.3; 1291.7; 1291.1; 1290.7; 1290.2; 1290.3; 1291.1; 1291.2; 1290.5; 1290.4 ];
%! Close = [ 1291.8; 1291.7; 1292.2; 1291.5; 1291.0; 1291.1; 1291.5; 1291.7; 1291.6; 1290.8; 1292.8 ];
%! character_dates = char ( [] );
%! for i = 1 : 11
%! character_dates = [ character_dates ; "a date" ] ;
%! endfor
%! graphics_toolkit('fltk'); candle( High, Low, Close, Open, 'brk', character_dates );
%! title("default plot with user selected black background with character dates argument");
I hope readers who are Octave users find this useful.

No comments: