File Coverage

lib/Finance/IG.pm
Criterion Covered Total %
statement 27 665 4.0
branch 1 306 0.3
condition 0 141 0.0
subroutine 9 39 23.0
pod 26 26 100.0
total 63 1177 5.3


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