File Coverage

lib/Amibroker/AFL/Optimizer.pm
Criterion Covered Total %
statement 18 20 90.0
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 25 27 92.5


line stmt bran cond sub pod time code
1             package Amibroker::AFL::Optimizer;
2            
3 1     1   23082 use 5.006;
  1         3  
4 1     1   4 use strict;
  1         2  
  1         18  
5 1     1   4 use warnings;
  1         16  
  1         36  
6 1     1   5 use File::Path qw(remove_tree);
  1         2  
  1         82  
7 1     1   746 use File::Copy::Recursive;
  1         9142  
  1         44  
8 1     1   832 use File::Slurp;
  1         16095  
  1         84  
9 1     1   359 use Win32::API;
  0            
  0            
10             use Carp;
11             use Amibroker::OLE::Interface;
12             use Amibroker::OLE::APXCreator;
13            
14             our $VERSION = '0.01';
15            
16             my $AMIBROKER_SERVICE_RUNS = 1000;
17            
18             sub new {
19             my @list = @_;
20             my $class = shift @list;
21             push @list, {} unless @list and _is_arg( $list[-1] );
22             my $self = _check_valid_args(@list);
23             croak(
24             '[ERROR] : No \'dbpath\' (Database path) supplied to Amibroker (Required Parameter) '
25             . "\n" )
26             unless $self->{dbpath};
27             croak( '[ERROR] : Invalid \'dbpath\' (Database path) to Amibroker ' . "\n" )
28             unless -d $self->{dbpath};
29             croak(
30             '[ERROR] : No \'destination_path\' path supplied to Amibroker (Required Parameter) '
31             . "\n" )
32             unless $self->{destination_path};
33             croak(
34             '[ERROR] : No \'timeframe\' supplied to Amibroker (Required Parameter) '
35             . "\n" )
36             unless $self->{timeframe};
37             croak( '[ERROR] : No \'symbol\' supplied to Amibroker (Required Parameter) '
38             . "\n" )
39             unless $self->{symbol};
40             croak(
41             '[ERROR] : No \'afl_template\' supplied to Amibroker (Required Parameter) '
42             . "\n" )
43             unless $self->{afl_template};
44             croak(
45             '[ERROR] : No \'lot_size\' supplied to Amibroker, (Required Parameter)'
46             . "\n" )
47             unless $self->{lot_size};
48            
49             print
50             '[WARN] : No \'log_path\' supplied to Amibroker : So, No logging '
51             . "\n"
52             unless $self->{log_path};
53             print
54             '[WARN] : No \'optimizer_name\' supplied to Amibroker : Default is taken '
55             . "\n"
56             unless $self->{optimizer_name};
57             print
58             '[WARN] : No \'min_win_percent\' supplied to Amibroker : Default 40% is taken '
59             . "\n"
60             unless $self->{min_win_percent};
61             print
62             '[WARN] : No \'min_profit_percent\' supplied to Amibroker : Default 50% is taken '
63             . "\n"
64             unless $self->{min_profit_percent};
65             print
66             '[WARN] : No \'min_no_of_trades\' supplied to Amibroker : Default 10 is taken '
67             . "\n"
68             unless $self->{min_no_of_trades};
69             print
70             '[WARN] : No \'selection_index\' supplied to Amibroker : Default 2 is taken '
71             . "\n"
72             unless $self->{selection_index};
73             print
74             '[WARN] : No \'profit_from\' supplied to Amibroker : Default 0 is taken '
75             . "\n"
76             unless $self->{_profit_from};
77             print
78             '[WARN] : No \'profit_to\' supplied to Amibroker : Default 20000 is taken '
79             . "\n"
80             unless $self->{profit_to};
81             print
82             '[WARN] : No \'profit_incr\' supplied to Amibroker : Default 5000 is taken '
83             . "\n"
84             unless $self->{profit_incr};
85             print
86             '[WARN] : No \'optimize_time\' supplied to Amibroker : By Default No time optimization is done '
87             . "\n"
88             unless $self->{optimize_time};
89             print
90             '[WARN] : No \'margin_amt\' supplied to Amibroker : By Default Ignoring margin amount '
91             . "\n"
92             unless $self->{margin_amt};
93             print
94             '[WARN] : No \'from or to\' dates are supplied to Amibroker : By Default Ignoring From & To Dates'
95             . "\n"
96             unless ( $self->{from} || $self->{to} );
97             bless $self, $class if defined $self;
98             $self->init();
99             return $self;
100             }
101            
102             sub init {
103             my $self = shift;
104             $self->{optimizer_name} = 'opt';
105             $self->{_timestamp} = $self->getLoggingTime();
106             $self->{_sandbox_path} = $self->getSandboxPath();
107             $self->{min_win_percent} = 40 unless $self->{min_win_percent};
108             $self->{min_profit_percent} = 50 unless $self->{min_profit_percent};
109             $self->{min_no_of_trades} = 10 unless $self->{min_no_of_trades};
110             $self->{selection_index} = 2 unless $self->{selection_index};
111             $self->{profit_from} = 0 unless $self->{profit_from};
112             $self->{profit_to} = 20000 unless $self->{profit_to};
113             $self->{profit_incr} = 5000 unless $self->{profit_incr};
114             $self->{optimize_time} = 'NO' unless $self->{optimize_time};
115             return 1;
116             }
117            
118             sub start_optimizer {
119             my $self = shift;
120             my $script = $self->{symbol};
121             $self->{_BT_Count} = 0;
122             #
123             # Step 1 : Start Amibroker engine
124             #
125             print "--------------------------------------\n\n";
126             print "Starting the amibroker engine\n";
127             my $broker = Amibroker::OLE::Interface->new(
128             {
129             verbose => 1,
130             dbpath => $self->{dbpath}
131             }
132             );
133             $broker->start_amibroker_engine();
134             $self->{_broker} = $broker;
135             print "Amibroker Engine Started\n" if ($broker);
136             #
137             # Step 2 : Create Sandbox path if it doesnt exists
138             #
139             File::Path::make_path( $self->{sandbox}, { verbose => 1 } );
140             #
141             # Step 3: a copy of AFL Template file
142             #
143             my $copiedFile = $self->copy_file(
144             $self->{afl_template}, $self->{destination_path},
145             $self->{timeframe}, $self->{optimizer_name},
146             $self->{symbol}
147             );
148             my $copyAfl = File::Slurp::read_file($copiedFile);
149             #
150             # Step 4: Replace STOCK NAME and LOT SIZE for the copied AFL File
151             #
152             $copyAfl =~ s/LOT_SIZE\=XXXX\;/LOT_SIZE\=$self->{lot_size}\;/g;
153             $copyAfl =~ s/STOCK_NAME\=\"XXXX\"\;/STOCK_NAME\=\"$script\"\;/g;
154             #
155             # Step 5: Save the Copied AFL file
156             #
157             File::Slurp::write_file( $copiedFile, $copyAfl );
158             #
159             # Step 6: Run for STRATEGY optimized two variables
160             #
161             if ( $copyAfl =~ /optimize/g ) {
162             my $status1 =
163             $self->run_optimize_engine( 'BASIC', $broker, $copiedFile, $script );
164            
165             # No results, so skip optimization for this Afl
166             # 99 is user-defined number - check for the called function
167             #
168             return 1 if ( $status1 == 99 );
169             }
170             #
171             # Step 7: Now get ready for time based optimization
172             #
173             if ( $self->{optimize_time} =~ /YES/i ) {
174             my $copy2Afl = File::Slurp::read_file($copiedFile);
175             my $firstTime =
176             "FirstTradeTime = optimize(\"FirstTradeTime\",093000,091500,123000,001000);";
177             my $lastTime =
178             "LastTradeTime = optimize(\"LastTradeTime\",151500,140000,153000,001000);";
179             $copy2Afl =~ s/FirstTradeTime.*/$firstTime/;
180             $copy2Afl =~ s/LastTradeTime.*/$lastTime/;
181             File::Slurp::write_file( $copiedFile, $copy2Afl );
182             #
183             # Step 8: Run for Time parameters optimized two variables
184             #
185             my $status2 =
186             $self->run_optimize_engine( 'TIME', $broker, $copiedFile, $script );
187            
188             # No results, so skip optimization for this Afl
189             # 99 is user-defined number - check for the called function
190             #
191             return 1 if ( $status2 == 99 );
192             }
193             #
194             # Step 20: Now insert profit limits to the AFL and save the files
195             #
196             $self->save_profit_limit_afls( $script, $copiedFile, $self->{lot_size} );
197             return 1;
198             }
199             #
200             # Create multiple afl files with different profit limits for backtesting
201             #
202             sub save_profit_limit_afls {
203             my ( $self, $script, $afl, $lotsize ) = @_;
204             my $copy3Afl = Utils::slurp_afl($afl);
205             my $destination = $self->{destination_path};
206             my $optname = $self->{optimizer_name};
207            
208             for (
209             my $i = $self->{profit_from} ;
210             $i <= $self->{profit_to} ;
211             $i = $i + $self->{profit_incr}
212             )
213             {
214             my $tempFile =
215             $destination . '\\'
216             . $script . '-'
217             . $self->{timeframe} . '-'
218             . $optname . '-'
219             . $i . '.afl';
220             my $tempcopy = $copy3Afl;
221             my $value = $i / $lotsize;
222             my $searchString = "\/\/PROFITPOINTSKEYWORD";
223             my $replaceString =
224             "ApplyStop\(stopTypeProfit\,stopModePoint\,$value\,True\,True\)\;";
225             $tempcopy =~ s/$searchString/$replaceString/;
226             Utils::Write_to_file( $tempFile, $tempcopy );
227             }
228             return 1;
229             }
230            
231             #
232             # Main logic of the tool goes here
233             #
234             sub run_optimize_engine {
235             my ( $self, $type, $broker, $copiedFile, $script ) = @_;
236             $self->service_amibroker()
237             if ( $self->{_BT_Count} >= $AMIBROKER_SERVICE_RUNS );
238             #
239             # Step 10 : Slurp AFL Template file to a string
240             #
241             my $slurpAfl = File::Slurp::read_file($copiedFile);
242             #
243             # Step 11 : Get the Sandbox and Result file path
244             #
245             my $sandbox_xml =
246             $self->{sandbox} . '/'
247             . $script . '-'
248             . $self->{timeframe} . '-'
249             . $self->{optimizer_name} . '-'
250             . $type . '.apx';
251             my $resultFile =
252             $self->{sandbox} . '/'
253             . $script . '-'
254             . $self->{timeframe} . '-'
255             . $self->{optimizer_name} . '-'
256             . $type . '.csv';
257             #
258             # Step 12 : Create APX file
259             #
260             my $range = 0; # by default range_type is all quotes
261             $range = 3
262             if ( $self->{from} && $self->{to} )
263             ; # if from & to dates are supplied, then go for From_and_To
264             my $apxFile = Amibroker::OLE::APXCreator::create_apx_file(
265             {
266             apx_file => $sandbox_xml,
267             afl_file => $slurpAfl,
268             symbol => $script,
269             timeframe => $self->{timeframe},
270             from => $self->{from},
271             to => $self->{to},
272             range_type => $range,
273             apply_to => 1
274             }
275             );
276             #
277             # Step 13 : Run Amibroker Optimizer
278             #
279             my $runStatus = $broker->run_analysis(
280             {
281             action => 5,
282             symbol => $self->{symbol},
283             apx_file => $apxFile,
284             result_file => $resultFile
285             }
286             );
287             print "ERROR in Amibroker Engine - Symbol: " . $script . "\n\n"
288             unless ($runStatus);
289             #
290             # Step 14 : Load the result file generated by amibroker to hash
291             #
292             my $result_array = $self->load_result_file( $resultFile, $script );
293             print
294             "\n***WARN: Result is empty - Amibroker Didnt run for Symbol = $script\n"
295             unless (@$result_array);
296             print "Please report to developer - Seems Dates.txt is screwed\n\n"
297             unless (@$result_array);
298             $self->save_result_file( $type, $result_array, $script, 'RAW' );
299             #
300             # Step 15: Filter Result array
301             #
302             my $minprofit = $self->{min_profit_percent};
303             my $mintrades = $self->{min_no_of_trades};
304             my $minwinner = $self->{min_win_percent};
305             my $resultSet =
306             $self->filter_results( $type, $minprofit, $mintrades, $minwinner,
307             $result_array );
308             #
309             # Step 16: To sure we have sufficient data for selection
310             #
311             my $countFlg = 0;
312             while ($#$resultSet < ( $self->{selection_index} * 3 )
313             && $countFlg < ( $self->{selection_index} * 3 ) )
314             {
315             $minprofit = $minprofit - 3;
316             $mintrades = $mintrades - 1;
317             $minwinner = $minwinner - 1;
318             $resultSet =
319             $self->filter_results( $type, $minprofit, $mintrades, $minwinner,
320             $result_array );
321             $countFlg++;
322             }
323            
324             # Step 17: If result set is less, then ignore that selection itself
325             #
326            
327             if ( $#$resultSet < $self->{selection_index} ) {
328             my $copyOptAfl_default = File::Slurp::read_file($copiedFile);
329             my $selected =
330             $self->get_the_default_values( $resultSet->[0], $copyOptAfl_default );
331             $self->update_optimized_params(
332             $selected, # Values to update the afl
333             $resultSet->[0], # Header info of the result set
334             $copiedFile, # File path and the physical file
335             $copyOptAfl_default # File contents that needs modification
336             );
337             return 99; # No results number (user-defined)
338             }
339             #
340             # Step 18: Save filtered list to file
341             #
342             $self->save_result_file( $type, $resultSet, $script, 'Filtered' );
343             my ( $selected, $sorted_array ) = $self->sort_and_select($resultSet);
344             $self->save_result_file( $type, $sorted_array, $script, 'Sorted' );
345             #
346             # Step 19: Copy again the AFL file and update the parameters in the file.
347             #
348             my $copyOptAfl = File::Slurp::read_file($copiedFile);
349             $self->update_optimized_params(
350             $selected, # Values to update the afl
351             $resultSet->[0], # Header info of the result set
352             $copiedFile, # File path and the physical file
353             $copyOptAfl # File contents that needs modification
354             );
355             return 1;
356             }
357            
358             #
359             # Service Amibroker - by restarting it.
360             # This will clean up the memory that is being holdup by the engine.
361             #
362             sub service_amibroker {
363             my $self = shift;
364             $self->{_BT_Count} = 0; # reseting the count
365             my $broker = $self->{_broker};
366             print "--------------------------------------\n";
367             print "Restarting Amibroker Engine\n";
368             $broker->shutdown_amibroker_engine();
369             sleep(30); #Sleep for half minute and let the amibroker engine relax
370            
371             print "Starting the amibroker engine\n";
372             my $newbroker = Amibroker::OLE::Interface->new(
373             {
374             verbose => 1,
375             dbpath => $self->{dbpath}
376             }
377             );
378             $newbroker->start_amibroker_engine();
379             $self->{_broker} = $newbroker;
380             print "Amibroker Engine Re-Started again\n" if ($newbroker);
381             print "--------------------------------------\n\n";
382             return 1;
383             }
384            
385             #
386             # Supports only Two parameters updation
387             #
388             sub update_optimized_params {
389             my ( $self, $selected, $header, $file, $slurpData ) = @_;
390             my $paramName1 = $header->[3];
391             my $paramName2 = $header->[4];
392             my $paramValue1 = $selected->[3];
393             my $paramValue2 = $selected->[4];
394             $slurpData =~ s/optimize\(\"$paramName1\".*;/$paramValue1\;/;
395             $slurpData =~ s/optimize\(\"$paramName2\".*;/$paramValue2\;/;
396            
397             File::Slurp::write_file( $file, $slurpData );
398             return 1;
399             }
400            
401             sub get_the_default_values {
402             my ( $self, $header, $slurpData ) = @_;
403             my $paramName1 = $header->[3];
404             $slurpData =~ /$paramName1.*",([A-Za-z0-9]+)\,/;
405             my $paramValue1 = $1;
406             my $paramName2 = $header->[4];
407             $slurpData =~ /$paramName2.*",([A-Za-z0-9]+)\,/;
408             my $paramValue2 = $1;
409             my @selected = ( 0, 0, 0, $paramValue1, $paramValue2 );
410             return \@selected;
411             }
412            
413             # Sorts the result set and selects only one row.
414             # Default sort is done for Profit% - You can add more in next release
415             #
416             sub sort_and_select {
417             my ( $self, $result ) = @_;
418             my $header = shift @$result;
419             my @sorted = sort { $b->[0] <=> $a->[0] } @$result;
420             my $selection;
421             if ( $#sorted < $self->{selection_index} ) {
422             $selection = $sorted[$#sorted];
423             }
424             else {
425             $selection = $sorted[ $self->{selection_index} - 1 ];
426             }
427             unshift @$result, $header;
428             return ( $selection, \@sorted );
429             }
430             #
431             # To save the results of hash to csv separated file.
432             #
433             sub save_result_file {
434             my ( $self, $type, $result_array, $script, $extn ) = @_;
435             my $file =
436             $self->{sandbox} . '\\'
437             . $script . '-'
438             . $self->{timeframe} . '-'
439             . $type . '-'
440             . $self->{optimizer_name} . '-'
441             . $extn . '.csv';
442             File::Path::make_path( $self->{sandbox}, { verbose => 1 } )
443             unless ( -d $self->{sandbox} );
444             open( my $SFH, '>', $file )
445             or croak( '[ERROR] : Could not open file ' . $file . " $!\n" );
446             $self->dump_data_to_file( $SFH, $result_array );
447             close $SFH;
448             return 1;
449             }
450             #
451             # Saving the data to file
452             #
453             sub dump_data_to_file {
454             my ( $self, $FH, $result_array ) = @_;
455             foreach (@$result_array) {
456             print $FH join ",", @$_;
457             print $FH "\n";
458             }
459             return 1;
460             }
461             #
462             # Logic to filter the result set
463             #
464             sub filter_results {
465             my ( $self, $type, $minprofit, $mintrades, $minwinner, $result_array ) = @_;
466             if ( $type eq 'TIME' ) {
467             $result_array = $self->clear_unwanted_timings( $result_array, 3 )
468             ; # clean FirstTradeTime
469             $result_array = $self->clear_unwanted_timings( $result_array, 4 )
470             ; # clean LastTradeTime
471             }
472             #
473             # Filter minimum profits % - array index is 0
474             $result_array = $self->recursive_filter( $result_array, $minprofit, 0 );
475             #
476             # Filter minimum number of trades - array index is 1
477             $result_array = $self->recursive_filter( $result_array, $mintrades, 1 );
478             #
479             # Filter minimum winners % - array index is 2
480             $result_array = $self->recursive_filter( $result_array, $minwinner, 2 );
481             return $result_array;
482             }
483             #
484             # Optimization results has timings that are from 1 to 100, we want just 1 to 60.
485             #
486             sub clear_unwanted_timings {
487             my ( $self, $result_array, $index ) = @_;
488             my @temp = @$result_array;
489             for ( my $i = $#temp ; $i > 0 ; $i-- ) {
490             unless ( $self->check_if_in_time_range( \@temp, $i, $index ) ) {
491             splice( @temp, $i, 1 );
492             }
493             }
494             return \@temp;
495             }
496            
497             sub check_if_in_time_range {
498             my ( $self, $temp, $i, $index ) = @_;
499             return 1
500             if ( $temp->[$i]->[$index] >= 91500 && $temp->[$i]->[$index] <= 95500 );
501             return 1
502             if ( $temp->[$i]->[$index] >= 100000 && $temp->[$i]->[$index] <= 105500 );
503             return 1
504             if ( $temp->[$i]->[$index] >= 110000 && $temp->[$i]->[$index] <= 115500 );
505             return 1
506             if ( $temp->[$i]->[$index] >= 120000 && $temp->[$i]->[$index] <= 125500 );
507             return 1
508             if ( $temp->[$i]->[$index] >= 130000 && $temp->[$i]->[$index] <= 135500 );
509             return 1
510             if ( $temp->[$i]->[$index] >= 140000 && $temp->[$i]->[$index] <= 145500 );
511             return 1
512             if ( $temp->[$i]->[$index] >= 150000 && $temp->[$i]->[$index] <= 151000 );
513             return 0; # return 0 if nothing fits
514             }
515            
516             #
517             # Call the filtering engine
518             #
519             sub recursive_filter {
520             my ( $self, $result_array, $min_value, $index ) = @_;
521             my @temp = @$result_array;
522            
523             for ( my $i = $#temp ; $i > 0 ; $i-- ) {
524             splice( @temp, $i, 1 ) if ( $temp[$i]->[$index] < $min_value );
525             }
526             return \@temp;
527             }
528            
529             #
530             # Copy a given file
531             #
532             sub copy_file {
533             my ( $self, $source, $destination, $timeframe, $optname, $script ) = @_;
534             my $final =
535             $destination . '\\'
536             . $script . '-'
537             . $timeframe . '-'
538             . $optname . '.afl';
539             File::Copy::copy( $source, $final )
540             or croak( '[ERROR] : Copy failed: ' . "$!" );
541             return $final;
542             }
543            
544             #
545             # Clean up the sandbox for allowing fresh optimize to run
546             #
547             sub clear_sandbox {
548             my $self = shift;
549             File::Path::remove_tree( $self->{sandbox}, { verbose => 1 } );
550             print "Sandbox cleaned up\n";
551             return 1;
552             }
553             #
554             # Keep a copy of sandbox in the logs foler for later debugging
555             #
556             sub copy_sandbox {
557             my $self = shift;
558             if ( $self->{log_path} ) {
559             my $logPath = $self->{log_path} . '/' . $self->{_timestamp};
560             File::Copy::Recursive::dircopy( $self->{sandbox}, $logPath )
561             or print("WARN: Could not copy Sandbox log: $!\n");
562             print "Sandbox copy completed\n";
563             }
564             return 1;
565             }
566             #
567             # Load the result file generated by Amibroker engine
568             #
569             sub load_result_file {
570             my ( $self, $resultFile ) = @_;
571             my @raw = File::Slurp::read_file($resultFile);
572             my @result;
573             foreach (@raw) {
574             chomp($_);
575             my @temp = split( /,/, $_ );
576             splice @temp, 0, 2;
577             splice @temp, 1, 18;
578             splice @temp, 2, 4;
579             splice @temp, 3, 10;
580             push( @result, \@temp );
581             }
582             return \@result;
583             }
584            
585             #
586             # create a timestamp for log files
587             #
588             sub getLoggingTime {
589             my $self = shift;
590             my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
591             localtime(time);
592             my $nice_timestamp = sprintf(
593             "%04d%02d%02d-%02d%02d%02d",
594             $year + 1900,
595             $mon + 1, $mday, $hour, $min, $sec
596             );
597             return $nice_timestamp;
598             }
599            
600             sub getSandboxPath {
601             my $self = shift;
602             my $GetTempAPI =
603             Win32::API->new( 'kernel32', 'GetTempPath', [ 'N', 'P' ], 'N' );
604            
605             my $lpBuffer = " " x 80;
606             my $length = $GetTempAPI->Call( 80, $lpBuffer );
607             my $tempPath = substr( $lpBuffer, 0, $length );
608            
609             $tempPath .= 'sandbox-' . $self->{_timestamp};
610             print "Temp directory: $tempPath\n";
611             return $tempPath;
612             }
613            
614             sub _is_arg {
615             my ($arg) = @_;
616             return ( ref $arg eq 'HASH' );
617             }
618            
619             sub _check_valid_args {
620             my @list = @_;
621             my %args_permitted = map { $_ => 1 } (
622             qw|
623             dbpath
624             destination_path
625             timeframe
626             symbol
627             afl_template
628             lot_size
629             log_path
630             optimizer_name
631             min_win_percent
632             min_profit_percent
633             min_no_of_trades
634             selection_index
635             profit_from
636             profit_to
637             profit_incr
638             optimize_time
639             margin_amt
640             from
641             to
642             |
643             );
644             my @bad_args = ();
645             my $arg = pop @list;
646             for my $k ( sort keys %{$arg} ) {
647             push @bad_args, $k unless $args_permitted{$k};
648             }
649             croak("Unrecognized option(s) passed to Amibroker OLE: @bad_args")
650             if @bad_args;
651             return $arg;
652             }
653            
654             #
655             # Explicitly call the destructor
656             #
657             sub DESTROY {
658             my $self = shift;
659             my $broker = $self->{_broker};
660             $broker->shutdown_amibroker_engine() if ( $self->{_broker} );
661             $self->copy_sandbox();
662             $self->clear_sandbox();
663             return 1;
664             }
665            
666             1; # End of Amibroker::AFL::Optimizer
667            
668             __END__