File Coverage

lib/Finance/IG.pm
Criterion Covered Total %
statement 27 655 4.1
branch 1 300 0.3
condition 0 138 0.0
subroutine 9 39 23.0
pod 26 26 100.0
total 63 1158 5.4


line stmt bran cond sub pod time code
1             package Finance::IG;
2              
3             # use 5.010000; I cannot get this to work, trying to say it should run with perl 5.10 or greater and should be fine with 5.32
4             # but get message ! Installing the dependencies failed: Your Perl (5.032001) is not in the range '5.10'
5 1     1   73651 use strict;
  1         3  
  1         33  
6 1     1   5 no strict 'refs';
  1         2  
  1         23  
7 1     1   17 use warnings;
  1         2  
  1         58  
8              
9             =encoding utf8
10              
11             =head1 NAME
12              
13             Finance::IG - - Module for doing useful stuff with IG Markets REST API.
14              
15             =head1 DESCRIPTION
16              
17             This is very much a first draft, but will enable you to get simple arrays of positions, print them out possily some simple trading.
18              
19             Its proof of concept in perl beyond anything else, extend it as you need to.
20              
21             I have only used it for spreadbet accounts, it would be simple to extend to CFD's but I dont have CFD data or an interest in CFD's so have not done this.
22              
23             You will need an API key to use this module, available free from IG Markets.
24              
25             =head1 VERSION
26              
27             Version 0.102
28              
29             =cut
30              
31             our $VERSION = '0.102';
32              
33              
34             =head1 SYNOPSIS
35              
36             use Finance::IG;
37             use strict;
38             no strict 'refs';
39             use warnings;
40              
41             my $ig=iFinance::IG->new(
42             username=> "demome",
43             password=> "mypassword",
44             apikey=> "4398232394029341776153276512736icab",
45             isdemo=>0,
46             );
47            
48             my $p=$ig->positions(); # Get a list of positions
49             $p=$ig->agg($p,$sortlist); # Aggregate them, so one item per instrument.
50              
51             my $format="%-41sinstrumentName %+6.2fsize %-9.2flevel ".
52             "%-9.2fbid £%-8.2fprofit %5.1fprofitpc%% £%10.2fatrisk\n",
53              
54             $ig->printpos("stdout" , [], $format);
55              
56             for my $position (@$p)
57             {
58             $ig-> printpos("stdout" ,$position,$format);
59             }
60              
61             =head1 UTILITIES
62              
63             The utility igdisp.pl is installed with this module and may be used to list your positions on IG. A help message can be obtained with igdisp.pl -h
64              
65             =head1 SUBROUTINES/METHODS
66              
67             This is a list of currently implemented methods
68              
69             =head2 new
70            
71             Normal parameters, as above.
72              
73             col=>1
74              
75             Causes Finance::IG to try to use Term::Chrome to do some simple coloration of output.
76             If Term::Chrome is not installed, it will be silently ignored. See printpos.
77              
78             =head2 login
79              
80             Originally needed to be called once after new and before other calls. Now this is done automatically,
81             so you do not need to use this or be aware of it. Look for a 401 error if your password is
82             wrong.
83              
84              
85             No Parameters.
86              
87             =head2 printpos print out a formatted hash as one line
88              
89             Parameters
90            
91             file - can be a file handle or the string stdout or the glob *STDOUT
92             A position of other shallow hash,
93             A format string. The format string is similar to a printf format string, for example %s says print out a string
94             however, the name of the item to be printed follows the letter, eg %sinstrumentName print the string instrument name.
95             optional up
96             optional down
97              
98             A title line can be printed by either passing an array ref instead of a position, in which case the array ref can contain
99             the titles to print. If the array is empty then the titles will be generated from the format string.
100              
101             up and down can be provided and represent negative and posite limits on dbid element by default.
102             Alternatively, provide up only and make it a subroutine ref.
103              
104             The subroutime takes parameter a position, and should return escape characters (from Term::Chrome to colorise the line.
105              
106             =head2 transactions - retrieve transactions history
107              
108             transactions(++$page,Time::Piece->strptime("2020-01-01","%Y-%m-%d-%H.%M"),scalar localtime)
109              
110             Parameters
111              
112             Paging number, start at 1
113             Start time, can be a string or a Time::Piece
114             Endtime
115              
116             return a reference to an array of transactions for that time span. Each transaction is a hash of data.
117              
118             =cut
119              
120 1     1   635 use Moose;
  1         483214  
  1         8  
121 1     1   8386 use JSON;
  1         11161  
  1         5  
122 1     1   695 use REST::Client;
  1         46198  
  1         42  
123             #use Data::Dump qw(dump); # used in some commented out debug statements
124             #use Scalar::Util;
125 1     1   604 use Time::Piece;
  1         8084  
  1         5  
126              
127             BEGIN {
128 1 50   1   213 if (eval("require Term::Chrome"))
129             {
130 0         0 Term::Chrome->import();
131             }
132             else
133             {
134 1     0 1 5 map { eval ("sub $_ {}") } qw(Red Blue Bold Reset Underline Green color); # need these to avoid compile time errors.
  7     0 1 3491  
        0 1    
        0 1    
        0 1    
        0 1    
        0 1    
135             }
136             }
137             has 'apikey' => (
138             is=>'ro',
139             isa=>'Str',
140             required=>1,
141             );
142             has 'username' => (
143             is=>'ro',
144             isa=>'Str',
145             required=>1,
146             );
147             has 'password' => (
148             is=>'ro',
149             isa=>'Str',
150             required=>1,
151             );
152             has 'isdemo' => (
153             is=>'ro',
154             isa=>'Bool',
155             required=>1,
156             );
157             has 'CST' => (
158             is=>'rw',
159             isa=>'Str',
160             );
161             has 'XSECURITYTOKEN' => (
162             is=>'rw',
163             isa=>'Str',
164             );
165              
166             has 'XSTTIME' => (
167             is=>'rw',
168             isa=>'Int',
169             );
170             has 'col' => ( # set to 1 to use Term::Chrome for coloration.
171             is=>'rw',
172             isa=>'Bool',
173             default=>0,
174             );
175             has 'uds' => (
176             is=>'rw',
177             isa=>'Str',
178             default=>'',
179             );
180              
181             around 'new' => sub {
182             my $orig = shift;
183             my $self = shift;
184             my $r;
185            
186             $r=$self->$orig(@_);
187             $r->login;
188             return $r;
189             };
190             sub _url
191             {
192 0     0     my ($self) = @_;
193 0 0         return 'https://demo-api.ig.com/gateway/deal' if ( $self->isdemo);
194 0           return 'https://api.ig.com/gateway/deal';
195             }
196              
197              
198             ##########################################################################
199             =head2 login - loginto the account.
200              
201             Parameters - none
202              
203             login to the object, using the parameters provided to new.
204              
205             You should call this just once per object after calling new.
206              
207             =cut
208             ##########################################################################
209             sub login {
210 0     0 1   my ($self) = @_;
211 0           my $headers =
212             {
213             'Content-Type' => 'application/json; charset=UTF-8',
214             'Accept' => 'application/json; charset=UTF-8',
215             VERSION => 2,
216             'X-IG-API-KEY'=> $self->apikey
217             };
218 0           my $data = {
219             identifier => $self->username,
220             password => $self->password,
221             };
222             # my $jdata = encode_json($data);
223 0           my $jdata=JSON->new->canonical->encode($data);
224              
225 0           my $client = REST::Client->new();
226 0           $client->setHost($self->_url);
227              
228              
229 0           $client->POST (
230             '/session',
231             $jdata,
232             $headers
233             );
234 0           my $code=$client->responseCode();
235 0 0         die "response code from login $code" if ($code!=200);
236 0   0       $self->CST($client->responseHeader('CST') // die "No CST header in login response");
237 0   0       $self->XSECURITYTOKEN($client->responseHeader('X-SECURITY-TOKEN') // die "No X-SECURITY-TOKEN in login response header");
238 0           $self->XSTTIME(time());
239 0           return;
240             }
241             ##########################################################################
242              
243             =head2 flatten
244              
245             Parameters
246             1 Ref to array of hashes to flatten or a ref to a hash to flatten
247             2 ref to an array of items to flatten, or just a single item name.
248              
249             Typical use of this is for a position that as it comes back from IG contains a market and a position
250             byut we would prefer all items at the top level. This would moves all the keys of position and market up one level and
251             would remove the keys market and position.
252              
253             $self->flatten($hash, [qw(market position)]);
254              
255             =cut
256             ##########################################################################
257             sub flatten
258             {
259 0     0 1   my ($self)=shift;
260 0           my ($hash)=shift;
261 0           my ($toflatten)=shift;
262              
263 0 0         $hash=[$hash] if (ref($hash) ne 'ARRAY');
264 0 0         $toflatten=[$toflatten] if (ref($toflatten) ne 'ARRAY');
265              
266 0           for my $h (@$hash)
267             {
268 0           for my $key (@$toflatten)
269             {
270 0 0         if (exists($h->{$key}))
271             {
272 0 0         if (defined($h->{$key}))
273             {
274 0 0         die "key $key to flatten is not a hash" if (ref($h->{$key}) ne 'HASH');
275 0           for my $subkey (keys %{$h->{$key}})
  0            
276             {
277 0 0         die "subkey exists $subkey" if (exists($h->{$subkey}));
278 0           $h->{$subkey}=$h->{$key}->{$subkey};
279             }
280             }
281 0           delete $h->{$key};
282             }
283             }
284             }
285             }
286             sub transactions
287             {
288              
289 0     0 1   my ($self) = shift;
290 0           my ($pageNumber)=shift;
291 0           my ($from) =shift;
292 0           my ($to)=shift;
293              
294 0           my $pageSize=50;
295              
296 0   0       $from//='';
297 0   0       $to//='';
298              
299 0 0         if (ref($to) eq 'Time::Piece')
300             {
301 0           $to=$to->strftime("%Y-%m-%dT%H:%M:%S");
302             }
303 0 0         if (ref($from) eq 'Time::Piece')
304             {
305 0           $from=$from->strftime("%Y-%m-%dT%H:%M:%S");
306             }
307 0 0         $to=~m/^[-0-9T:]*$/ or die "Invalid date format for 'to' $to, is a ".ref(\$to);
308 0 0         $from=~m/^[-0-9T:]*$/ or die "Invalid date format for 'from' $from";
309              
310 0           my $headers = {
311             'Content-Type' => 'application/json; charset=UTF-8',
312             'Accept' => 'application/json; charset=UTF-8',
313             VERSION => 2,
314             CST=>$self->CST,
315             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
316             'X-IG-API-KEY'=> $self->apikey,
317             };
318             #my $jheaders = encode_json($headers);
319 0           my $jheaders=JSON->new->canonical->encode($headers);
320              
321 0           my $client = REST::Client->new();
322 0           $client->setHost($self->_url);
323              
324 0 0         $from and $from="from=$from";
325 0 0         $to and $to="to=$to";
326 0           my $rpage=$pageNumber; # requested page number as integer, 1 is first
327 0 0         $pageNumber and $pageNumber="pageNumber=$pageNumber";
328 0 0         $pageSize and $pageSize="pageSize=$pageSize";
329              
330 0 0         my $url=join '', map { $_?'&'.$_:'' } ($from,$to,$pageNumber,$pageSize);
  0            
331 0           $url=~s/^&//;
332 0 0         $url='?'.$url if ($url);
333              
334 0           $url='/history/transactions'.$url;
335 0           $client->GET (
336             $url,
337             $headers
338             );
339              
340 0           my $code=$client->responseCode();
341 0 0         if ($code==200)
342             {
343 0           my $resp=decode_json($client->responseContent());
344             # $resp=$self->flatten($resp,[qw/transactions metadata/]);
345             #die encode_json($resp);
346              
347 0           my @activities=@{$resp->{transactions}};
  0            
348             # pncerint encode_json( $resp->{metadata} );
349             # {"pageData":{"totalPages":11,"pageNumber":11,"pageSize":50},"size":534}***** 34
350 0 0         return undef if ($rpage > $resp->{metadata}->{pageData}->{pageNumber});
351             # return undef if (@activities==0);
352 0           return \@activities;
353             }
354             else
355             {
356 0           print "failed $code: ".$client->responseContent()."\n";
357 0           return undef;
358             }
359             }
360              
361             # example from/ to sting format:
362             # 2020-10-28
363             # 2020-10-28T15:30
364              
365             # keys in retirn, when called with detailed=1
366             # type, goodTillDate, actions(ARRAY) , epic, direction, level, channel, marketName, date, dealReference, guaranteedStop, stopLevel, size, currency, stopDistance, trailingStep, status, trailingStopDistance, limitLevel, description, dealId, period, limitDistance
367             # without:
368             # period, details, date, dealId, epic, description, channel, status, type
369              
370             sub history
371             {
372 0     0 1   my ($self) = shift;
373 0           my ($detailed)=shift; ## undef, not detailed, 1 for detailed.
374 0           my ($pageNumber)=shift;
375              
376              
377 0           my ($from) = shift;
378 0           my ($to) = shift;
379              
380 0   0       $pageNumber//='';
381 0           my $pageSize=50;
382              
383 0   0       $from//='';
384 0   0       $to//='';
385              
386 0 0         if (ref($to) eq 'Time::Piece')
387             {
388 0           $to=$to->strftime("%Y-%m-%dT%H:%M:%S");
389             }
390 0 0         if (ref($from) eq 'Time::Piece')
391             {
392 0           $from=$from->strftime("%Y-%m-%dT%H:%M:%S");
393             }
394              
395 0 0         $to=~m/^[-0-9T:]*$/ or die "Invalid date format for 'to' $to";
396 0 0         $from=~m/^[-0-9T:]*$/ or die "Invalid date format for 'from' $from";
397              
398 0           my $headers = {
399             'Content-Type' => 'application/json; charset=UTF-8',
400             'Accept' => 'application/json; charset=UTF-8',
401             VERSION => 2,
402             CST=>$self->CST,
403             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
404             'X-IG-API-KEY'=> $self->apikey,
405             };
406             #my $jheaders = encode_json($headers);
407 0           my $jheaders=JSON->new->canonical->encode($headers);
408              
409 0           my $client = REST::Client->new();
410 0           $client->setHost($self->_url);
411 0 0         $from="from=$from" if ($from ne '');
412 0 0         $to="to=$to" if ($to ne '');
413 0 0         if ($detailed)
414             {
415 0           $detailed="detailed=true"
416             }
417             else
418             {
419 0           $detailed='';
420             }
421              
422 0 0         $pageNumber="pageNumber=$pageNumber" if ($pageNumber);
423 0   0       $pageSize//='';
424 0 0         $pageSize="pageSize=$pageSize" if ($pageSize);
425              
426             # my $sep='?';
427             # map { $_ eq '' or $_=$sep.$_ and $sep='&'} ($from,$to,$detailed,$pageNumber,$pageSize);
428              
429 0 0         my $url=join '', map { $_?'&'.$_:'' } ($from,$to,$detailed,$pageNumber,$pageSize);
  0            
430 0           $url=~s/^&//;
431 0 0         $url='?'.$url if ($url);
432              
433 0           $url='/history/activity'.$url;
434              
435             # die $url;
436              
437 0           $client->GET (
438             $url,
439             $headers
440             );
441              
442 0           my $code=$client->responseCode();
443              
444 0 0         if ($code==200)
445             {
446 0           my $resp=decode_json($client->responseContent());
447 0           my @activities=@{$resp->{activities}};
  0            
448 0 0         return undef if (@activities==0);
449 0           $self->flatten(\@activities,'details');
450 0           return \@activities;
451             }
452             else
453             {
454 0           print "failed $code: ".$client->responseContent()."\n";
455 0           return undef;
456             }
457             }
458             # example response:
459             #{"metadata":{"paging":{"size":50,"next":"/history/activity?version=3&from=2020-10-28T00:00:00&to=2020-10-29T16:41:45&detailed=false&pageSize=50"}}
460             # "activities": [.... ]
461             # }
462             # each activity looks like:
463             #{
464             # details=>null,
465             # dealId=>"DIAAAAFRS39HJAK",
466             # period=>"DFB",
467             # type=>"POSITION",
468             # epic=>"UA.D.ATVI.DAILY.IP",
469             # description=>"Position partially closed=> J6GK8WA9",
470             # date=>"2020-10-29T17:47:46",
471             # status=>"ACCEPTED",
472             # channel=>"SYSTEM"
473             #
474             # or with detail
475             # {"activities":
476             # [
477             # [
478             # {"date":"2020-11-19T18:41:04",
479             # "epic":"UC.D.MU.DAILY.IP",
480             # "period":"DFB",
481             # "dealId":"DIAAAAFVXZV5LA5",
482             # "channel":"WEB",
483             # "type":"POSITION",
484             # "status":"ACCEPTED",
485             # "description":"Position opened: VXZV5LA5",
486             # "details":
487             # {
488             # "dealReference":"6XQESB1EQGWY4FR2",
489             # "actions":
490             # [
491             # {"actionType":"POSITION_OPENED",
492             # "affectedDealId":"DIAAAAFVXZV5LA5"
493             # }
494             # ],
495             # "marketName":"Micron Technology Inc (All Sessions)",
496             # "goodTillDate":null,
497             # "currency":"GBP",
498             # "size":0.4,
499             # "direction":"BUY",
500             # "level":6123,
501             # "stopLevel":null,
502             # "stopDistance":null,
503             # "guaranteedStop":false,
504             # "trailingStopDistance":null,
505             # "trailingStep":null,
506             # "limitLevel":null,
507             # "limitDistance":null
508             # }
509             # },
510             # {"date":"2020-11-17T11:33:52",
511             #"epic":"KA.D.FSTA.DAILY.IP",
512             #"period":"DFB",
513             #"dealId":"DIAAAAFVEFD4GAG",
514             #"channel":"WEB",
515             #"type":"POSITION",
516             #"status":"ACCEPTED",
517             #"description":"Position/s closed: HH93GXAZ",
518             #"details":{"dealReference":"6XQESB1EQAZNR6V3",
519             #"actions":[{"actionType":"POSITION_CLOSED",
520             #"affectedDealId":"DIAAAAFHH93GXAZ"}],
521             #"marketName":"Fuller Smith & Turner",
522             #"goodTillDate":null,
523             #"currency":"GBP",
524             #"size":1,
525             #"direction":"SELL",
526             #"level":726.2,
527             #"stopLevel":null,
528             #"stopDistance":null,
529             #"guaranteedStop":false,
530             #"trailingStopDistance":null,
531             #"trailingStep":null,
532             #"limitLevel":null,
533             #"limitDistance":null}},
534             #}
535              
536             # with detailed=1
537              
538             #{
539             # "activities": [
540             # {
541             # "date": "2020-11-19T18:41:04",
542             # "epic": "UC.D.MU.DAILY.IP",
543             # "period": "DFB",
544             # "dealId": "DIAAAAFVXZV5LA5",
545             # "channel": "WEB",
546             # "type": "POSITION",
547             # "status": "ACCEPTED",
548             # "description": "Position opened: VXZV5LA5",
549             # "details": {
550             # "dealReference": "6XQESB1EQGWY4FR2",
551             # "actions": [
552             # {
553             # "actionType": "POSITION_OPENED",
554             # "affectedDealId": "DIAAAAFVXZV5LA5"
555             # }
556             # ],
557             # "marketName": "Micron Technology Inc (All Sessions)",
558             # "goodTillDate": null,
559             # "currency": "GBP",
560             # "size": 0.4,
561             # "direction": "BUY",
562             # "level": 6123,
563             # "stopLevel": null,
564             # "stopDistance": null,
565             # "guaranteedStop": false,
566             # "trailingStopDistance": null,
567             # "trailingStep": null,
568             # "limitLevel": null,
569             # "limitDistance": null
570             # }
571             # },
572             # {
573             # "date": "2020-11-17T11:33:52",
574             # "epic": "KA.D.FSTA.DAILY.IP",
575             # "period": "DFB",
576             # "dealId": "DIAAAAFVEFD4GAG",
577             # "channel": "WEB",
578             # "type": "POSITION",
579             # "status": "ACCEPTED",
580             # "description": "Position/s closed: HH93GXAZ",
581             # "details": {
582             # "dealReference": "6XQESB1EQAZNR6V3",
583             # "actions": [
584             # {
585             # "actionType": "POSITION_CLOSED",
586             # "affectedDealId": "DIAAAAFHH93GXAZ"
587             # }
588             # ],
589             # "marketName": "Fuller Smith & Turner",
590             # "goodTillDate": null,
591             # "currency": "GBP",
592             # "size": 1,
593             # "direction": "SELL",
594             # "level": 726.2,
595             # "stopLevel": null,
596             # "stopDistance": null,
597             # "guaranteedStop": false,
598             # "trailingStopDistance": null,
599             # "trailingStep": null,
600             # "limitLevel": null,
601             # "limitDistance": null
602             # }
603             # },
604             # {
605             # "date": "2020-11-17T11:33:09",
606             # "epic": "KA.D.FSTA.DAILY.IP",
607             # "period": "DFB",
608             # "dealId": "DIAAAAFVEFBBKA4",
609             # "channel": "WEB",
610             # "type": "POSITION",
611             # "status": "ACCEPTED",
612             # "description": "Position opened: VEFBBKA4",
613             # "details": {
614             # "dealReference": "6XQESB1EQAZKR1V2",
615             # "actions": [
616             # {
617             # "actionType": "POSITION_OPENED",
618             # "affectedDealId": "DIAAAAFVEFBBKA4"
619             # }
620             # ],
621             # "marketName": "Fuller Smith & Turner",
622             # "goodTillDate": null,
623             # "currency": "GBP",
624             # "size": 2,
625             # "direction": "BUY",
626             # "level": 779.9,
627             # "stopLevel": null,
628             # "stopDistance": null,
629             # "guaranteedStop": false,
630             # "trailingStopDistance": null,
631             # "trailingStep": null,
632             # "limitLevel": null,
633             # "limitDistance": null
634             # }
635             # },
636             # {
637             # "date": "2020-11-16T17:17:29",
638             # "epic": "UD.D.WIXUS.DAILY.IP",
639             # "period": "DFB",
640             # "dealId": "DIAAAAFU94TQRAR",
641             # "channel": "WEB",
642             # "type": "POSITION",
643             # "status": "ACCEPTED",
644             # "description": "Position opened: U94TQRAR",
645             # "details": {
646             # "dealReference": "6XQESB1EQ90XNSR2",
647             # "actions": [
648             # {
649             # "actionType": "POSITION_OPENED",
650             # "affectedDealId": "DIAAAAFU94TQRAR"
651             # }
652             # ],
653             # "marketName": "Wix.com Ltd",
654             # "goodTillDate": null,
655             # "currency": "GBP",
656             # "size": 0.31,
657             # "direction": "BUY",
658             # "level": 24142,
659             # "stopLevel": null,
660             # "stopDistance": null,
661             # "guaranteedStop": false,
662             # "trailingStopDistance": null,
663             # "trailingStep": null,
664             # "limitLevel": null,
665             # "limitDistance": null
666             # }
667             # },
668             # {
669             # "date": "2020-11-16T17:08:33",
670             # "epic": "UD.D.ZMUS.DAILY.IP",
671             # "period": "DFB",
672             # "dealId": "DIAAAAFU924B7A3",
673             # etc....
674             ##########################################################################
675             #
676              
677             =head2 accounts - retrieve a list of accounts
678              
679             Parameters - none
680              
681             Return value - Array ref containing hashes of accounts.
682              
683             =cut
684             ##########################################################################
685             sub accounts
686             {
687 0     0 1   my ($self) = shift;
688              
689 0           my $headers = {
690             'Content-Type' => 'application/json; charset=UTF-8',
691             'Accept' => 'application/json; charset=UTF-8',
692             VERSION => 1,
693             CST=>$self->CST,
694             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
695             'X-IG-API-KEY'=> $self->apikey,
696             };
697             #my $jheaders = encode_json($headers);
698 0           my $jheaders=JSON->new->canonical->encode($headers);
699              
700 0           my $client = REST::Client->new();
701 0           $client->setHost($self->_url);
702 0           my $r=$client->GET ( '/accounts', $headers);
703              
704 0           my $resp=decode_json($client->responseContent());
705              
706 0           my $accounts=[];
707 0           @$accounts=@{$resp->{accounts}};
  0            
708              
709 0           return $accounts;
710              
711             }
712              
713             # Typical return data:
714             #[
715             # {"accountId":"...",
716             # "status":"ENABLED",
717             # "canTransferFrom":true,
718             # "preferred":true,
719             # "accountAlias":null,
720             # "accountType":"SPREADBET",
721             # "accountName":"Spread bet",
722             # "balance":{
723             # "deposit":89051.36,
724             # "balance":152475.8,
725             # "available":85942.65,
726             # "profitLoss":22518.21
727             # },
728             # "canTransferTo":true,
729             # "currency":"GBP"
730             # },
731             # {"accountId":"...",
732             # "status":"ENABLED",
733             # "canTransferFrom":true,
734             # "preferred":false,
735             # "accountAlias":null,
736             # "accountType":"CFD",
737             # "accountName":"CFD",
738             # "balance":{
739             # "available":0,
740             # "profitLoss":0,
741             # "balance":0,
742             # "deposit":0
743             # },
744             # "canTransferTo":true,
745             # "currency":"GBP"
746             # }
747             #]
748             ##########################################################################
749             #
750             # Return a ref to an array of positions. Each position is
751             # a variable structure deep hash
752             #
753             ##########################################################################
754             sub positions
755             {
756 0     0 1   my ($self) = shift;
757              
758 0           my $headers = {
759             'Content-Type' => 'application/json; charset=UTF-8',
760             'Accept' => 'application/json; charset=UTF-8',
761             VERSION => 2,
762             # 'IG-ACCOUNT-ID'=> $accountid,
763             CST=>$self->CST,
764             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
765             'X-IG-API-KEY'=> $self->apikey,
766             };
767             #my $jheaders=JSON->new->canonical->encode($headers); # for debug
768              
769 0           my $client = REST::Client->new();
770 0           $client->setHost($self->_url);
771             #my $r;
772             # $headers->{VERSION}=2;
773             #$r=$client->GET (
774 0           $client->GET ( '/positions',
775             $headers
776             );
777 0           my $resp=decode_json($client->responseContent());
778              
779 0           my $positions=[];
780 0           @$positions=@{$resp->{positions}};
  0            
781              
782 0           return $positions;
783             }
784             # example of the structure of a position
785             # Regeneron Pharmaceuticals Inc, 0.06
786             # {
787             # "position" : {
788             # "trailingStopDistance" : null,
789             # "size" : 0.06,
790             # "limitedRiskPremium" : null,
791             # "stopLevel" : 50128,
792             # "direction" : "BUY",
793             # "level" : 50303,
794             # "dealReference" : "6XQESB1E506WW334",
795             # "controlledRisk" : false,
796             # "currency" : "GBP",
797             # "contractSize" : 1,
798             # "createdDateUTC" : "2020-04-03T14:26:07",
799             # "trailingStep" : null,
800             # "createdDate" : "2020/04/03 15:26:07:000",
801             # "limitLevel" : null,
802             # "dealId" : "DIAAAAEL2T7AEAS"
803             # },
804             # "market" : {
805             # "lotSize" : 1,
806             # "marketStatus" : "EDITS_ONLY",
807             # "instrumentType" : "SHARES",
808             # "expiry" : "DFB",
809             # "streamingPricesAvailable" : false,
810             # "instrumentName" : "Regeneron Pharmaceuticals Inc",
811             # "offer" : 60261,
812             # "delayTime" : 0,
813             # "updateTime" : "20:59:56",
814             # "high" : 61455,
815             # "percentageChange" : -2.01,
816             # "netChange" : -1236,
817             # "low" : 59886,
818             # "bid" : 60261,
819             # "updateTimeUTC" : "19:59:56",
820             # "scalingFactor" : 1,
821             # "epic" : "UC.D.REGN.DAILY.IP"
822             # }
823             # }
824             #####################################################################
825             # Aggregate an array of positions into an array of unique
826             # positions with 1 element per instrument, Items will be combined
827             # where more than one position is combined, in a field dependent way.
828             # for exeample sizes will be added as will be profit
829             # a reference to an array is expected and a reference to a new array
830             # returned.
831             #####################################################################
832              
833             =head2 agg - aggregate positions into a flattened 1 element per instrument form.
834              
835             Parameters
836              
837             1 Reference to an array of positions
838             2 (Optional) Ref to an array of keys to sort on
839              
840             agg does three things actually. First, it joins together multiple positions of the same instrument,
841             generating sensible values for things like profit/loss and size
842              
843             Second, it performs some flattening of the deep structure for a position which comes from IG.
844              
845             Third it sorts the result. The default sort order I use is -profitpc instrumentName, but
846             you can provide a 2rd parameter, a reference to an array of items to sort by.
847             Each item can optionally be preceeded by - to reverse the prder. If the first item equates equal, then
848             the next item is used.
849              
850             =cut
851             #####################################################################
852             sub agg
853             {
854 0     0 1   my ($self,$positions,$sortlist)=@_;
855 0           my %totals; # aggregated totals as arrays of individuals.
856              
857 0           $self->flatten($positions, [qw/market position/]);
858 0           for my $position (@$positions)
859             {
860              
861 0           my $json = JSON->new;
862 0 0         $position->{size}=-abs($position->{size}) if ($position->{direction} eq 'SELL');
863             # $position->{size}= -abs($position->{size}) if ($position->{direction}//'' ne 'BUY');
864 0           $position->{profit}=($self->fetch($position,'bid')-$self->fetch($position,'level'))*$self->fetch($position,'size');
865              
866 0 0         $position->{held}=Time::Piece->strptime($position->{createdDateUTC},"%Y-%m-%dT%H:%M:%S") or die "strptime failed for ".$position->{createdDateOnly};
867 0           $position->{held}=(gmtime()-$position->{held})/(24*3600);
868              
869 0   0       my $ra=($totals{$position->{instrumentName}}||=[]);
870 0           push(@$ra,$position);
871              
872             }
873              
874             # totals is a hash on instrument name each element is a pointer to an array of positions for the same instrument.
875              
876 0           my $aggregated=[];
877 0           for my $total (values %totals)
878             { # for one particular name
879 0           my $position={}; # initialise the new aggregate position
880              
881 0           $position->{profit}=0;
882 0           $position->{size}=0;
883 0           $position->{held}=0;
884 0           $position->{stopLevel}=[];
885 0           $position->{createdDate}=[];
886 0           $position->{createdDateUTC}=[];
887              
888 0           for my $subtotal ( @$total) # go through all the positions for that one name
889             {
890 0   0       $position->{instrumentName}//=$subtotal->{instrumentName};
891 0           $position->{size}+=$subtotal->{size};
892 0           my $h;
893 0 0         $h=Time::Piece->strptime($subtotal->{createdDateUTC},"%Y-%m-%dT%H:%M:%S") or die "strptime failed for ".$subtotal->{createdDateOnly};
894 0           $h=(gmtime()-$h)/(24*3600);
895 0           $h=int($h*10)/10;
896 0           $subtotal->{held}=$h;
897 0           $position->{held}+=$subtotal->{held}*$subtotal->{size}; # this is a size-weighted average. Needs division by total size.
898 0   0       $position->{bid}//=$subtotal->{bid};
899 0           $position->{profit}+=$subtotal->{profit} ;
900 0   0       $position->{epic}//=$subtotal->{epic};
901              
902 0   0       $position->{currency}//=$subtotal->{currency};
903 0   0       $position->{marketStatus}//=$subtotal->{marketStatus};
904              
905 0 0         push(@{$position->{stopLevel}},$subtotal->{stopLevel}) if $subtotal->{stopLevel};
  0            
906 0           push(@{$position->{createdDate}},$subtotal->{createdDate});
  0            
907 0           push(@{$position->{createdDateUTC}},$subtotal->{createdDateUTC});
  0            
908             }
909              
910             # now we have various housekeeping to do in some cases, eg where an average is calculated as a sum above, we divide by the number to get a true mean.
911             ###########
912              
913 0 0         $position->{held}=sprintf("%0.1f",$position->{held}/$position->{size}); $position->{held}.=" av" if (@$total>1);
  0            
914              
915              
916 0           $position->{level}=$position->{bid}-$position->{profit}/$position->{size}; # open level for multiple positions
917              
918 0 0         $position->{profitpc}=int(0.5+1000*$position->{profit}/($position->{level}*abs($position->{size})))/10 if ($position->{level}>0);
919              
920 0           $position->{atrisk}=$position->{bid}*$position->{size};
921              
922 0           $position->{createdDate}=$self->sortrange($position->{createdDate});
923 0           $position->{createdDateUTC}=$self->sortrange($position->{createdDateUTC});
924 0           $position->{createdDateOnly}=$position->{createdDate};
925 0           $position->{createdDateOnly}=~s/T[^-]+//g;
926              
927 0 0         $position->{slpc}=join(',',map { $_?(int(1000.0*$_/$position->{bid})/10):''} @{$position->{stopLevel}});
  0            
  0            
928 0           $position->{stopLevel}=join(',',@{$position->{stopLevel}});
  0            
929            
930             ###########
931             # end of aggregated operations
932              
933              
934 0           push(@$aggregated,$position);
935             }
936              
937             # @$aggregated=sort { $b->{profitpc}<=>$a->{profitpc} } @$aggregated;
938 0   0       $sortlist//=[qw(-profitpc instrumentName)]; # default sort
939 0           $self->sorter($sortlist,$aggregated);
940 0           return $aggregated;
941              
942             }
943             # like agg, but do not do actual aggregation.
944             # so we sort, add certain extra characteristics but thats all.
945             ##########################################################################
946             #
947              
948             =head2 nonagg - like agg but do not do actual aggregation
949              
950             Parameters
951              
952             1 Reference to an array of positions
953             2 (Optional) Ref to an array of keys to sort on
954              
955             Return value - Array ref containing hashes of accounts. Should be the same size as the original.
956              
957             =cut
958             ##########################################################################
959             #sub nonagg
960             #{
961             # my ($self,$positions,$sortlist)=@_;
962             # my %totals; # aggregated totals as arrays of individuals.
963             #
964             # $self->flatten($positions, [qw/market position/]);
965             # for my $position (@$positions)
966             # {
967             #
968             # my $json = JSON->new;
969             #
970             # $position->{profit}=($self->fetch($position,'bid')-$self->fetch($position,'level'))*$self->fetch($position,'size');
971             # # create new profits element
972             #
973             # my $open=$position->{bid}-$position->{profit}/$position->{size};
974             # $position->{level}=$open;
975             # $position->{profitpc}=int(0.5+1000*$position->{profit}/($position->{level}*$position->{size}))/10;
976             # $position->{atrisk}=$position->{bid}*$position->{size};
977             # $position->{createdDateOnly}=$position->{createdDate};
978             # $position->{createdDateOnly}=~s/ .*$//;
979             # }
980             #
981             # $sortlist//=[qw(-profitpc instrumentName)]; # default sort
982             # $self->sorter($sortlist,$positions);
983             # return $positions;
984             #}
985             sub nonagg
986             {
987 0     0 1   my ($self,$positions,$sortlist)=@_;
988 0           my %totals; # aggregated totals as arrays of individuals.
989              
990 0           $self->flatten($positions, [qw/market position/]);
991 0           for my $position (@$positions)
992             {
993              
994 0           my $json = JSON->new;
995              
996 0 0         $position->{size}=-abs($position->{size}) if ($position->{direction} eq 'SELL');
997 0           $position->{profit}=($position->{bid}-$position->{level})*$position->{size};
998             # create new profits element
999              
1000             # my $open=$position->{bid}-$position->{profit}/$position->{size};
1001             # $position->{level}=$open;
1002 0           $position->{profitpc}=int(0.5+1000*$position->{profit}/($position->{level}*abs($position->{size})))/10;
1003 0           $position->{atrisk}=$position->{bid}*$position->{size};
1004 0           $position->{createdDateOnly}=$position->{createdDate};
1005 0           $position->{createdDateOnly}=~s/ .*$//;
1006 0 0         $position->{held}=Time::Piece->strptime($position->{createdDateUTC},"%Y-%m-%dT%H:%M:%S") or die "strptime failed for ".$position->{createdDateOnly};
1007 0           $position->{held}=(gmtime()-$position->{held})/(24*3600);
1008 0           $position->{held}=int($position->{held}*10+0.5)/10;
1009 0           $position->{dailyp}='';
1010 0 0         $position->{dailyp}=((1+$position->{profitpc}/100.0)**(1/$position->{held})-1)*100 if ($position->{held}>0);
1011            
1012             }
1013              
1014 0   0       $sortlist//=[qw(-profitpc instrumentName)]; # default sort
1015 0           $self->sorter($sortlist,$positions);
1016 0           return $positions;
1017             }
1018             ####################################################################
1019             # General array sort function.
1020             # Given an array of hash refs, and a sort key
1021             # considtying of an array of an array of keys to the hashes
1022             # sort in place the array.
1023             #
1024             # sortkey, arrayref of keys. Sort order direction reversed
1025             # if key has - appended to start, eg -profitpc gives largest first
1026             # pos array eo be sorted, its an inplace sort.
1027             # uses the determinant $x eq $x+0 to determine if numeric or not.
1028             # improvements: may need to use a deep fetch to locate the items
1029             ####################################################################
1030              
1031             =head2 sorter - general array sort function for an array of hashes
1032              
1033             Parameters
1034              
1035             1 Ref to array of keys to sort. Each my be prefixed with a - to
1036             reverse the order on that key. If keys compare equal the next key is used.
1037             2 Ref to an array of positions to sort.
1038              
1039             The array is sorted in-place. A numeric comparison is done if for
1040             both items $x == $x+0
1041              
1042             Formatted datetimes are correctly sorted.
1043              
1044             =cut
1045             ####################################################################
1046             sub sorter
1047             {
1048 0     0 1   my ($self,$sortkey,$pos)=@_;
1049              
1050             @$pos= sort {
1051 0           my ($result)=0;
  0            
1052 0           for my $fkey (@$sortkey)
1053             {
1054 0           my $key=$fkey;
1055 0           my $dir=1;
1056 0 0         $dir=-1 if ($key=~s/^-//);
1057             # die "key=$key value=$b->{createdDateUTC} keys are ".join(', ',keys %$a); ;
1058 0 0 0       next if (!exists($a->{$key}) or !exists($b->{$key}));
1059 0           my ($x1,$x2)=($a->{$key},$b->{$key});
1060 0           map { s/[£%]//g } ($x1,$x2);
  0            
1061              
1062 1     1   10 { no warnings qw(numeric);
  1         1  
  1         5515  
  0            
1063 0           my $warning;
1064              
1065 0 0 0       if ($x1 eq $x1+0 and $x2 eq $x2+0)
1066             {
1067 0           $result=$x1<=>$x2;
1068             }
1069             else
1070             { # note that this correctly handles a formatted date
1071 0           $result=$x1 cmp $x2;
1072             }
1073             }
1074 0 0         return $result*$dir if ($result);
1075             }
1076 0           return 0;
1077             }
1078             @$pos;
1079              
1080             }
1081             ####################################################################
1082             # The idea is this will close all the supplied positions, optionally returning a reference to
1083             # either/both an array of closed/non closed positions;
1084             # This is not quite working yet, needs more work,
1085             ####################################################################
1086              
1087             =head2 close - close the supplied positions.
1088              
1089              
1090              
1091             Parameters
1092              
1093             1 Ref to array of positions to close.
1094             reverse the order on that key.
1095             2/3 ref to done / notdone arrays to sort succesful / failed
1096             closes in to.
1097              
1098             The idea is this will close all the supplied positions, optionally returning a reference to
1099              
1100              
1101             =head3 Status - very experimental.
1102              
1103             Contains die / print statements that you may wish to remove
1104              
1105             =cut
1106             ####################################################################
1107             sub close
1108             {
1109 0     0 1   my $self=shift;
1110 0           my $positions=shift; # to close
1111 0           my $done=shift;
1112 0           my $notdone=shift;
1113              
1114 0           my $verbose=0;
1115              
1116 0           my @done;
1117             my @notdone;
1118              
1119 0           my $headers = {
1120             'Content-Type' => 'application/json; charset=UTF-8',
1121             'Accept' => 'application/json; charset=UTF-8',
1122             VERSION => 1,
1123             # 'IG-ACCOUNT-ID'=> $accountid,
1124             CST=>$self->CST,
1125             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
1126             'X-IG-API-KEY'=> $self->apikey,
1127             '_method'=>'DELETE',
1128             };
1129              
1130 0           my $data = {
1131             #encryptedPassword => "false",
1132             #identifier => $self->username,
1133             #password => $self->password
1134             #direction => 'BUY',
1135             # epic=>
1136             # expiry=>
1137             orderType=>'MARKET',
1138             #size=>0.1
1139             ##guaranteedStop=>'false',
1140             forceOpen=>'true',
1141             #timeInForce => "EXECUTE_AND_ELIMINATE", # "GOOD_TILL_CANCELLED"
1142             timeInForce => "", # "GOOD_TILL_CANCELLED"
1143             };
1144 0           my $client = REST::Client->new();
1145              
1146 0           $client->setHost($self->_url);
1147              
1148              
1149 0           my %existhash;
1150 0           map { $existhash{$self->fetch($_,'epic')}=$_ } @$positions; # creat a hash on epic
  0            
1151              
1152 0           for my $position (@$positions)
1153             {
1154             # die dump($position);
1155              
1156 0           my $existingsize=0;
1157 0           my $epic=$self->fetch($position,'epic');
1158 0           my $name=$self->fetch($position,'instrumentName');
1159              
1160 0           my $ms=$self->fetch($position,'marketStatus');
1161              
1162 0 0         if ($ms ne 'TRADEABLE')
1163             {
1164 0           push(@notdone,$position);
1165 0           print "$name, market status is $ms\n";
1166 0           next;
1167             }
1168              
1169              
1170             #$data->{epic}=$self->fetch($position,'epic');
1171 0           $data->{epic}=$epic;
1172 0           $data->{size}=$self->fetch($position,'size');
1173             # $data->{currencyCode}=$self->fetch($position,'currency');
1174 0           $data->{expiry}='DFB';
1175             # $data->{expiry}='-';
1176 0           $data->{direction}='SELL';
1177              
1178             #my $jdata = encode_json($data);
1179 0           my $jdata=JSON->new->canonical->encode($data);
1180 0           $client->PUT (
1181             '/positions/otc',
1182             $jdata,
1183             $headers
1184             );
1185 0           my $code=$client->responseCode();
1186 0 0         if ($code==200)
1187             {
1188 0           my $resp=decode_json($client->responseContent());
1189 0           my $dealReference=$resp->{dealReference};
1190 0           print "$name, dr=$dealReference\n";
1191 0 0 0       if (defined $dealReference and length($dealReference)>5)
1192             {
1193 0           push(@done,$position);
1194 0           die;
1195 0           next;
1196 0           next;
1197             }
1198             }
1199             else
1200             {
1201 0           print "$name failed $code: ".$client->responseContent()."\n";
1202 0           push(@notdone,$position);
1203             }
1204             }
1205 0 0         @$done=@done if ($done);
1206 0 0         @$notdone=@notdone if ($notdone);
1207              
1208 0           printf "done=%d notdone=%d\n",0+@done,0+@notdone;
1209 0           print "notdone:\n";
1210              
1211 0           my $cpc='%%';
1212 0           my $format="%-41sinstrumentName %+4.2fsize %-9.2flevel ".
1213             "%-8.2fbid £%-8.2fprofit %4.1fprofitpc%% £%10.2fatrisk %-9sstopLevel %-4sslpc$cpc\n";
1214              
1215 0           $self->printpos("stdout" , ['Name','Size','Open','Latest','P/L','P/L%','Value','Stop','Stop'], $format);
1216              
1217 0           map { $self->printpos("stdout" , $_, $format) } @notdone;
  0            
1218              
1219             }
1220             #####################################################################
1221             # given a ref to an array of positions, attempt to buy the same
1222             # position in this object.
1223             # if the position already exists or is succesfully brought, count as success.
1224             # If the buy fails, include it in the returned list.
1225             # If all buys succesful then return an empty list.
1226             # done and notdone references may be supplied and if they are these should point to arrays
1227             # of the succesful and unsuccesful positions.
1228             # return value is NOT now used.
1229             # ignortradeable ... use this if the positionis an old one, so that tradeable status could
1230             # be out of date.
1231             #####################################################################
1232              
1233             =head2 buy - attempt to buy a number of instruments.
1234              
1235             Parameters
1236              
1237             1 Reference to an array of positions
1238             2 Optional ref to an array done, to be filled with succesful buys
1239             3 Optional ref to an array notdone, to be filled with the failed
1240             4 ignore tradeable, one of the fields in a position relates to the market
1241             being open or closed (TRADEABLE) If this field is current, its a
1242             good indication to skip this one (place it in the notdone array.
1243             But if its out of date then setting this flag 1 attempts the trade
1244             anyway.
1245              
1246             Attempt to buy positions. I have used this to move positions
1247             between a demo account and real account or vice-versa.
1248              
1249             =head3 Status - very experimental.
1250              
1251             Contains print statements that should
1252             probably be removed.
1253              
1254             =cut
1255             #####################################################################
1256             sub buy
1257             {
1258 0     0 1   my $self=shift;
1259 0           my $positions=shift; # to buy
1260 0           my $done=shift;
1261 0           my $notdone=shift;
1262 0           my $ignoretradeable=shift;
1263              
1264 0           my $verbose=0;
1265              
1266 0           my @done;
1267             my @notdone;
1268 0           my $headers = {
1269             'Content-Type' => 'application/json; charset=UTF-8',
1270             'Accept' => 'application/json; charset=UTF-8',
1271             VERSION => 2,
1272             # 'IG-ACCOUNT-ID'=> $accountid,
1273             CST=>$self->CST,
1274             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
1275             'X-IG-API-KEY'=> $self->apikey,
1276             };
1277              
1278 0           my $data = {
1279             direction => 'BUY',
1280             #epic=>
1281             #size=>0.1
1282             orderType=>'MARKET',
1283             guaranteedStop=>'false',
1284             forceOpen=>'false',
1285             timeInForce => "EXECUTE_AND_ELIMINATE", # "GOOD_TILL_CANCELLED"
1286             };
1287 0           my $client = REST::Client->new();
1288 0           $client->setHost($self->_url);
1289              
1290 0           my $existing=$self->positions;
1291 0           my %existhash;
1292 0           map { $existhash{$self->fetch($_,'epic')}=$_ } @$existing;
  0            
1293              
1294 0           for my $position (@$positions)
1295             {
1296             # die dump($position);
1297              
1298 0           my $existingsize=0;
1299 0           my $epic=$self->fetch($position,'epic');
1300 0           my $name=$self->fetch($position,'instrumentName');
1301              
1302 0           my $ms=$self->fetch($position,'marketStatus');
1303              
1304              
1305 0 0         if (exists $existhash{$epic})
1306             {
1307 0           my $existingposition=$existhash{$epic};
1308 0           $existingsize=$self->fetch($existingposition,'size');
1309             }
1310              
1311 0           my $demandsize=$self->fetch($position,'size');
1312 0           my $wantedsize=$demandsize-$existingsize;
1313              
1314 0           print "existingsize=$existingsize wantedsize=$wantedsize, demandsize=$demandsize\n";
1315 0 0         if ($wantedsize<=0)
1316             {
1317 0           push(@done,$position);
1318 0           print "$name, not needed\n";
1319 0           next;
1320             }
1321              
1322 0 0 0       if ($ms ne 'TRADEABLE' and !$ignoretradeable)
1323             {
1324 0           push(@notdone,$position);
1325 0           print "$name, market status is $ms\n";
1326 0           next;
1327             }
1328              
1329              
1330             #$data->{epic}=$self->fetch($position,'epic');
1331 0           $data->{epic}=$epic;
1332 0           $data->{size}=$wantedsize;
1333 0           $data->{currencyCode}=$self->fetch($position,'currency');
1334 0           $data->{expiry}='DFB';
1335              
1336             #my $jdata = encode_json($data);
1337 0           my $jdata=JSON->new->canonical->encode($data);
1338             # die $jdata;
1339 0           print "$data->{direction}: $position->{instrumentName} $position->{size}\n";
1340 0           $client->POST (
1341             '/positions/otc',
1342             $jdata,
1343             $headers
1344             );
1345 0           my $code=$client->responseCode();
1346 0 0         if ($code==200)
1347             {
1348 0           print "200: ".$client->responseContent()."\n";
1349 0           my $resp=decode_json($client->responseContent());
1350 0           my $dealReference=$resp->{dealReference};
1351 0           print "$name, dr=$dealReference\n";
1352 0 0 0       if (defined $dealReference and length($dealReference)>5)
1353             {
1354 0           push(@done,$position);
1355 0           next;
1356             }
1357             }
1358 0           print "$name, failed code $code \n";
1359 0           push(@notdone,$position);
1360             }
1361 0 0         @$done=@done if ($done);
1362 0 0         @$notdone=@notdone if ($notdone);
1363 0           printf "done=%d notdone=%d\n",0+@done,0+@notdone;
1364 0           print "notdone:\n";
1365              
1366 0           return;
1367              
1368 0           my $format="%-41sinstrumentName %+4.2fsize %-9.2flevel ".
1369             "%-8.2fbid £%-8.2fprofit %4.1fprofitpc%% £%10.2fatrisk\n";
1370              
1371 0           $self->printpos("stdout" , ['Name','Size','Open','Latest','P/L','P/L%','Value','Stop','Stop'], $format);
1372              
1373 0           map { $self->printpos("stdout" , $_, $format) } @notdone;
  0            
1374              
1375             }
1376              
1377             #####################################################################
1378              
1379             =head2 prices - Obtain historical prices
1380              
1381             Obtain historical price information on an instrument.
1382              
1383             =head3 Parameters
1384              
1385             Unused parameters should be set as undef or ''. (either);
1386              
1387             1 A aubstring to be searched for in the name. Eg "UB.D.FTNT.DAILY.IP"
1388              
1389             2 Resolution. Should be one of the IG defined strings (left) or (in my opinion more memorable) aliases (right)
1390              
1391             DAY 1d
1392             HOUR 1h
1393             HOUR_2 1h
1394             HOUR_3 2h
1395             HOUR_4 3h
1396             MINUTE 1m
1397             MINUTE_2 2m
1398             MINUTE_3 3m
1399             MINUTE_5 5m
1400             MINUTE_10 10m
1401             MINUTE_15 15m
1402             MINUTE_30 30m
1403             SECOND 1s
1404             WEEK 1w
1405             MONTH 1M
1406              
1407             4, 5 pageNumber, pageSize What page to produce, and how many items on it.
1408              
1409             6, 7 from , to (dates) can be a string of the form 2021-01-01T16:15:00 or a Time::Piece
1410              
1411             8 max Limits the number of price points (not applicable if a date range has been specified)
1412              
1413            
1414              
1415              
1416              
1417             =cut
1418              
1419             #####################################################################
1420             # Historical prices
1421             # epic, resolution , pagenum, pagessize, from.to max
1422             #####################################################################
1423             sub prices
1424             {
1425              
1426 0     0 1   my $self=shift;
1427 0           my $epic=shift;
1428 0           my $resolution=shift;
1429 0           my $pagenumber=shift;
1430 0           my $pagesize=shift;
1431              
1432 0           my $from=shift;
1433 0           my $to=shift;
1434 0           my $max=shift;
1435              
1436            
1437 0 0         if (ref($to) eq 'Time::Piece')
1438             {
1439 0           $to=$to->strftime("%Y-%m-%dT%H:%M:%S");
1440             }
1441 0 0         if (ref($from) eq 'Time::Piece')
1442             {
1443 0           $from=$from->strftime("%Y-%m-%dT%H:%M:%S");
1444             }
1445              
1446 0   0       $pagesize//=1; # set a default of 1 item per page
1447             # $pagenumber=1; # set a default of page 1, not needed as already set as defult
1448              
1449 0           my $headers = {
1450             'Content-Type' => 'application/json; charset=UTF-8',
1451             'Accept' => 'application/json; charset=UTF-8',
1452             VERSION => 3,
1453             # 'IG-ACCOUNT-ID'=> $accountid,
1454             CST=>$self->CST,
1455             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
1456             'X-IG-API-KEY'=> $self->apikey,
1457             };
1458              
1459 0           $resolution="MINUTE_10";
1460 0           $resolution="HOUR_4";
1461              
1462             # An alternative and more memorable resolution constants. IG values can also be used.
1463 0 0         $resolution="DAY" if ($resolution eq '1d');
1464 0 0         $resolution="HOUR" if ($resolution eq'1h');
1465 0 0         $resolution="HOUR_2" if ($resolution eq '1h');
1466 0 0         $resolution="HOUR_3" if ($resolution eq '2h');
1467 0 0         $resolution="HOUR_4" if ($resolution eq '3h');
1468 0 0         $resolution="MINUTE" if ($resolution eq '1m');
1469 0 0         $resolution="MINUTE_2" if ($resolution eq '2m');
1470 0 0         $resolution="MINUTE_3" if ($resolution eq '3m');
1471 0 0         $resolution="MINUTE_5" if ($resolution eq '5m');
1472 0 0         $resolution="MINUTE_10" if ($resolution eq '10m');
1473 0 0         $resolution="MINUTE_15" if ($resolution eq '15m');
1474 0 0         $resolution="MINUTE_30" if ($resolution eq '30m');
1475 0 0         $resolution="SECOND" if ($resolution eq '1s');
1476 0 0         $resolution="WEEK" if ($resolution eq '1w');
1477 0 0         $resolution="MONTH" if ($resolution eq '1M');
1478              
1479             defined $resolution and
1480 0 0 0       (0==grep { $resolution eq $_} qw(DAY HOUR HOUR_2 HOUR_3 HOUR_4 MINUTE MINUTE_10 MINUTE_15 MINUTE_2 MINUTE_3 MINUTE_30 MINUTE_5 MONTH SECOND WEEK)) and
  0            
1481             die "Resolution is '$resolution', not recognised";
1482              
1483             #my $jheaders=JSON->new->canonical->encode($headers);
1484              
1485 0           my $client = REST::Client->new();
1486 0           $client->setHost($self->_url);
1487             #my $r;
1488              
1489 0           my $values={
1490             pageNumber=>$pagenumber,
1491             pageSize=>$pagesize,
1492             resolution=>$resolution,
1493             from=>$from,
1494             to=>$to,
1495             max=>$max,
1496             } ;
1497              
1498 0           delete @$values{ grep {!$values->{$_} } keys %$values} ; # delete all empty or undef values
  0            
1499 0           map { $values->{$_}=$_."=".$values->{$_} } keys %$values ;
  0            
1500            
1501 0           my $url;
1502 0           $url=join('&',sort values(%$values));
1503 0 0         $url='?'.$url if ($url);
1504 0           $url="prices/$epic".$url;
1505              
1506              
1507 0           $client->GET ( $url,
1508             $headers
1509             );
1510              
1511            
1512 0           my $resp=decode_json($client->responseContent());
1513              
1514              
1515              
1516 0           $self->flatten_withunder($resp);
1517             # print JSON->new->canonical->pretty->encode($resp); exit;
1518              
1519 0           return $resp;
1520             }
1521             #####################################################################
1522             # flatten_withunder
1523             # flattens a deep hash, 3 levels max, where complex hashes are
1524             # removed and replace with _ joined shallow hash values
1525             # for exapmple:
1526             # {
1527             # "metadata" : {
1528             # "allowance" : {
1529             # "allowanceExpiry" : 530567,
1530             # "remainingAllowance" : 9557,
1531             # "totalAllowance" : 10000
1532             # },
1533             # ...
1534             #
1535             # becomes
1536             # {
1537             # "metadata_allowance_allowanceExpiry" : 530473,
1538             # "metadata_allowance_remainingAllowance" : 9556,
1539             # "metadata_allowance_totalAllowance" : 10000,
1540             # ...
1541             # The advantage of a flattened structure is its easier to print.
1542              
1543             #####################################################################
1544              
1545             =head2 flatten_withunder
1546              
1547             Flatten a deep structure, up to 3 layers deep using underscores to create new keys by concatenating deeper keys.
1548             Deep keys are removed. More than 3 layers can be removed by calling multiply.
1549              
1550             =head3 Parameters
1551            
1552             One or more scalers to opperate on or an array. Each will be flattened
1553             where there are hashes or hashes or hashes of hashes of hashes
1554             to a single depth, with elements joined by underscores
1555              
1556             =head3 Example
1557              
1558             {
1559             "metadata" : {
1560             "allowance" : {
1561             "allowanceExpiry" : 530567,
1562             "remainingAllowance" : 9557,
1563             "totalAllowance" : 10000
1564             },
1565             ...
1566            
1567             becomes
1568             {
1569             "metadata_allowance_allowanceExpiry" : 530473,
1570             "metadata_allowance_remainingAllowance" : 9556,
1571             "metadata_allowance_totalAllowance" : 10000,
1572             ...
1573              
1574             The advantage of a flattened structure is its easier to print with existing fuunctions like printpos
1575              
1576             =cut
1577              
1578             #####################################################################
1579             sub flatten_withunder
1580             {
1581 0     0 1   my ($self)=shift;
1582 0           my (@items)=@_;
1583 0           my $fudebug=0;
1584 0 0         $fudebug and printf "%d items to process\n",0+@items;
1585 0           for my $item (@items)
1586             {
1587 0 0         $fudebug and print "item is a ".ref($item)."\n";
1588 0 0         return if (ref($item)eq '');
1589 0 0         if (ref($item) eq 'HASH')
1590             {
1591 0 0         $fudebug and print "is a hash\n";
1592 0           for my $key (keys %$item)
1593             {
1594 0 0         $fudebug and print "key1 $key\n";
1595 0 0         if (ref($item->{$key}) eq 'HASH')
1596             {
1597 0           for my $key2 (keys %{$item->{$key}})
  0            
1598             {
1599 0 0         $fudebug and print "keyr2 $key2\n";
1600 0           $item->{$key."_".$key2}=$item->{$key}->{$key2};
1601 0 0         $fudebug and printf "creating $key"."_"."$key2 as a %s\n",ref($item->{$key}->{$key2});
1602            
1603             # $self->flatten_withunder($item->{$key}) if (ref($item->{$key}->{$key2}) eq 'HASH');
1604 0 0         if (ref($item->{$key}->{$key2}) eq 'HASH')
1605             {
1606 0           for my $key3 (keys %{$item->{$key}->{$key2}})
  0            
1607             {
1608 0 0         $fudebug and print "key3 $key3\n";
1609 0           $item->{$key."_".$key2."_".$key3}=$item->{$key}->{$key2}->{$key3};
1610 0 0         $fudebug and printf "creating $key"."_$key2"."_$key3 as a %s\n",ref($item->{$key}->{$key2}->{$key3});
1611             }
1612 0 0         $fudebug and print "deleting $key->$key2 and $key _$key2\n";
1613 0           delete $item->{$key}->{$key2};
1614 0           delete $item->{$key."_".$key2};
1615             }
1616             }
1617 0 0         $fudebug and print "deleting: $key\n";
1618 0           delete $item->{$key};
1619             }
1620 0 0         if (ref($item->{$key}) eq 'ARRAY')
1621             {
1622 0 0         $fudebug and print "$key is array ref\n";
1623 0           for (@{$item->{$key}})
  0            
1624             {
1625 0           $self->flatten_withunder($_);
1626             }
1627             }
1628             }
1629             }
1630 0 0         if (ref($item) eq 'ARRAY')
1631             {
1632 0 0         $fudebug and print "is an array\n";
1633 0           for (@$item)
1634             {
1635 0           $self->flatten_withunder($_);
1636             }
1637             }
1638             }
1639 0 0         $fudebug and print "processed\n";
1640             }
1641              
1642              
1643             #####################################################################
1644             # uses known structure of supplied deep hash to search for item
1645             # should probably replace with a more generalised deep fetch function.
1646             #####################################################################
1647              
1648             =head2 fetch
1649              
1650             This function is a way to hide the various structures a position may have
1651              
1652             Obsolete but still used sometimes.
1653              
1654             Parameters
1655              
1656             1 A position hash ref, $h
1657             2 The name of the item to be retrieved.
1658              
1659             Returns undef if not found, or the value of item if it is.
1660              
1661             The function looks first in $h->{item} then
1662             in $h->{position}=>{item} and then in $h->{market}->{item}
1663              
1664             Its only useful with positions, not hashes in general.
1665              
1666             =cut
1667             #####################################################################
1668             sub fetch
1669             {
1670 0     0 1   my ($self,$position,$item)=@_;
1671              
1672             # return "NOT A HASREF $position"if (ref($position) ne 'HASH');
1673 0 0         die "supplied position $position to fetch() is not a HASHREF" if (ref($position) ne 'HASH');
1674 0 0         defined $item or die "fetch, item undefined";
1675 0           my $p=$position->{position};
1676 0           my $m=$position->{market};
1677              
1678 0 0         if (exists $position->{$item}) { return $position->{$item}; }
  0 0          
    0          
1679 0           elsif (exists $p->{$item}) { return $p->{$item}; }
1680 0           elsif (exists $m->{$item}) { return $m->{$item}; }
1681             else {
1682 0           return undef;
1683             }
1684              
1685             }
1686              
1687             #####################################################################
1688             # given an instrument name in search, look for it inside the instrumentName, and return
1689             # the epic. Fail if result is not 1 item.
1690             # used for filling in the epic (a unique identifier) in old data files
1691             # where I forgot to store it.
1692             #####################################################################
1693              
1694             =head2 epicsearch
1695              
1696             Find the epic (unique identifier) for an instrument from the underlying share.
1697              
1698             This function calls IG's search API looking for a match to the name. If found
1699             the value of the epic is returned.
1700              
1701             =head3 Status - very experimental. Seems to work well.
1702              
1703             Contains print and die statements. Useful if you forgot to record the epic.
1704              
1705             =cut
1706              
1707             #####################################################################
1708             sub epicsearch
1709             {
1710 0     0 1   my ($self,$search)=@_;
1711 0           my $headers =
1712             {
1713             'Content-Type' => 'application/json; charset=UTF-8',
1714             'Accept' => 'application/json; charset=UTF-8',
1715             VERSION => 1,
1716             CST=>$self->CST,
1717             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
1718             'X-IG-API-KEY'=> $self->apikey,
1719             };
1720             #my $jheaders = encode_json($headers);
1721 0           my $jheaders=JSON->new->canonical->encode($headers);
1722 0           my $client = REST::Client->new();
1723 0           $client->setHost($self->_url);
1724 0           $search=~s#/#%2F#g;
1725 0           my $url="/markets?searchTerm=$search";
1726 0           $search=~s#%2F#/#g;
1727 0           $url=~s/ /%20/g;
1728 0           my $r=$client->GET ( $url, $headers);
1729              
1730             # my $resp=decode_json($client->responseContent());
1731              
1732              
1733             #print "url=$url\n";
1734 0           my $code;
1735              
1736 0           $code=$client->responseCode();
1737              
1738 0           my $retried=0;
1739 0   0       while ($code==403 and $retried<4)
1740             {
1741 0           sleep 10;
1742 0           $retried++;
1743 0           $r=$client->GET ( $url, $headers);
1744 0           $code=$client->responseCode();
1745             # print "search retried\n";
1746             }
1747              
1748 0 0         die "response code from url='$url' code=$code retried $retried times" if ($code!=200);
1749              
1750 0           my $markets=decode_json($client->responseContent);
1751             # print JSON->new->ascii->pretty->encode($markets)."\n";
1752              
1753 0           my @wantedmarkets=grep { $_->{expiry} eq 'DFB' } @{$markets->{markets}};
  0            
  0            
1754 0           @wantedmarkets=grep { $self->_nothe($self->fetch($_,'instrumentName') , $search) } @wantedmarkets;
  0            
1755              
1756 0           @wantedmarkets=map { $_->{epic} } @wantedmarkets;
  0            
1757 0 0         die "Zero epics found for search $search" if (@wantedmarkets==0);
1758 0 0         die "Multiple epics found @wantedmarkets for search $search" if (@wantedmarkets!=1);
1759              
1760 0           return $wantedmarkets[0];
1761              
1762             }
1763             #####################################################
1764             # remove a trailing 'the'
1765             #####################################################
1766             sub _nothe
1767             {
1768 0     0     my ($self,$x,$y)=@_;
1769              
1770             # print "comparing $x $y \n";
1771 0           $x=~s#/.*$##;
1772 0           $y=~s#/.*$##;
1773              
1774 0           return $x eq $y;
1775             }
1776             # so this is used to read one of my old data files.
1777             ##################################################################################
1778             # Reads am ascii file - older format and returns a list of positions,
1779             # a hashref keyed on epic.
1780             ##################################################################################
1781              
1782             =head2 readfile_oldformat
1783              
1784              
1785             Parameters
1786              
1787             1 Path to a file to read
1788              
1789             A file readable by this function may be generated by using printpos with format as follows:
1790             "%-41sinstrumentName %+6.2fsize %-9.2flevel ".
1791             "%-9.2fbid £%-8.2fprofit %5.1fprofitpc%% £%10.2fatrisk\n",
1792              
1793             This file was originally generated to be human readable so reading by machine is a stretch.
1794              
1795             =head3 Status - downright broken (for you). Sorry!
1796              
1797             May contains print and die statements. Contaions hardcoded paths that will need to be
1798             changed.
1799              
1800             =cut
1801             ##################################################################################
1802             sub readfile_oldformat
1803             {
1804 0     0 1   my ($self, $f,$writenewfile)=@_;
1805 0           my $positions={};
1806 0           my $totalline;
1807 0           $f="/home/mark/igrec/results/$f";
1808 0 0         open(F,$f) or die "cannot open $f";
1809             #Roku Inc +0.38 16501.00 21842.0 £2029.58 32.4% £ 8299.96
1810 0           my @fieldhashnames=qw(epic instrumentName size level bid profit profitpc atrisk);
1811 0           while (<F>)
1812             {
1813 0           my @fields;
1814 0           my @names=@fieldhashnames;
1815 0           my $position={};
1816              
1817 0           chomp;
1818 0 0         if (m/\|/)
    0          
    0          
1819             {
1820 0           die;
1821             }
1822             elsif (m/^Name/)
1823             {
1824 0           s/[£%]//g;
1825 0           @fields=split(/ +/);
1826 0           unshift(@fields,'Epic');
1827             # print "#".join("\|",@fields)."\n";
1828             }
1829             elsif (m/^Total/)
1830             {
1831 0           $totalline=$_;
1832             }
1833             else
1834             {
1835 0           my $name=substr($_,0,42);
1836 0           my $line=substr($_,43);
1837 0           $name=~s/ +$//;
1838 0           $line=~s/[\$£%]//g;
1839 0           @fields=split(/ +/,$line);
1840 0           my $epic=$self->epicsearch($name);
1841 0           unshift(@fields,$epic,$name);
1842             #die "$line\n@fields\n@names";
1843 0           while (@names)
1844             {
1845 0           $position->{shift(@names)}=shift(@fields);
1846             }
1847 0           $positions->{$epic}=$position;
1848             }
1849             }
1850             # close F;
1851 0 0         if ($writenewfile)
1852             {
1853 0           $f=~s/results/r2/;
1854 0 0         if (! -e $f)
1855             {
1856 0 0         open(my $g,">" , $f) or die "Cannot open $f for write";
1857 0           my $format= "%sepic|%sinstrumentName|%0.2fsize|%-0.2flevel|".
1858             "%-0.2fbid|£%-0.2fprofit|%0.1fprofitpc%%|£%0.2fatrisk\n",
1859             print $g "Epic|Instrumentname|Size|Level|Bid|Profit£|Profitpc%|Atrisk£\n";
1860 0           my $a=$self->agg([values %$positions]);
1861 0           for (@$a)
1862             {
1863 0           $self->printpos($g,$_,$format);
1864             }
1865 0           print $g $totalline."\n";
1866             }
1867             }
1868 0           return $positions;
1869             }
1870             ##################################################################################
1871             # Reads am ascii file and returns a list of positions,
1872             # a hashref keyed on epic.
1873             ##################################################################################
1874              
1875             =head2 readfile
1876              
1877              
1878             Parameters
1879              
1880             1 Path to a file to read
1881              
1882             A file readable by this function may be generated by using printpos with format as follows:
1883             "%sepic|%sinstrumentName|%0.2fsize|%-0.2flevel|".
1884             "%-0.2fbid|£%-0.2fprofit|%0.1fprofitpc%%|£%0.2fatrisk|%smarketStatus\n",
1885              
1886             =head3 Status - downright broken (for you). Sorry!
1887              
1888             The function contains a hardcoded path for reading the files. You would need a
1889             crontab entry to generate them.
1890              
1891             May contain print and die statements. Contains hardcoded paths that will need to be
1892             changed.
1893              
1894             =cut
1895             ##################################################################################
1896             sub readfile
1897             {
1898 0     0 1   my ($self,$f)=@_;
1899              
1900 0           my $positions={};
1901 0           $f="/home/mark/igrec/r2/$f";
1902 0 0         open(F,$f) or die "cannot open $f";
1903 0           my @fieldhashnames=qw(epic instrumentName size level bid profit profitpc atrisk tradeable);
1904 0           my $ln=0;
1905 0           while (<F>)
1906             {
1907 0           my @fields;
1908 0           my @names=@fieldhashnames;
1909 0           my $position={};
1910              
1911 0           $ln++;
1912 0           chomp;
1913 0 0         if (m/^Total/)
    0          
    0          
    0          
    0          
    0          
1914             {
1915 0           next;
1916             }
1917             elsif (m/ Positions$/)
1918             {
1919 0           next;
1920             }
1921             elsif (m/^ *$/)
1922             {
1923 0           next;
1924             }
1925             elsif (m/#/)
1926             {
1927 0           next;
1928             }
1929             elsif (!m/\|/)
1930             {
1931 0           die "No | lin line $ln file $f";
1932             }
1933             elsif (m/Epic/)
1934             {
1935 0           next;
1936             }
1937             else
1938             {
1939 0           s/[£&]//g;
1940 0           @fields=split(/\|/);
1941 0           for my $fieldname (@fieldhashnames)
1942             {
1943 0 0         die if (!defined $names[0]);
1944             #print "names[0]=$names[0]\n";
1945 0           $position->{$fieldname}=shift(@fields);
1946             }
1947 0           $positions->{$position->{epic}}=$position;
1948 0   0       $position->{marketStatus}//=''; # older files do not record this.
1949             }
1950             }
1951 0           return $positions;
1952             }
1953             #####################################################################
1954             # format strings contained embedded printf specifiers followed by
1955             # a hash element name .
1956             #
1957             # eg "%sdate %sdescription %sepic %sstatus\n";
1958             # eg "%-20sdate %-30sdescription %-20sepic %-15sstatus\n";
1959             # eg
1960             # "%sepic|%sinstrumentName|%6.2fsize|%-9.2flevel|".
1961             # "%-9.2fbid|£%-8.2fprofit|%5.1fprofitpc%%|£%10.2fatrisk\n",
1962             #eg
1963             # "%-41sinstrumentName %+6.2fsize %-9.2flevel ".
1964             # "%-9.2fbid £%-8.2fprofit %5.1fprofitpc%% £%10.2fatrisk\n",
1965             # Arguments:
1966             # 1) An IG object ref. (self) Is not really used.
1967             # 2) Either "stdout" or an open writable file handle.
1968             # 3) A hash possibly deep, with items. Ig the item is not found directly in the hash,
1969             # the $self->fetch function is used for access. If still not found
1970             # then "UNDEF" is printed.
1971             # CHANGED to $self->uds
1972             # OR: If this is an array ref, then a title line is ptinted using the format string
1973             # and the referenced array of titles
1974             # OR: If empty dtring ort undef, derive titles from the format
1975             # string and print a title line.
1976             # 4) A formatting string. Can contain text, containing embedded
1977             # format instructions like %6.2fsize here %6.2f is a print f
1978             # specifier and size is the name of the item to retrieve from the hash.
1979             # 5,6) up /down can be percent gives green if > up, bold green if > 5*up.
1980             # can be a coloration function of position.
1981             # just one function, so no down ever.
1982             # function takes argument position, and returns optional colors
1983             #####################################################################
1984              
1985             =head2 printpos
1986              
1987             =head3 Parmeters
1988              
1989             A file handle or the word stdout, all output sent here.
1990              
1991             A hashref of items to print
1992             OR: If this is an array ref, then a title line is ptinted using the format string
1993             and the referenced array of titles
1994             OR: If empty string or undef, derive titles from the format
1995             string and print a title line.
1996              
1997             A formatting string. Can contain text, containing embedded
1998             format instructions like %6.2fsize here %6.2f is a print f
1999             specifier and size is the name of the item to retrieve from the hash.
2000              
2001             OPTIONAL up can be percent gives green if > up, bold green if > 5*up.
2002             can be a coloration function of position. Just one function, so no down ever if a function is given
2003             function takes argument position, and returns optional colors
2004              
2005             OPTIONAL down can be percent gives red if <down , bold red if < 5*down.
2006              
2007             =head3 Description
2008              
2009             This is a very general function will work with any hash.
2010              
2011             =cut
2012             #####################################################################
2013             sub printpos
2014             {
2015              
2016 0     0 1   my ($self,$out,$position,$format,$up,$down)=@_;
2017              
2018 0           my $colsub;
2019              
2020 0 0         $out=*STDOUT if ($out eq "stdout");
2021              
2022 0 0 0       $down=-$up if (defined $up and ref($up) eq '' and !defined $down) ;
      0        
2023              
2024 0 0 0       if (defined $up and ref($up) ne 'CODE')
2025             {
2026             $colsub=sub
2027             {
2028 0     0     my ($position)=shift;
2029 0           my $v1=$position->{dbid};
2030 0           my $col='';
2031 0           $v1=~s/%//;
2032 0 0 0       $col=Green if (defined $up and $v1>$up);
2033 0 0 0       $col=Red if (defined $down and $v1<$down);
2034 0 0 0       $col=Green+Bold if (defined $up and $v1>$up*5);
2035 0 0 0       $col=Red+Bold if (defined $down and $v1<5*$down);
2036 0           return $col;
2037 0           };
2038             }
2039 0 0 0       $colsub=$up if (defined $up and ref($up) eq 'CODE');
2040 0 0   0     $colsub=sub {''} if (!defined $up);
  0            
2041              
2042              
2043 0           my $titles=$format;
2044 0 0         if (ref($position) eq 'ARRAY') # its titles to print!
2045             {
2046             #$format=~s/%[-+]/%/g;
2047             #print "$format\n";
2048 0           while ($format=~m/[-+]?([0-9]+)\.([0-9]+)/)
2049             {
2050 0           my $x;
2051 0           $x=$1;
2052 0 0         abs($2)>abs($x) and $x=$2;
2053 0           $format=~s/%([-+]?)([0-9]+)\.([0-9]+)/%$1$x/;
2054             }
2055             #print "#1 $format\n";
2056 0           $format=~s/%\+\+/%+/g;
2057             #print "#2 $format\n";
2058 0           $format=~s/%([-\+]?[0-9]+)\.[0-9]+/%$1/g;
2059             #print "#3 $format\n";
2060 0           $format=~s/%([-\+]?[0-9]+)[fd]/%$1s/g;
2061             #print "#4 $format\n";
2062 0           $format=~s/%([-\+]?[0-9]*)([a-zA-Z_][a-zA-Z0-9_]*)/%$1s/g;
2063             #die $format;
2064             # print "$format\n"; exit;
2065             #$"=":"; print "@$position\n";
2066            
2067              
2068 0           $format=~s/[\x82\x83\xc3]+//g; # so we get some strange characters like ÃÂ occuring in pairs. Not sure why. This removes them.
2069             #$format="%-41s %+7s %11s %-10s £%-10s %5s%% £%12s %-9s %-4s";
2070             #print "$format\n"; #exit;
2071 0 0 0       print $out Bold if ($self->col and defined $INC{'Term/Chrome.pm'});
2072             # print "format='$format' @$position\n";
2073 0           printf $out $format,@$position;
2074 0 0 0       print $out Reset if ($self->col and defined $INC{'Term/Chrome.pm'});
2075 0           return;
2076             }
2077              
2078             # auto generated title list from the names
2079 0 0 0       if (!defined $position or $position eq '')
2080             {
2081 0           $titles=~s/\n//g;
2082 0           $titles=~s/%([-+0-9.]*)([sfd])/%/g;
2083 0           $titles=~s/%%/__PC__/g;
2084 0           $titles=~s/%//; # just one
2085 0           $titles=~s/£%([a-zA-Z]+)/%$1£/g;
2086 0           my @titles=split(/%/,$titles);
2087 0           map {s/[|,]//g } @titles;
  0            
2088 0           map {s/ +//g } @titles;
  0            
2089 0           map { s/__PC__//g; } @titles;
  0            
2090 0           map { s/([\w']+)/\u\L$1/g; } @titles;
  0            
2091 0           while ($format=~m/%[-+]?([0-9]+)\.([0-9]+)/)
2092             {
2093 0           my $x;
2094             #my $x=$1+$2;
2095 0           $x=$1;
2096 0 0         $2>$x and $x=$2;
2097 0           $format=~s/%([-+]?)([0-9]+)\.([0-9]+)/%$1$x/;
2098             }
2099 0           $format=~s/(%[-+0-9.]*)[a-zA-Z]+/$1s/g;
2100             #$format=~s/(%[-+0-9]+)\.[0-9]+/$1/g;
2101 0           $format=~s/£//g;
2102             #die "format=$format titles=@titles";
2103 0           $format=~s/[\x82\x83\xc3]+//g; # so we get some strange characters like ÃÂ occuring in pairs. Not sure why. This removes them.
2104 0 0 0       print $out Bold if ($self->col and defined $INC{'Term/Chrome.pm'});
2105 0           printf $out $format, @titles;
2106 0 0 0       print $out Reset if ($self->col and defined $INC{'Term/Chrome.pm'});
2107 0           return;
2108             }
2109              
2110              
2111              
2112             # $p=$position->{position};
2113             # $m=$position->{market};
2114              
2115 0           $format=~s/%%/##/g;
2116              
2117              
2118             # while (($format=~s/%([-+0-9]+\.[0-9]+)([a-z][a-zA-Z0-9]*)/%$1__S__/) || ($format=~s/%([-+0-9]*)([a-z][a-zA-Z0-9]*)/%$1__F__/))
2119             # {
2120             # my $s;
2121             # $s=$activity->{$2};
2122             # my $pos=$1;
2123             # $pos=~s/-//;
2124             # $s=substr($s,0,$pos) if (defined(pos) and $pos ne '' and $pos<length($s));
2125             # push(@args,$s);
2126             # }
2127              
2128 0           my $col='';
2129 0           while ($format=~s/%([-+0-9.]*[dsf])([a-zA-Z_][a-zA-Z0-9_]*)/%s/)
2130             {
2131 0           my $s;
2132              
2133 0           my $item=$2;
2134 0   0       my $len=$1//"";
2135             # die "item is UNDEF" if ($item eq 'UNDEF');
2136             # die "len is UNDEF" if ($len eq 'UNDEF');
2137             # $len='' if ($len eq 'UNDEF');
2138 0 0         $len="%".$len if ($len);
2139 0 0 0       if (defined $item and $item ne '' and exists $position->{$item} and defined $position->{$item})
    0 0        
      0        
2140             {
2141 0           $position->{$item}=~s/%//g;
2142             #$position->{$item}='0' if ($position->{$item} eq 'UNDEF');
2143 0           $s=sprintf($len,$position->{$item});
2144 0 0 0       if ($item eq 'dbid' and exists $INC{'Term/Chrome.pm'} and $self->col)
      0        
2145             {
2146             ##my $v1=$position->{dbid};
2147             ##$v1=~s/%//;
2148             ##$col=Green if (defined $up and $v1>$up);
2149             ##$col=Red if (defined $down and $v1<$down);
2150             ##$col=Green+Bold if (defined $up and $v1>$up*5);
2151             ##$col=Red+Bold if (defined $down and $v1<5*$down);
2152              
2153             # $col=&$colsub($position);
2154             }
2155             # $col=Yellow if (defined $up);
2156             # $col=&$colsub($position);
2157             }
2158             elsif (defined $self->fetch($position,$item))
2159             {
2160             #$s=sprintf($len,$self->fetch($position,$2)//"UNDEF");
2161 0   0       $s=sprintf($len,$self->fetch($position,$item)//$self->uds);
2162 0 0 0       if ($item eq 'dbid' and defined $INC{'Term/Chrome.pm'} and $self->col)
      0        
2163             {
2164             #my $v1;
2165              
2166             #$v1=$self->fetch($position,'dbid');
2167             #$v1=~s/%//;
2168             #$v1=100*$v1/$self->fetch($position,'bid');
2169             ###$col=Green if (defined $up and $self->col and $self->fetch($position,'dbid')/$self->fetch($position,'bid')>$up/100);
2170             ###$col=Red if (defined $down and $self->col and $self->fetch($position,'dbid')/$self->fetch($position,'bid')<$down/100);
2171             #$col=Green if (defined $up and $self->col and $v1>$up);
2172             #$col=Red if (defined $down and $self->col and $v1<$down);
2173             #$col=Green+Bold if (defined $up and $self->col and $v1>$up*5);
2174             #$col=Red+Bold if (defined $down and $self->col and $v1<5*$down);
2175             #$col=&$colsub($position);
2176             }
2177             #$col=&$colsub($position);
2178              
2179             }
2180             else
2181             {
2182 0           $len=~s/[df]/s/;
2183 0           $len=~s/\.[0-9]+//;
2184             #$s=sprintf($len,"UNDEF");
2185 0           $s=sprintf($len,$self->uds);
2186             }
2187              
2188 0           $col=&$colsub($position);
2189 0           $len=~s/[dsf]$//;
2190 0 0         if ($len ne '') # len can be something like 0.2
2191             {
2192 0           $len=~s/%//;
2193 0 0         $len=abs($len) if ($len ne '');
2194 0 0 0       $s=substr($s,0,$len) if ($len and $len<length($s) and $len>=1);
      0        
2195             }
2196              
2197 0           $format=~s/%s/$s/;
2198             }
2199              
2200 0 0 0       $col=&$colsub($position)//'' if ($self->col and defined $INC{'Term/Chrome.pm'});
      0        
2201 0           $format=~s/##/%/g;
2202 0           $format=~s/£-/-£/g;
2203 0           $format=~s/[\x82\x83\xc3]+//g; # so we get some strange characters like ÃÂ occuring in pairs. Not sure why. This removes them.
2204 0           print $out $col, $format;
2205 0 0         if (ref($col) ne '')
2206 0           { print $out Reset;
2207             }
2208              
2209             }
2210              
2211              
2212              
2213             =head2 sortrange
2214              
2215             =head3 Parameters
2216            
2217             Ref to an array containing dates in printed ascii format.
2218              
2219             If there are no dates or an empty array, an empty string is returned.
2220              
2221             If there is one date, then that date is returned
2222              
2223             If there is more than one then the first and last after sorting is returned, with a dash between them.
2224              
2225             This is used in aggregation of positions and relates to creation dates with multiple positions
2226             in the same security purchased at different times.
2227              
2228             =cut
2229              
2230             sub sortrange
2231             {
2232 0     0 1   my ($self,$ar)=@_;
2233              
2234 0           my @dates=sort @$ar;
2235            
2236 0 0         return '' if (@dates==0);
2237 0 0         return $dates[0] if (@dates==1);
2238 0           return $dates[0] . "-".$dates[-1];
2239             }
2240              
2241              
2242              
2243             =head2 Red Blue Bold Reset Underline Green color
2244              
2245             =head3 Description
2246              
2247             The above parameterless functions are provided if Term::Chrome is not available. They are "do nothing" subs provided to satisfy references only.
2248              
2249             =head1 DEPENDENCIES
2250              
2251             Moose
2252             Term::Chrom if available.
2253              
2254             =head1 UTILITIES
2255              
2256             A more complete position lister is given as igdisp.pl
2257              
2258             =head1 AUTHOR
2259              
2260             Mark Winder, C<< <markwin at cpan.org> >>
2261              
2262             =head1 BUGS
2263              
2264             Please report any bugs or feature requests to C<bug-finance-ig at rt.cpan.org>, or through
2265             the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Finance-IG>. I will be notified, and then you'll
2266             automatically be notified of progress on your bug as I make changes.
2267              
2268              
2269              
2270              
2271             =head1 SUPPORT
2272              
2273             You can find documentation for this module with the perldoc command.
2274              
2275             perldoc Finance::IG
2276              
2277              
2278             You can also look for information at:
2279              
2280             =over 4
2281              
2282             =item * RT: CPAN's request tracker (report bugs here)
2283              
2284             L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Finance-IG>
2285              
2286             =item * CPAN Ratings
2287              
2288             L<https://cpanratings.perl.org/d/Finance-IG>
2289              
2290             =item * Search CPAN
2291              
2292             L<https://metacpan.org/release/Finance-IG>
2293              
2294             =back
2295              
2296              
2297             =head1 ACKNOWLEDGEMENTS
2298              
2299             =head1 FURTHER READING
2300              
2301             IG REST API Reference https://labs.ig.com/rest-trading-api-reference
2302              
2303             =head1 LICENSE AND COPYRIGHT
2304              
2305             This software is Copyright (c) 2020 by Mark Winder.
2306              
2307             This is free software, licensed under:
2308              
2309             The Artistic License 2.0 (GPL Compatible)
2310              
2311              
2312             =cut
2313              
2314             1; # End of Finance::IG