File Coverage

lib/Finance/IG.pm
Criterion Covered Total %
statement 27 654 4.1
branch 1 298 0.3
condition 0 138 0.0
subroutine 9 39 23.0
pod 19 26 73.0
total 56 1155 4.8


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   60419 use strict;
  1         2  
  1         27  
6 1     1   4 no strict 'refs';
  1         2  
  1         18  
7 1     1   5 use warnings;
  1         9  
  1         47  
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.101
28              
29             =cut
30              
31             our $VERSION = '0.101';
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   540 use Moose;
  1         392348  
  1         6  
121 1     1   6805 use JSON;
  1         9179  
  1         5  
122 1     1   547 use REST::Client;
  1         36361  
  1         31  
123             #use Data::Dump qw(dump); # used in some commented out debug statements
124             #use Scalar::Util;
125 1     1   504 use Time::Piece;
  1         6411  
  1         4  
126              
127             BEGIN {
128 1 50   1   174 if (eval("require Term::Chrome"))
129             {
130 0         0 Term::Chrome->import();
131             }
132             else
133             {
134 1     0 0 3 map { eval ("sub $_ {}") } qw(Red Blue Bold Reset Underline Green color); # need these to avoid compile time errors.
  7     0 0 2827  
        0 0    
        0 0    
        0 0    
        0 0    
        0 0    
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             # $position->{size}= -abs($position->{size}) if ($position->{direction}//'' ne 'BUY');
863 0           $position->{profit}=($self->fetch($position,'bid')-$self->fetch($position,'level'))*$self->fetch($position,'size');
864              
865 0 0         $position->{held}=Time::Piece->strptime($position->{createdDateUTC},"%Y-%m-%dT%H:%M:%S") or die "strptime failed for ".$position->{createdDateOnly};
866 0           $position->{held}=(gmtime()-$position->{held})/(24*3600);
867              
868 0   0       my $ra=($totals{$position->{instrumentName}}||=[]);
869 0           push(@$ra,$position);
870              
871             }
872              
873             # totals is a hash on instrument name each element is a pointer to an array of positions for the same instrument.
874              
875 0           my $aggregated=[];
876 0           for my $total (values %totals)
877             { # for one particular name
878 0           my $position={}; # initialise the new aggregate position
879              
880 0           $position->{profit}=0;
881 0           $position->{size}=0;
882 0           $position->{held}=0;
883 0           $position->{stopLevel}=[];
884 0           $position->{createdDate}=[];
885 0           $position->{createdDateUTC}=[];
886              
887 0           for my $subtotal ( @$total) # go through all the positions for that one name
888             {
889 0   0       $position->{instrumentName}//=$subtotal->{instrumentName};
890 0           $position->{size}+=$subtotal->{size};
891 0           my $h;
892 0 0         $h=Time::Piece->strptime($subtotal->{createdDateUTC},"%Y-%m-%dT%H:%M:%S") or die "strptime failed for ".$subtotal->{createdDateOnly};
893 0           $h=(gmtime()-$h)/(24*3600);
894 0           $h=int($h*10)/10;
895 0           $subtotal->{held}=$h;
896 0           $position->{held}+=$subtotal->{held}*$subtotal->{size}; # this is a size-weighted average. Needs division by total size.
897 0   0       $position->{bid}//=$subtotal->{bid};
898 0           $position->{profit}+=$subtotal->{profit} ;
899 0   0       $position->{epic}//=$subtotal->{epic};
900              
901 0   0       $position->{currency}//=$subtotal->{currency};
902 0   0       $position->{marketStatus}//=$subtotal->{marketStatus};
903              
904 0 0         push(@{$position->{stopLevel}},$subtotal->{stopLevel}) if $subtotal->{stopLevel};
  0            
905 0           push(@{$position->{createdDate}},$subtotal->{createdDate});
  0            
906 0           push(@{$position->{createdDateUTC}},$subtotal->{createdDateUTC});
  0            
907             }
908              
909             # 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.
910             ###########
911              
912 0 0         $position->{held}=sprintf("%0.1f",$position->{held}/$position->{size}); $position->{held}.=" av" if (@$total>1);
  0            
913              
914              
915 0           $position->{level}=$position->{bid}-$position->{profit}/$position->{size}; # open level for multiple positions
916              
917 0 0         $position->{profitpc}=int(0.5+1000*$position->{profit}/($position->{level}*abs($position->{size})))/10 if ($position->{level}>0);
918              
919 0           $position->{atrisk}=$position->{bid}*$position->{size};
920              
921 0           $position->{createdDate}=$self->sortrange($position->{createdDate});
922 0           $position->{createdDateUTC}=$self->sortrange($position->{createdDateUTC});
923 0           $position->{createdDateOnly}=$position->{createdDate};
924 0           $position->{createdDateOnly}=~s/T[^-]+//g;
925              
926 0 0         $position->{slpc}=join(',',map { $_?(int(1000.0*$_/$position->{bid})/10):''} @{$position->{stopLevel}});
  0            
  0            
927 0           $position->{stopLevel}=join(',',@{$position->{stopLevel}});
  0            
928            
929             ###########
930             # end of aggregated operations
931              
932              
933 0           push(@$aggregated,$position);
934             }
935              
936             # @$aggregated=sort { $b->{profitpc}<=>$a->{profitpc} } @$aggregated;
937 0   0       $sortlist//=[qw(-profitpc instrumentName)]; # default sort
938 0           $self->sorter($sortlist,$aggregated);
939 0           return $aggregated;
940              
941             }
942             # like agg, but do not do actual aggregation.
943             # so we sort, add certain extra characteristics but thats all.
944             ##########################################################################
945             #
946              
947             =head2 nonagg - like agg but do not do actual aggregation
948              
949             Parameters
950              
951             1 Reference to an array of positions
952             2 (Optional) Ref to an array of keys to sort on
953              
954             Return value - Array ref containing hashes of accounts. Should be the same size as the original.
955              
956             =cut
957             ##########################################################################
958             #sub nonagg
959             #{
960             # my ($self,$positions,$sortlist)=@_;
961             # my %totals; # aggregated totals as arrays of individuals.
962             #
963             # $self->flatten($positions, [qw/market position/]);
964             # for my $position (@$positions)
965             # {
966             #
967             # my $json = JSON->new;
968             #
969             # $position->{profit}=($self->fetch($position,'bid')-$self->fetch($position,'level'))*$self->fetch($position,'size');
970             # # create new profits element
971             #
972             # my $open=$position->{bid}-$position->{profit}/$position->{size};
973             # $position->{level}=$open;
974             # $position->{profitpc}=int(0.5+1000*$position->{profit}/($position->{level}*$position->{size}))/10;
975             # $position->{atrisk}=$position->{bid}*$position->{size};
976             # $position->{createdDateOnly}=$position->{createdDate};
977             # $position->{createdDateOnly}=~s/ .*$//;
978             # }
979             #
980             # $sortlist//=[qw(-profitpc instrumentName)]; # default sort
981             # $self->sorter($sortlist,$positions);
982             # return $positions;
983             #}
984             sub nonagg
985             {
986 0     0 1   my ($self,$positions,$sortlist)=@_;
987 0           my %totals; # aggregated totals as arrays of individuals.
988              
989 0           $self->flatten($positions, [qw/market position/]);
990 0           for my $position (@$positions)
991             {
992              
993 0           my $json = JSON->new;
994              
995 0 0         $position->{size}=-abs($position->{size}) if ($position->{direction} eq 'SELL');
996 0           $position->{profit}=($position->{bid}-$position->{level})*$position->{size};
997             # create new profits element
998              
999             # my $open=$position->{bid}-$position->{profit}/$position->{size};
1000             # $position->{level}=$open;
1001 0           $position->{profitpc}=int(0.5+1000*$position->{profit}/($position->{level}*abs($position->{size})))/10;
1002 0           $position->{atrisk}=$position->{bid}*$position->{size};
1003 0           $position->{createdDateOnly}=$position->{createdDate};
1004 0           $position->{createdDateOnly}=~s/ .*$//;
1005 0 0         $position->{held}=Time::Piece->strptime($position->{createdDateUTC},"%Y-%m-%dT%H:%M:%S") or die "strptime failed for ".$position->{createdDateOnly};
1006 0           $position->{held}=(gmtime()-$position->{held})/(24*3600);
1007 0           $position->{held}=int($position->{held}*10+0.5)/10;
1008 0           $position->{dailyp}='';
1009 0 0         $position->{dailyp}=((1+$position->{profitpc}/100.0)**(1/$position->{held})-1)*100 if ($position->{held}>0);
1010            
1011             }
1012              
1013 0   0       $sortlist//=[qw(-profitpc instrumentName)]; # default sort
1014 0           $self->sorter($sortlist,$positions);
1015 0           return $positions;
1016             }
1017             ####################################################################
1018             # General array sort function.
1019             # Given an array of hash refs, and a sort key
1020             # considtying of an array of an array of keys to the hashes
1021             # sort in place the array.
1022             #
1023             # sortkey, arrayref of keys. Sort order direction reversed
1024             # if key has - appended to start, eg -profitpc gives largest first
1025             # pos array eo be sorted, its an inplace sort.
1026             # uses the determinant $x eq $x+0 to determine if numeric or not.
1027             # improvements: may need to use a deep fetch to locate the items
1028             ####################################################################
1029              
1030             =head2 sorter - general array sort function for an array of hashes
1031              
1032             Parameters
1033              
1034             1 Ref to array of keys to sort. Each my be prefixed with a - to
1035             reverse the order on that key. If keys compare equal the next key is used.
1036             2 Ref to an array of positions to sort.
1037              
1038             The array is sorted in-place. A numeric comparison is done if for
1039             both items $x == $x+0
1040              
1041             Formatted datetimes are correctly sorted.
1042              
1043             =cut
1044             ####################################################################
1045             sub sorter
1046             {
1047 0     0 1   my ($self,$sortkey,$pos)=@_;
1048              
1049             @$pos= sort {
1050 0           my ($result)=0;
  0            
1051 0           for my $fkey (@$sortkey)
1052             {
1053 0           my $key=$fkey;
1054 0           my $dir=1;
1055 0 0         $dir=-1 if ($key=~s/^-//);
1056             # die "key=$key value=$b->{createdDateUTC} keys are ".join(', ',keys %$a); ;
1057 0 0 0       next if (!exists($a->{$key}) or !exists($b->{$key}));
1058 0           my ($x1,$x2)=($a->{$key},$b->{$key});
1059 0           map { s/[£%]//g } ($x1,$x2);
  0            
1060              
1061 1     1   8 { no warnings qw(numeric);
  1         1  
  1         4509  
  0            
1062 0           my $warning;
1063              
1064 0 0 0       if ($x1 eq $x1+0 and $x2 eq $x2+0)
1065             {
1066 0           $result=$x1<=>$x2;
1067             }
1068             else
1069             { # note that this correctly handles a formatted date
1070 0           $result=$x1 cmp $x2;
1071             }
1072             }
1073 0 0         return $result*$dir if ($result);
1074             }
1075 0           return 0;
1076             }
1077             @$pos;
1078              
1079             }
1080             ####################################################################
1081             # The idea is this will close all the supplied positions, optionally returning a reference to
1082             # either/both an array of closed/non closed positions;
1083             # This is not quite working yet, needs more work,
1084             ####################################################################
1085              
1086             =head2 close - close the supplied positions.
1087              
1088              
1089              
1090             Parameters
1091              
1092             1 Ref to array of positions to close.
1093             reverse the order on that key.
1094             2/3 ref to done / notdone arrays to sort succesful / failed
1095             closes in to.
1096              
1097             The idea is this will close all the supplied positions, optionally returning a reference to
1098              
1099              
1100             =head3 Status - very experimental.
1101              
1102             Contains die / print statements that you may wish to remove
1103              
1104             =cut
1105             ####################################################################
1106             sub close
1107             {
1108 0     0 1   my $self=shift;
1109 0           my $positions=shift; # to close
1110 0           my $done=shift;
1111 0           my $notdone=shift;
1112              
1113 0           my $verbose=0;
1114              
1115 0           my @done;
1116             my @notdone;
1117              
1118 0           my $headers = {
1119             'Content-Type' => 'application/json; charset=UTF-8',
1120             'Accept' => 'application/json; charset=UTF-8',
1121             VERSION => 1,
1122             # 'IG-ACCOUNT-ID'=> $accountid,
1123             CST=>$self->CST,
1124             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
1125             'X-IG-API-KEY'=> $self->apikey,
1126             '_method'=>'DELETE',
1127             };
1128              
1129 0           my $data = {
1130             #encryptedPassword => "false",
1131             #identifier => $self->username,
1132             #password => $self->password
1133             #direction => 'BUY',
1134             # epic=>
1135             # expiry=>
1136             orderType=>'MARKET',
1137             #size=>0.1
1138             ##guaranteedStop=>'false',
1139             forceOpen=>'true',
1140             #timeInForce => "EXECUTE_AND_ELIMINATE", # "GOOD_TILL_CANCELLED"
1141             timeInForce => "", # "GOOD_TILL_CANCELLED"
1142             };
1143 0           my $client = REST::Client->new();
1144              
1145 0           $client->setHost($self->_url);
1146              
1147              
1148 0           my %existhash;
1149 0           map { $existhash{$self->fetch($_,'epic')}=$_ } @$positions; # creat a hash on epic
  0            
1150              
1151 0           for my $position (@$positions)
1152             {
1153             # die dump($position);
1154              
1155 0           my $existingsize=0;
1156 0           my $epic=$self->fetch($position,'epic');
1157 0           my $name=$self->fetch($position,'instrumentName');
1158              
1159 0           my $ms=$self->fetch($position,'marketStatus');
1160              
1161 0 0         if ($ms ne 'TRADEABLE')
1162             {
1163 0           push(@notdone,$position);
1164 0           print "$name, market status is $ms\n";
1165 0           next;
1166             }
1167              
1168              
1169             #$data->{epic}=$self->fetch($position,'epic');
1170 0           $data->{epic}=$epic;
1171 0           $data->{size}=$self->fetch($position,'size');
1172             # $data->{currencyCode}=$self->fetch($position,'currency');
1173 0           $data->{expiry}='DFB';
1174             # $data->{expiry}='-';
1175 0           $data->{direction}='SELL';
1176              
1177             #my $jdata = encode_json($data);
1178 0           my $jdata=JSON->new->canonical->encode($data);
1179 0           $client->PUT (
1180             '/positions/otc',
1181             $jdata,
1182             $headers
1183             );
1184 0           my $code=$client->responseCode();
1185 0 0         if ($code==200)
1186             {
1187 0           my $resp=decode_json($client->responseContent());
1188 0           my $dealReference=$resp->{dealReference};
1189 0           print "$name, dr=$dealReference\n";
1190 0 0 0       if (defined $dealReference and length($dealReference)>5)
1191             {
1192 0           push(@done,$position);
1193 0           die;
1194 0           next;
1195 0           next;
1196             }
1197             }
1198             else
1199             {
1200 0           print "$name failed $code: ".$client->responseContent()."\n";
1201 0           push(@notdone,$position);
1202             }
1203             }
1204 0 0         @$done=@done if ($done);
1205 0 0         @$notdone=@notdone if ($notdone);
1206              
1207 0           printf "done=%d notdone=%d\n",0+@done,0+@notdone;
1208 0           print "notdone:\n";
1209              
1210 0           my $cpc='%%';
1211 0           my $format="%-41sinstrumentName %+4.2fsize %-9.2flevel ".
1212             "%-8.2fbid £%-8.2fprofit %4.1fprofitpc%% £%10.2fatrisk %-9sstopLevel %-4sslpc$cpc\n";
1213              
1214 0           $self->printpos("stdout" , ['Name','Size','Open','Latest','P/L','P/L%','Value','Stop','Stop'], $format);
1215              
1216 0           map { $self->printpos("stdout" , $_, $format) } @notdone;
  0            
1217              
1218             }
1219             #####################################################################
1220             # given a ref to an array of positions, attempt to buy the same
1221             # position in this object.
1222             # if the position already exists or is succesfully brought, count as success.
1223             # If the buy fails, include it in the returned list.
1224             # If all buys succesful then return an empty list.
1225             # done and notdone references may be supplied and if they are these should point to arrays
1226             # of the succesful and unsuccesful positions.
1227             # return value is NOT now used.
1228             # ignortradeable ... use this if the positionis an old one, so that tradeable status could
1229             # be out of date.
1230             #####################################################################
1231              
1232             =head2 buy - attempt to buy a number of instruments.
1233              
1234             Parameters
1235              
1236             1 Reference to an array of positions
1237             2 Optional ref to an array done, to be filled with succesful buys
1238             3 Optional ref to an array notdone, to be filled with the failed
1239             4 ignore tradeable, one of the fields in a position relates to the market
1240             being open or closed (TRADEABLE) If this field is current, its a
1241             good indication to skip this one (place it in the notdone array.
1242             But if its out of date then setting this flag 1 attempts the trade
1243             anyway.
1244              
1245             Attempt to buy positions. I have used this to move positions
1246             between a demo account and real account or vice-versa.
1247              
1248             =head3 Status - very experimental.
1249              
1250             Contains print statements that should
1251             probably be removed.
1252              
1253             =cut
1254             #####################################################################
1255             sub buy
1256             {
1257 0     0 1   my $self=shift;
1258 0           my $positions=shift; # to buy
1259 0           my $done=shift;
1260 0           my $notdone=shift;
1261 0           my $ignoretradeable=shift;
1262              
1263 0           my $verbose=0;
1264              
1265 0           my @done;
1266             my @notdone;
1267 0           my $headers = {
1268             'Content-Type' => 'application/json; charset=UTF-8',
1269             'Accept' => 'application/json; charset=UTF-8',
1270             VERSION => 2,
1271             # 'IG-ACCOUNT-ID'=> $accountid,
1272             CST=>$self->CST,
1273             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
1274             'X-IG-API-KEY'=> $self->apikey,
1275             };
1276              
1277 0           my $data = {
1278             direction => 'BUY',
1279             #epic=>
1280             #size=>0.1
1281             orderType=>'MARKET',
1282             guaranteedStop=>'false',
1283             forceOpen=>'false',
1284             timeInForce => "EXECUTE_AND_ELIMINATE", # "GOOD_TILL_CANCELLED"
1285             };
1286 0           my $client = REST::Client->new();
1287 0           $client->setHost($self->_url);
1288              
1289 0           my $existing=$self->positions;
1290 0           my %existhash;
1291 0           map { $existhash{$self->fetch($_,'epic')}=$_ } @$existing;
  0            
1292              
1293 0           for my $position (@$positions)
1294             {
1295             # die dump($position);
1296              
1297 0           my $existingsize=0;
1298 0           my $epic=$self->fetch($position,'epic');
1299 0           my $name=$self->fetch($position,'instrumentName');
1300              
1301 0           my $ms=$self->fetch($position,'marketStatus');
1302              
1303              
1304 0 0         if (exists $existhash{$epic})
1305             {
1306 0           my $existingposition=$existhash{$epic};
1307 0           $existingsize=$self->fetch($existingposition,'size');
1308             }
1309              
1310 0           my $demandsize=$self->fetch($position,'size');
1311 0           my $wantedsize=$demandsize-$existingsize;
1312              
1313 0           print "existingsize=$existingsize wantedsize=$wantedsize, demandsize=$demandsize\n";
1314 0 0         if ($wantedsize<=0)
1315             {
1316 0           push(@done,$position);
1317 0           print "$name, not needed\n";
1318 0           next;
1319             }
1320              
1321 0 0 0       if ($ms ne 'TRADEABLE' and !$ignoretradeable)
1322             {
1323 0           push(@notdone,$position);
1324 0           print "$name, market status is $ms\n";
1325 0           next;
1326             }
1327              
1328              
1329             #$data->{epic}=$self->fetch($position,'epic');
1330 0           $data->{epic}=$epic;
1331 0           $data->{size}=$wantedsize;
1332 0           $data->{currencyCode}=$self->fetch($position,'currency');
1333 0           $data->{expiry}='DFB';
1334              
1335             #my $jdata = encode_json($data);
1336 0           my $jdata=JSON->new->canonical->encode($data);
1337             # die $jdata;
1338 0           print "$data->{direction}: $position->{instrumentName} $position->{size}\n";
1339 0           $client->POST (
1340             '/positions/otc',
1341             $jdata,
1342             $headers
1343             );
1344 0           my $code=$client->responseCode();
1345 0 0         if ($code==200)
1346             {
1347 0           print "200: ".$client->responseContent()."\n";
1348 0           my $resp=decode_json($client->responseContent());
1349 0           my $dealReference=$resp->{dealReference};
1350 0           print "$name, dr=$dealReference\n";
1351 0 0 0       if (defined $dealReference and length($dealReference)>5)
1352             {
1353 0           push(@done,$position);
1354 0           next;
1355             }
1356             }
1357 0           print "$name, failed code $code \n";
1358 0           push(@notdone,$position);
1359             }
1360 0 0         @$done=@done if ($done);
1361 0 0         @$notdone=@notdone if ($notdone);
1362 0           printf "done=%d notdone=%d\n",0+@done,0+@notdone;
1363 0           print "notdone:\n";
1364              
1365 0           return;
1366              
1367 0           my $format="%-41sinstrumentName %+4.2fsize %-9.2flevel ".
1368             "%-8.2fbid £%-8.2fprofit %4.1fprofitpc%% £%10.2fatrisk\n";
1369              
1370 0           $self->printpos("stdout" , ['Name','Size','Open','Latest','P/L','P/L%','Value','Stop','Stop'], $format);
1371              
1372 0           map { $self->printpos("stdout" , $_, $format) } @notdone;
  0            
1373              
1374             }
1375              
1376             #####################################################################
1377              
1378             =head2 prices - Obtain historical prices
1379              
1380             Obtain historical price information on an instrument.
1381              
1382             =head3 Parameters
1383              
1384             Unused parameters should be set as undef or ''. (either);
1385              
1386             1 A aubstring to be searched for in the name. Eg "UB.D.FTNT.DAILY.IP"
1387              
1388             2 Resolution. Should be one of the IG defined strings (left) or (in my opinion more memorable) aliases (right)
1389              
1390             DAY 1d
1391             HOUR 1h
1392             HOUR_2 1h
1393             HOUR_3 2h
1394             HOUR_4 3h
1395             MINUTE 1m
1396             MINUTE_2 2m
1397             MINUTE_3 3m
1398             MINUTE_5 5m
1399             MINUTE_10 10m
1400             MINUTE_15 15m
1401             MINUTE_30 30m
1402             SECOND 1s
1403             WEEK 1w
1404             MONTH 1M
1405              
1406             4, 5 pageNumber, pageSize What page to produce, and how many items on it.
1407              
1408             6, 7 from , to (dates) can be a string of the form 2021-01-01T16:15:00 or a Time::Piece
1409              
1410             8 max Limits the number of price points (not applicable if a date range has been specified)
1411              
1412            
1413              
1414              
1415              
1416             =cut
1417              
1418             #####################################################################
1419             # Historical prices
1420             # epic, resolution , pagenum, pagessize, from.to max
1421             #####################################################################
1422             sub prices
1423             {
1424              
1425 0     0 1   my $self=shift;
1426 0           my $epic=shift;
1427 0           my $resolution=shift;
1428 0           my $pagenumber=shift;
1429 0           my $pagesize=shift;
1430              
1431 0           my $from=shift;
1432 0           my $to=shift;
1433 0           my $max=shift;
1434              
1435            
1436 0 0         if (ref($to) eq 'Time::Piece')
1437             {
1438 0           $to=$to->strftime("%Y-%m-%dT%H:%M:%S");
1439             }
1440 0 0         if (ref($from) eq 'Time::Piece')
1441             {
1442 0           $from=$from->strftime("%Y-%m-%dT%H:%M:%S");
1443             }
1444              
1445 0   0       $pagesize//=1; # set a default of 1 item per page
1446             # $pagenumber=1; # set a default of page 1, not needed as already set as defult
1447              
1448 0           my $headers = {
1449             'Content-Type' => 'application/json; charset=UTF-8',
1450             'Accept' => 'application/json; charset=UTF-8',
1451             VERSION => 3,
1452             # 'IG-ACCOUNT-ID'=> $accountid,
1453             CST=>$self->CST,
1454             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
1455             'X-IG-API-KEY'=> $self->apikey,
1456             };
1457              
1458 0           $resolution="MINUTE_10";
1459 0           $resolution="HOUR_4";
1460              
1461             # An alternative and more memorable resolution constants. IG values can also be used.
1462 0 0         $resolution="DAY" if ($resolution eq '1d');
1463 0 0         $resolution="HOUR" if ($resolution eq'1h');
1464 0 0         $resolution="HOUR_2" if ($resolution eq '1h');
1465 0 0         $resolution="HOUR_3" if ($resolution eq '2h');
1466 0 0         $resolution="HOUR_4" if ($resolution eq '3h');
1467 0 0         $resolution="MINUTE" if ($resolution eq '1m');
1468 0 0         $resolution="MINUTE_2" if ($resolution eq '2m');
1469 0 0         $resolution="MINUTE_3" if ($resolution eq '3m');
1470 0 0         $resolution="MINUTE_5" if ($resolution eq '5m');
1471 0 0         $resolution="MINUTE_10" if ($resolution eq '10m');
1472 0 0         $resolution="MINUTE_15" if ($resolution eq '15m');
1473 0 0         $resolution="MINUTE_30" if ($resolution eq '30m');
1474 0 0         $resolution="SECOND" if ($resolution eq '1s');
1475 0 0         $resolution="WEEK" if ($resolution eq '1w');
1476 0 0         $resolution="MONTH" if ($resolution eq '1M');
1477              
1478             defined $resolution and
1479 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            
1480             die "Resolution is '$resolution', not recognised";
1481              
1482             #my $jheaders=JSON->new->canonical->encode($headers);
1483              
1484 0           my $client = REST::Client->new();
1485 0           $client->setHost($self->_url);
1486             #my $r;
1487              
1488 0           my $values={
1489             pageNumber=>$pagenumber,
1490             pageSize=>$pagesize,
1491             resolution=>$resolution,
1492             from=>$from,
1493             to=>$to,
1494             max=>$max,
1495             } ;
1496              
1497 0           delete @$values{ grep {!$values->{$_} } keys %$values} ; # delete all empty or undef values
  0            
1498 0           map { $values->{$_}=$_."=".$values->{$_} } keys %$values ;
  0            
1499            
1500 0           my $url;
1501 0           $url=join('&',sort values(%$values));
1502 0 0         $url='?'.$url if ($url);
1503 0           $url="prices/$epic".$url;
1504              
1505              
1506 0           $client->GET ( $url,
1507             $headers
1508             );
1509              
1510            
1511 0           my $resp=decode_json($client->responseContent());
1512              
1513              
1514              
1515 0           $self->flatten_withunder($resp);
1516             # print JSON->new->canonical->pretty->encode($resp); exit;
1517              
1518 0           return $resp;
1519             }
1520             #####################################################################
1521             # flatten_withunder
1522             # flattens a deep hash, 3 levels max, where complex hashes are
1523             # removed and replace with _ joined shallow hash values
1524             # for exapmple:
1525             # {
1526             # "metadata" : {
1527             # "allowance" : {
1528             # "allowanceExpiry" : 530567,
1529             # "remainingAllowance" : 9557,
1530             # "totalAllowance" : 10000
1531             # },
1532             # ...
1533             #
1534             # becomes
1535             # {
1536             # "metadata_allowance_allowanceExpiry" : 530473,
1537             # "metadata_allowance_remainingAllowance" : 9556,
1538             # "metadata_allowance_totalAllowance" : 10000,
1539             # ...
1540             # The advantage of a flattened structure is its easier to print.
1541              
1542             #####################################################################
1543              
1544             =head2 flatten_withunder
1545              
1546             Flatten a deep structure, up to 3 layers deep using underscores to create new keys by concatenating deeper keys.
1547             Deep keys are removed. More than 3 layers can be removed by calling multiply.
1548              
1549             =head3 Parameters
1550            
1551             One or more scalers to opperate on or an array. Each will be flattened
1552             where there are hashes or hashes or hashes of hashes of hashes
1553             to a single depth, with elements joined by underscores
1554              
1555             =head3 Example
1556              
1557             {
1558             "metadata" : {
1559             "allowance" : {
1560             "allowanceExpiry" : 530567,
1561             "remainingAllowance" : 9557,
1562             "totalAllowance" : 10000
1563             },
1564             ...
1565            
1566             becomes
1567             {
1568             "metadata_allowance_allowanceExpiry" : 530473,
1569             "metadata_allowance_remainingAllowance" : 9556,
1570             "metadata_allowance_totalAllowance" : 10000,
1571             ...
1572              
1573             The advantage of a flattened structure is its easier to print with existing fuunctions like printpos
1574              
1575             =cut
1576              
1577             #####################################################################
1578             sub flatten_withunder
1579             {
1580 0     0 1   my ($self)=shift;
1581 0           my (@items)=@_;
1582 0           my $fudebug=0;
1583 0 0         $fudebug and printf "%d items to process\n",0+@items;
1584 0           for my $item (@items)
1585             {
1586 0 0         $fudebug and print "item is a ".ref($item)."\n";
1587 0 0         return if (ref($item)eq '');
1588 0 0         if (ref($item) eq 'HASH')
1589             {
1590 0 0         $fudebug and print "is a hash\n";
1591 0           for my $key (keys %$item)
1592             {
1593 0 0         $fudebug and print "key1 $key\n";
1594 0 0         if (ref($item->{$key}) eq 'HASH')
1595             {
1596 0           for my $key2 (keys %{$item->{$key}})
  0            
1597             {
1598 0 0         $fudebug and print "keyr2 $key2\n";
1599 0           $item->{$key."_".$key2}=$item->{$key}->{$key2};
1600 0 0         $fudebug and printf "creating $key"."_"."$key2 as a %s\n",ref($item->{$key}->{$key2});
1601            
1602             # $self->flatten_withunder($item->{$key}) if (ref($item->{$key}->{$key2}) eq 'HASH');
1603 0 0         if (ref($item->{$key}->{$key2}) eq 'HASH')
1604             {
1605 0           for my $key3 (keys %{$item->{$key}->{$key2}})
  0            
1606             {
1607 0 0         $fudebug and print "key3 $key3\n";
1608 0           $item->{$key."_".$key2."_".$key3}=$item->{$key}->{$key2}->{$key3};
1609 0 0         $fudebug and printf "creating $key"."_$key2"."_$key3 as a %s\n",ref($item->{$key}->{$key2}->{$key3});
1610             }
1611 0 0         $fudebug and print "deleting $key->$key2 and $key _$key2\n";
1612 0           delete $item->{$key}->{$key2};
1613 0           delete $item->{$key."_".$key2};
1614             }
1615             }
1616 0 0         $fudebug and print "deleting: $key\n";
1617 0           delete $item->{$key};
1618             }
1619 0 0         if (ref($item->{$key}) eq 'ARRAY')
1620             {
1621 0 0         $fudebug and print "$key is array ref\n";
1622 0           for (@{$item->{$key}})
  0            
1623             {
1624 0           $self->flatten_withunder($_);
1625             }
1626             }
1627             }
1628             }
1629 0 0         if (ref($item) eq 'ARRAY')
1630             {
1631 0 0         $fudebug and print "is an array\n";
1632 0           for (@$item)
1633             {
1634 0           $self->flatten_withunder($_);
1635             }
1636             }
1637             }
1638 0 0         $fudebug and print "processed\n";
1639             }
1640              
1641              
1642             #####################################################################
1643             # uses known structure of supplied deep hash to search for item
1644             # should probably replace with a more generalised deep fetch function.
1645             #####################################################################
1646              
1647             =head2 fetch
1648              
1649             This function is a way to hide the various structures a position may have
1650              
1651             Obsolete but still used sometimes.
1652              
1653             Parameters
1654              
1655             1 A position hash ref, $h
1656             2 The name of the item to be retrieved.
1657              
1658             Returns undef if not found, or the value of item if it is.
1659              
1660             The function looks first in $h->{item} then
1661             in $h->{position}=>{item} and then in $h->{market}->{item}
1662              
1663             Its only useful with positions, not hashes in general.
1664              
1665             =cut
1666             #####################################################################
1667             sub fetch
1668             {
1669 0     0 1   my ($self,$position,$item)=@_;
1670              
1671             # return "NOT A HASREF $position"if (ref($position) ne 'HASH');
1672 0 0         die "supplied position $position to fetch() is not a HASHREF" if (ref($position) ne 'HASH');
1673 0 0         defined $item or die "fetch, item undefined";
1674 0           my $p=$position->{position};
1675 0           my $m=$position->{market};
1676              
1677 0 0         if (exists $position->{$item}) { return $position->{$item}; }
  0 0          
    0          
1678 0           elsif (exists $p->{$item}) { return $p->{$item}; }
1679 0           elsif (exists $m->{$item}) { return $m->{$item}; }
1680             else {
1681 0           return undef;
1682             }
1683              
1684             }
1685              
1686             #####################################################################
1687             # given an instrument name in search, look for it inside the instrumentName, and return
1688             # the epic. Fail if result is not 1 item.
1689             # used for filling in the epic (a unique identifier) in old data files
1690             # where I forgot to store it.
1691             #####################################################################
1692              
1693             =head2 epicsearch
1694              
1695             Find the epic (unique identifier) for an instrument from the underlying share.
1696              
1697             This function calls IG's search API looking for a match to the name. If found
1698             the value of the epic is returned.
1699              
1700             =head3 Status - very experimental. Seems to work well.
1701              
1702             Contains print and die statements. Useful if you forgot to record the epic.
1703              
1704             =cut
1705              
1706             #####################################################################
1707             sub epicsearch
1708             {
1709 0     0 1   my ($self,$search)=@_;
1710 0           my $headers =
1711             {
1712             'Content-Type' => 'application/json; charset=UTF-8',
1713             'Accept' => 'application/json; charset=UTF-8',
1714             VERSION => 1,
1715             CST=>$self->CST,
1716             'X-SECURITY-TOKEN'=> $self->XSECURITYTOKEN,
1717             'X-IG-API-KEY'=> $self->apikey,
1718             };
1719             #my $jheaders = encode_json($headers);
1720 0           my $jheaders=JSON->new->canonical->encode($headers);
1721 0           my $client = REST::Client->new();
1722 0           $client->setHost($self->_url);
1723 0           $search=~s#/#%2F#g;
1724 0           my $url="/markets?searchTerm=$search";
1725 0           $search=~s#%2F#/#g;
1726 0           $url=~s/ /%20/g;
1727 0           my $r=$client->GET ( $url, $headers);
1728              
1729             # my $resp=decode_json($client->responseContent());
1730              
1731              
1732             #print "url=$url\n";
1733 0           my $code;
1734              
1735 0           $code=$client->responseCode();
1736              
1737 0           my $retried=0;
1738 0   0       while ($code==403 and $retried<4)
1739             {
1740 0           sleep 10;
1741 0           $retried++;
1742 0           $r=$client->GET ( $url, $headers);
1743 0           $code=$client->responseCode();
1744             # print "search retried\n";
1745             }
1746              
1747 0 0         die "response code from url='$url' code=$code retried $retried times" if ($code!=200);
1748              
1749 0           my $markets=decode_json($client->responseContent);
1750             # print JSON->new->ascii->pretty->encode($markets)."\n";
1751              
1752 0           my @wantedmarkets=grep { $_->{expiry} eq 'DFB' } @{$markets->{markets}};
  0            
  0            
1753 0           @wantedmarkets=grep { $self->_nothe($self->fetch($_,'instrumentName') , $search) } @wantedmarkets;
  0            
1754              
1755 0           @wantedmarkets=map { $_->{epic} } @wantedmarkets;
  0            
1756 0 0         die "Zero epics found for search $search" if (@wantedmarkets==0);
1757 0 0         die "Multiple epics found @wantedmarkets for search $search" if (@wantedmarkets!=1);
1758              
1759 0           return $wantedmarkets[0];
1760              
1761             }
1762             #####################################################
1763             # remove a trailing 'the'
1764             #####################################################
1765             sub _nothe
1766             {
1767 0     0     my ($self,$x,$y)=@_;
1768              
1769             # print "comparing $x $y \n";
1770 0           $x=~s#/.*$##;
1771 0           $y=~s#/.*$##;
1772              
1773 0           return $x eq $y;
1774             }
1775             # so this is used to read one of my old data files.
1776             ##################################################################################
1777             # Reads am ascii file - older format and returns a list of positions,
1778             # a hashref keyed on epic.
1779             ##################################################################################
1780              
1781             =head2 readfile_oldformat
1782              
1783              
1784             Parameters
1785              
1786             1 Path to a file to read
1787              
1788             A file readable by this function may be generated by using printpos with format as follows:
1789             "%-41sinstrumentName %+6.2fsize %-9.2flevel ".
1790             "%-9.2fbid £%-8.2fprofit %5.1fprofitpc%% £%10.2fatrisk\n",
1791              
1792             This file was originally generated to be human readable so reading by machine is a stretch.
1793              
1794             =head3 Status - downright broken (for you). Sorry!
1795              
1796             May contains print and die statements. Contaions hardcoded paths that will need to be
1797             changed.
1798              
1799             =cut
1800             ##################################################################################
1801             sub readfile_oldformat
1802             {
1803 0     0 1   my ($self, $f,$writenewfile)=@_;
1804 0           my $positions={};
1805 0           my $totalline;
1806 0           $f="/home/mark/igrec/results/$f";
1807 0 0         open(F,$f) or die "cannot open $f";
1808             #Roku Inc +0.38 16501.00 21842.0 £2029.58 32.4% £ 8299.96
1809 0           my @fieldhashnames=qw(epic instrumentName size level bid profit profitpc atrisk);
1810 0           while (<F>)
1811             {
1812 0           my @fields;
1813 0           my @names=@fieldhashnames;
1814 0           my $position={};
1815              
1816 0           chomp;
1817 0 0         if (m/\|/)
    0          
    0          
1818             {
1819 0           die;
1820             }
1821             elsif (m/^Name/)
1822             {
1823 0           s/[£%]//g;
1824 0           @fields=split(/ +/);
1825 0           unshift(@fields,'Epic');
1826             # print "#".join("\|",@fields)."\n";
1827             }
1828             elsif (m/^Total/)
1829             {
1830 0           $totalline=$_;
1831             }
1832             else
1833             {
1834 0           my $name=substr($_,0,42);
1835 0           my $line=substr($_,43);
1836 0           $name=~s/ +$//;
1837 0           $line=~s/[\$£%]//g;
1838 0           @fields=split(/ +/,$line);
1839 0           my $epic=$self->epicsearch($name);
1840 0           unshift(@fields,$epic,$name);
1841             #die "$line\n@fields\n@names";
1842 0           while (@names)
1843             {
1844 0           $position->{shift(@names)}=shift(@fields);
1845             }
1846 0           $positions->{$epic}=$position;
1847             }
1848             }
1849             # close F;
1850 0 0         if ($writenewfile)
1851             {
1852 0           $f=~s/results/r2/;
1853 0 0         if (! -e $f)
1854             {
1855 0 0         open(my $g,">" , $f) or die "Cannot open $f for write";
1856 0           my $format= "%sepic|%sinstrumentName|%0.2fsize|%-0.2flevel|".
1857             "%-0.2fbid|£%-0.2fprofit|%0.1fprofitpc%%|£%0.2fatrisk\n",
1858             print $g "Epic|Instrumentname|Size|Level|Bid|Profit£|Profitpc%|Atrisk£\n";
1859 0           my $a=$self->agg([values %$positions]);
1860 0           for (@$a)
1861             {
1862 0           $self->printpos($g,$_,$format);
1863             }
1864 0           print $g $totalline."\n";
1865             }
1866             }
1867 0           return $positions;
1868             }
1869             ##################################################################################
1870             # Reads am ascii file and returns a list of positions,
1871             # a hashref keyed on epic.
1872             ##################################################################################
1873              
1874             =head2 readfile
1875              
1876              
1877             Parameters
1878              
1879             1 Path to a file to read
1880              
1881             A file readable by this function may be generated by using printpos with format as follows:
1882             "%sepic|%sinstrumentName|%0.2fsize|%-0.2flevel|".
1883             "%-0.2fbid|£%-0.2fprofit|%0.1fprofitpc%%|£%0.2fatrisk|%smarketStatus\n",
1884              
1885             =head3 Status - downright broken (for you). Sorry!
1886              
1887             The function contains a hardcoded path for reading the files. You would need a
1888             crontab entry to generate them.
1889              
1890             May contain print and die statements. Contains hardcoded paths that will need to be
1891             changed.
1892              
1893             =cut
1894             ##################################################################################
1895             sub readfile
1896             {
1897 0     0 1   my ($self,$f)=@_;
1898              
1899 0           my $positions={};
1900 0           $f="/home/mark/igrec/r2/$f";
1901 0 0         open(F,$f) or die "cannot open $f";
1902 0           my @fieldhashnames=qw(epic instrumentName size level bid profit profitpc atrisk tradeable);
1903 0           my $ln=0;
1904 0           while (<F>)
1905             {
1906 0           my @fields;
1907 0           my @names=@fieldhashnames;
1908 0           my $position={};
1909              
1910 0           $ln++;
1911 0           chomp;
1912 0 0         if (m/^Total/)
    0          
    0          
    0          
    0          
    0          
1913             {
1914 0           next;
1915             }
1916             elsif (m/ Positions$/)
1917             {
1918 0           next;
1919             }
1920             elsif (m/^ *$/)
1921             {
1922 0           next;
1923             }
1924             elsif (m/#/)
1925             {
1926 0           next;
1927             }
1928             elsif (!m/\|/)
1929             {
1930 0           die "No | lin line $ln file $f";
1931             }
1932             elsif (m/Epic/)
1933             {
1934 0           next;
1935             }
1936             else
1937             {
1938 0           s/[£&]//g;
1939 0           @fields=split(/\|/);
1940 0           for my $fieldname (@fieldhashnames)
1941             {
1942 0 0         die if (!defined $names[0]);
1943             #print "names[0]=$names[0]\n";
1944 0           $position->{$fieldname}=shift(@fields);
1945             }
1946 0           $positions->{$position->{epic}}=$position;
1947 0   0       $position->{marketStatus}//=''; # older files do not record this.
1948             }
1949             }
1950 0           return $positions;
1951             }
1952             #####################################################################
1953             # format strings contained embedded printf specifiers followed by
1954             # a hash element name .
1955             #
1956             # eg "%sdate %sdescription %sepic %sstatus\n";
1957             # eg "%-20sdate %-30sdescription %-20sepic %-15sstatus\n";
1958             # eg
1959             # "%sepic|%sinstrumentName|%6.2fsize|%-9.2flevel|".
1960             # "%-9.2fbid|£%-8.2fprofit|%5.1fprofitpc%%|£%10.2fatrisk\n",
1961             #eg
1962             # "%-41sinstrumentName %+6.2fsize %-9.2flevel ".
1963             # "%-9.2fbid £%-8.2fprofit %5.1fprofitpc%% £%10.2fatrisk\n",
1964             # Arguments:
1965             # 1) An IG object ref. (self) Is not really used.
1966             # 2) Either "stdout" or an open writable file handle.
1967             # 3) A hash possibly deep, with items. Ig the item is not found directly in the hash,
1968             # the $self->fetch function is used for access. If still not found
1969             # then "UNDEF" is printed.
1970             # CHANGED to $self->uds
1971             # OR: If this is an array ref, then a title line is ptinted using the format string
1972             # and the referenced array of titles
1973             # OR: If empty dtring ort undef, derive titles from the format
1974             # string and print a title line.
1975             # 4) A formatting string. Can contain text, containing embedded
1976             # format instructions like %6.2fsize here %6.2f is a print f
1977             # specifier and size is the name of the item to retrieve from the hash.
1978             # 5,6) up /down can be percent gives green if > up, bold green if > 5*up.
1979             # can be a coloration function of position.
1980             # just one function, so no down ever.
1981             # function takes argument position, and returns optional colors
1982             #####################################################################
1983              
1984             =head2 printpos
1985              
1986             =head3 Parmeters
1987              
1988             A file handle or the word stdout, all output sent here.
1989              
1990             A hashref of items to print
1991             OR: If this is an array ref, then a title line is ptinted using the format string
1992             and the referenced array of titles
1993             OR: If empty string or undef, derive titles from the format
1994             string and print a title line.
1995              
1996             A formatting string. Can contain text, containing embedded
1997             format instructions like %6.2fsize here %6.2f is a print f
1998             specifier and size is the name of the item to retrieve from the hash.
1999              
2000             OPTIONAL up can be percent gives green if > up, bold green if > 5*up.
2001             can be a coloration function of position. Just one function, so no down ever if a function is given
2002             function takes argument position, and returns optional colors
2003              
2004             OPTIONAL down can be percent gives red if <down , bold red if < 5*down.
2005              
2006             =head3 Description
2007              
2008             This is a very general function will work with any hash.
2009              
2010             =cut
2011             #####################################################################
2012             sub printpos
2013             {
2014              
2015 0     0 1   my ($self,$out,$position,$format,$up,$down)=@_;
2016              
2017 0           my $colsub;
2018              
2019 0 0         $out=*STDOUT if ($out eq "stdout");
2020              
2021 0 0 0       $down=-$up if (defined $up and ref($up) eq '' and !defined $down) ;
      0        
2022              
2023 0 0 0       if (defined $up and ref($up) ne 'CODE')
2024             {
2025             $colsub=sub
2026             {
2027 0     0     my ($position)=shift;
2028 0           my $v1=$position->{dbid};
2029 0           my $col='';
2030 0           $v1=~s/%//;
2031 0 0 0       $col=Green if (defined $up and $v1>$up);
2032 0 0 0       $col=Red if (defined $down and $v1<$down);
2033 0 0 0       $col=Green+Bold if (defined $up and $v1>$up*5);
2034 0 0 0       $col=Red+Bold if (defined $down and $v1<5*$down);
2035 0           return $col;
2036 0           };
2037             }
2038 0 0 0       $colsub=$up if (defined $up and ref($up) eq 'CODE');
2039 0 0   0     $colsub=sub {''} if (!defined $up);
  0            
2040              
2041              
2042 0           my $titles=$format;
2043 0 0         if (ref($position) eq 'ARRAY') # its titles to print!
2044             {
2045             #$format=~s/%[-+]/%/g;
2046             #print "$format\n";
2047 0           while ($format=~m/[-+]?([0-9]+)\.([0-9]+)/)
2048             {
2049 0           my $x;
2050 0           $x=$1;
2051 0 0         abs($2)>abs($x) and $x=$2;
2052 0           $format=~s/%([-+]?)([0-9]+)\.([0-9]+)/%$1$x/;
2053             }
2054             #print "#1 $format\n";
2055 0           $format=~s/%\+\+/%+/g;
2056             #print "#2 $format\n";
2057 0           $format=~s/%([-\+]?[0-9]+)\.[0-9]+/%$1/g;
2058             #print "#3 $format\n";
2059 0           $format=~s/%([-\+]?[0-9]+)[fd]/%$1s/g;
2060             #print "#4 $format\n";
2061 0           $format=~s/%([-\+]?[0-9]*)([a-zA-Z_][a-zA-Z0-9_]*)/%$1s/g;
2062             #die $format;
2063             # print "$format\n"; exit;
2064             #$"=":"; print "@$position\n";
2065            
2066              
2067 0           $format=~s/[\x82\x83\xc3]//g; # so we get some strange characters like ÃÂ occuring in pairs. Not sure why. This removes them.
2068             #$format="%-41s %+7s %11s %-10s £%-10s %5s%% £%12s %-9s %-4s";
2069             #print "$format\n"; #exit;
2070 0 0 0       print $out Bold if ($self->col and defined $INC{'Term/Chrome.pm'});
2071             # print "format='$format' @$position\n";
2072 0           printf $out $format,@$position;
2073 0 0 0       print $out Reset if ($self->col and defined $INC{'Term/Chrome.pm'});
2074 0           return;
2075             }
2076              
2077             # auto generated title list from the names
2078 0 0 0       if (!defined $position or $position eq '')
2079             {
2080 0           $titles=~s/\n//g;
2081 0           $titles=~s/%([-+0-9.]*)([sfd])/%/g;
2082 0           $titles=~s/%%/__PC__/g;
2083 0           $titles=~s/%//; # just one
2084 0           $titles=~s/£%([a-zA-Z]+)/%$1£/g;
2085 0           my @titles=split(/%/,$titles);
2086 0           map {s/[|,]//g } @titles;
  0            
2087 0           map {s/ +//g } @titles;
  0            
2088 0           map { s/__PC__//g; } @titles;
  0            
2089 0           map { s/([\w']+)/\u\L$1/g; } @titles;
  0            
2090 0           while ($format=~m/%[-+]?([0-9]+)\.([0-9]+)/)
2091             {
2092 0           my $x;
2093             #my $x=$1+$2;
2094 0           $x=$1;
2095 0 0         $2>$x and $x=$2;
2096 0           $format=~s/%([-+]?)([0-9]+)\.([0-9]+)/%$1$x/;
2097             }
2098 0           $format=~s/(%[-+0-9.]*)[a-zA-Z]+/$1s/g;
2099             #$format=~s/(%[-+0-9]+)\.[0-9]+/$1/g;
2100 0           $format=~s/£//g;
2101             #die "format=$format titles=@titles";
2102 0           $format=~s/[\x82\x83\xc3]//g; # so we get some strange characters like ÃÂ occuring in pairs. Not sure why. This removes them.
2103 0 0 0       print $out Bold if ($self->col and defined $INC{'Term/Chrome.pm'});
2104 0           printf $out $format, @titles;
2105 0 0 0       print $out Reset if ($self->col and defined $INC{'Term/Chrome.pm'});
2106 0           return;
2107             }
2108              
2109              
2110              
2111             # $p=$position->{position};
2112             # $m=$position->{market};
2113              
2114 0           $format=~s/%%/##/g;
2115              
2116              
2117             # 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__/))
2118             # {
2119             # my $s;
2120             # $s=$activity->{$2};
2121             # my $pos=$1;
2122             # $pos=~s/-//;
2123             # $s=substr($s,0,$pos) if (defined(pos) and $pos ne '' and $pos<length($s));
2124             # push(@args,$s);
2125             # }
2126              
2127 0           my $col='';
2128 0           while ($format=~s/%([-+0-9.]*[dsf])([a-zA-Z_][a-zA-Z0-9_]*)/%s/)
2129             {
2130 0           my $s;
2131              
2132 0           my $item=$2;
2133 0   0       my $len=$1//"";
2134             # die "item is UNDEF" if ($item eq 'UNDEF');
2135             # die "len is UNDEF" if ($len eq 'UNDEF');
2136             # $len='' if ($len eq 'UNDEF');
2137 0 0         $len="%".$len if ($len);
2138 0 0 0       if (defined $item and $item ne '' and exists $position->{$item} and defined $position->{$item})
    0 0        
      0        
2139             {
2140 0           $position->{$item}=~s/%//g;
2141             #$position->{$item}='0' if ($position->{$item} eq 'UNDEF');
2142 0           $s=sprintf($len,$position->{$item});
2143 0 0 0       if ($item eq 'dbid' and exists $INC{'Term/Chrome.pm'} and $self->col)
      0        
2144             {
2145             ##my $v1=$position->{dbid};
2146             ##$v1=~s/%//;
2147             ##$col=Green if (defined $up and $v1>$up);
2148             ##$col=Red if (defined $down and $v1<$down);
2149             ##$col=Green+Bold if (defined $up and $v1>$up*5);
2150             ##$col=Red+Bold if (defined $down and $v1<5*$down);
2151              
2152             # $col=&$colsub($position);
2153             }
2154             # $col=Yellow if (defined $up);
2155             # $col=&$colsub($position);
2156             }
2157             elsif (defined $self->fetch($position,$item))
2158             {
2159             #$s=sprintf($len,$self->fetch($position,$2)//"UNDEF");
2160 0   0       $s=sprintf($len,$self->fetch($position,$item)//$self->uds);
2161 0 0 0       if ($item eq 'dbid' and defined $INC{'Term/Chrome.pm'} and $self->col)
      0        
2162             {
2163             #my $v1;
2164              
2165             #$v1=$self->fetch($position,'dbid');
2166             #$v1=~s/%//;
2167             #$v1=100*$v1/$self->fetch($position,'bid');
2168             ###$col=Green if (defined $up and $self->col and $self->fetch($position,'dbid')/$self->fetch($position,'bid')>$up/100);
2169             ###$col=Red if (defined $down and $self->col and $self->fetch($position,'dbid')/$self->fetch($position,'bid')<$down/100);
2170             #$col=Green if (defined $up and $self->col and $v1>$up);
2171             #$col=Red if (defined $down and $self->col and $v1<$down);
2172             #$col=Green+Bold if (defined $up and $self->col and $v1>$up*5);
2173             #$col=Red+Bold if (defined $down and $self->col and $v1<5*$down);
2174             #$col=&$colsub($position);
2175             }
2176             #$col=&$colsub($position);
2177              
2178             }
2179             else
2180             {
2181 0           $len=~s/[df]/s/;
2182 0           $len=~s/\.[0-9]+//;
2183             #$s=sprintf($len,"UNDEF");
2184 0           $s=sprintf($len,$self->uds);
2185             }
2186              
2187 0           $col=&$colsub($position);
2188 0           $len=~s/[dsf]$//;
2189 0 0         if ($len ne '') # len can be something like 0.2
2190             {
2191 0           $len=~s/%//;
2192 0 0         $len=abs($len) if ($len ne '');
2193 0 0 0       $s=substr($s,0,$len) if ($len and $len<length($s) and $len>=1);
      0        
2194             }
2195              
2196 0           $format=~s/%s/$s/;
2197             }
2198              
2199 0 0 0       $col=&$colsub($position)//'' if ($self->col and defined $INC{'Term/Chrome.pm'});
      0        
2200 0           $format=~s/##/%/g;
2201 0           $format=~s/£-/-£/g;
2202 0           $format=~s/[\x82\x83\xc3]//g; # so we get some strange characters like ÃÂ occuring in pairs. Not sure why. This removes them.
2203 0           print $out $col, $format;
2204 0 0         if (ref($col) ne '')
2205 0           { print $out Reset;
2206             }
2207              
2208             }
2209              
2210              
2211              
2212             =head2 sortrange
2213              
2214             =head3 Parameters
2215            
2216             Ref to an array containing dates in printed ascii format.
2217              
2218             If there are no dates or an empty array, an empty string is returned.
2219              
2220             If there is one date, then that date is returned
2221              
2222             If there is more than one then the first and last after sorting is returned, with a dash between them.
2223              
2224             This is used in aggregation of positions and relates to creation dates with multiple positions
2225             in the same security purchased at different times.
2226              
2227             =cut
2228              
2229             sub sortrange
2230             {
2231 0     0 1   my ($self,$ar)=@_;
2232              
2233 0           my @dates=sort @$ar;
2234            
2235 0 0         return '' if (@dates==0);
2236 0 0         return $dates[0] if (@dates==1);
2237 0           return $dates[0] . "-".$dates[-1];
2238             }
2239              
2240             =head1 DEPENDENCIES
2241              
2242             Moose
2243             Term::Chrom if available.
2244              
2245             =head1 UTILITIES
2246              
2247             A more complete position lister is given as igdisp.pl
2248              
2249             =head1 AUTHOR
2250              
2251             Mark Winder, C<< <markwin at cpan.org> >>
2252              
2253             =head1 BUGS
2254              
2255             Please report any bugs or feature requests to C<bug-finance-ig at rt.cpan.org>, or through
2256             the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Finance-IG>. I will be notified, and then you'll
2257             automatically be notified of progress on your bug as I make changes.
2258              
2259              
2260              
2261              
2262             =head1 SUPPORT
2263              
2264             You can find documentation for this module with the perldoc command.
2265              
2266             perldoc Finance::IG
2267              
2268              
2269             You can also look for information at:
2270              
2271             =over 4
2272              
2273             =item * RT: CPAN's request tracker (report bugs here)
2274              
2275             L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Finance-IG>
2276              
2277             =item * CPAN Ratings
2278              
2279             L<https://cpanratings.perl.org/d/Finance-IG>
2280              
2281             =item * Search CPAN
2282              
2283             L<https://metacpan.org/release/Finance-IG>
2284              
2285             =back
2286              
2287              
2288             =head1 ACKNOWLEDGEMENTS
2289              
2290             =head1 FURTHER READING
2291              
2292             IG REST API Reference https://labs.ig.com/rest-trading-api-reference
2293              
2294             =head1 LICENSE AND COPYRIGHT
2295              
2296             This software is Copyright (c) 2020 by Mark Winder.
2297              
2298             This is free software, licensed under:
2299              
2300             The Artistic License 2.0 (GPL Compatible)
2301              
2302              
2303             =cut
2304              
2305             1; # End of Finance::IG