chiark / gitweb /
Allow computing and using dest stall poe limit
[ypp-sc-tools.db-test.git] / yarrg / web / routetrade
1 <%doc>
2
3  This is part of the YARRG website.  YARRG is a tool and website
4  for assisting players of Yohoho Puzzle Pirates.
5
6  Copyright (C) 2009 Ian Jackson <ijackson@chiark.greenend.org.uk>
7  Copyright (C) 2009 Clare Boothby
8
9   YARRG's client code etc. is covered by the ordinary GNU GPL (v3 or later).
10   The YARRG website is covered by the GNU Affero GPL v3 or later, which
11    basically means that every installation of the website will let you
12    download the source.
13
14  This program is free software: you can redistribute it and/or modify
15  it under the terms of the GNU Affero General Public License as
16  published by the Free Software Foundation, either version 3 of the
17  License, or (at your option) any later version.
18
19  This program is distributed in the hope that it will be useful,
20  but WITHOUT ANY WARRANTY; without even the implied warranty of
21  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  GNU Affero General Public License for more details.
23
24  You should have received a copy of the GNU Affero General Public License
25  along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
27  Yohoho and Puzzle Pirates are probably trademarks of Three Rings and
28  are used without permission.  This program is not endorsed or
29  sponsored by Three Rings.
30
31
32  This Mason component is the core trade planner for a specific route.
33
34
35 </%doc>
36 <%args>
37 $dbh
38 @islandids
39 @archipelagoes
40 $qa
41 $routeparams
42 </%args>
43 <& query_age:pageload &>
44
45 <%perl>
46
47 my $loss_per_league= defined $routeparams->{LossPerLeaguePct}
48         ? $routeparams->{LossPerLeaguePct}*0.01 : 1e-7;
49 my $loss_per_delay_slot= 1e-8;
50
51 my $now= time;
52
53 my @flow_conds;
54 my @query_params;
55 my %dists;
56 my $expected_total_profit;
57
58 my $sd_condition= sub {
59         my ($bs, $ix) = @_;
60         my $islandid= $islandids[$ix];
61         if (defined $islandid) {
62                 return "${bs}.islandid = $islandid";
63         } else {
64                 push @query_params, $archipelagoes[$ix];
65                 return "${bs}_islands.archipelago = ?";
66         }
67 };
68
69 my $specific= !grep { !defined $_ } @islandids;
70
71 my %ipair2subflowinfs;
72 # $ipair2subflowinfs{$orgi,$dsti}= [ [$orgix,$distix], ... ]
73
74 my @subflows;
75 # $subflows[0]{Flow} = { ... }
76 # $subflows[0]{Org} = $orgix
77 # $subflows[0]{Dst} = $dstix
78
79 foreach my $org_i (0..$#islandids) {
80         my $org_isle= $islandids[$org_i];
81         my $org_cond= $sd_condition->('sell',$org_i);
82         my @dst_conds;
83         foreach my $dst_i ($org_i..$#islandids) {
84                 my $dst_isle= $islandids[$dst_i];
85                 # Don't ever consider sailing things round the houses:
86                 next if defined $dst_isle and
87                         grep { $dst_isle == $_ } @islandids[$org_i..$dst_i-1];
88                 next if defined $org_isle and
89                         grep { $org_isle == $_ } @islandids[$org_i+1..$dst_i];
90                 my $dst_cond;
91                 if ($dst_i==$org_i and !defined $org_isle) {
92                         # we always want arbitrage, but mentioning an arch
93                         # once shouldn't produce intra-arch trades
94                         $dst_cond= "sell.islandid = buy.islandid";
95                 } else {
96                         $dst_cond= $sd_condition->('buy',$dst_i);
97                 }
98                 push @dst_conds, $dst_cond;
99
100                 if ($specific) {
101                         push @{ $ipair2subflowinfs{$org_isle,$dst_isle} },
102                                 [ $org_i, $dst_i ];
103                 }
104         }
105         push @flow_conds, "$org_cond AND (
106                         ".join("
107                      OR ",@dst_conds)."
108                 )";
109 }
110
111 my $stmt= "             
112         SELECT  sell_islands.islandname                         org_name,
113                 sell_islands.islandid                           org_id,
114                 sell.price                                      org_price,
115                 sell.qty                                        org_qty_stall,
116                 sell_stalls.stallname                           org_stallname,
117                 sell.stallid                                    org_stallid,
118                 sell_uploads.timestamp                          org_timestamp,
119                 buy_islands.islandname                          dst_name,
120                 buy_islands.islandid                            dst_id,
121                 buy.price                                       dst_price,
122                 buy.qty                                         dst_qty_stall,
123                 buy_stalls.stallname                            dst_stallname,
124                 buy.stallid                                     dst_stallid,
125                 buy_uploads.timestamp                           dst_timestamp,
126 ".($qa->{ShowStalls} ? "
127                 sell.qty                                        org_qty_agg,
128                 buy.qty                                         dst_qty_agg,
129 " : "
130                 (SELECT sum(qty) FROM sell AS sell_agg
131                   WHERE sell_agg.commodid = commods.commodid
132                   AND   sell_agg.islandid = sell.islandid
133                   AND   sell_agg.price = sell.price)            org_qty_agg,
134                 (SELECT sum(qty) FROM buy AS buy_agg
135                   WHERE buy_agg.commodid = commods.commodid
136                   AND   buy_agg.islandid = buy.islandid
137                   AND   buy_agg.price = buy.price)              dst_qty_agg,
138 ")."
139                 commods.commodname                              commodname,
140                 commods.commodid                                commodid,
141                 commods.unitmass                                unitmass,
142                 commods.unitvolume                              unitvolume,
143                 dist                                            dist,
144                 buy.price - sell.price                          unitprofit
145         FROM commods
146         JOIN sell ON commods.commodid = sell.commodid
147         JOIN buy  ON commods.commodid = buy.commodid
148         JOIN islands AS sell_islands ON sell.islandid = sell_islands.islandid
149         JOIN islands AS buy_islands  ON buy.islandid  = buy_islands.islandid
150         JOIN uploads AS sell_uploads ON sell.islandid = sell_uploads.islandid
151         JOIN uploads AS buy_uploads  ON buy.islandid  = buy_uploads.islandid
152         JOIN stalls  AS sell_stalls  ON sell.stallid  = sell_stalls.stallid
153         JOIN stalls  AS buy_stalls   ON buy.stallid   = buy_stalls.stallid
154         JOIN dists ON aiid = sell.islandid AND biid = buy.islandid
155         WHERE   (
156                 ".join("
157            OR   ", @flow_conds)."
158         )
159           AND   buy.price > sell.price
160         ORDER BY org_name, dst_name, commodname, unitprofit DESC,
161                  org_price, dst_price DESC,
162                  org_stallname, dst_stallname
163      ";
164
165 my $sth= $dbh->prepare($stmt);
166 $sth->execute(@query_params);
167 my @flows;
168
169 my $distquery= $dbh->prepare("
170                 SELECT dist FROM dists WHERE aiid = ? AND biid = ?
171                 ");
172 my $distance= sub {
173         my ($from,$to)= @_;
174         my $d= $dists{$from}{$to};
175         return $d if defined $d;
176         $distquery->execute($from,$to);
177         $d = $distquery->fetchrow_array();
178         defined $d or die "$from $to ?";
179         $dists{$from}{$to}= $d;
180         return $d;
181 };
182
183 my @cols= ({ NoSort => 1 });
184
185 my $addcols= sub {
186         my $base= shift @_;
187         foreach my $name (@_) {
188                 my $col= { Name => $name, %$base };
189                 $col->{Numeric}=1 if !$col->{Text};
190                 push @cols, $col;
191         }
192 };
193
194 if ($qa->{ShowStalls}) {
195         $addcols->({ Text => 1 }, qw(
196                 org_name org_stallname
197                 dst_name dst_stallname
198         ));
199 } else {
200         $addcols->({Text => 1 }, qw(
201                 org_name dst_name
202         ));
203 }
204 $addcols->({ Text => 1 }, qw(commodname));
205 $addcols->({ DoReverse => 1 },
206         qw(     org_price org_qty_agg dst_price dst_qty_agg
207         ));
208 $addcols->({ DoReverse => 1, SortColKey => 'MarginSortKey' },
209         qw(     Margin
210         ));
211 $addcols->({ DoReverse => 1 },
212         qw(     unitprofit MaxQty MaxCapital MaxProfit dist
213         ));
214 foreach my $v (qw(MaxMass MaxVolume)) {
215    $addcols->({
216         DoReverse => 1, Total => 0, SortColKey => "${v}SortKey" }, $v);
217 }
218
219 </%perl>
220
221 % if ($qa->{'debug'}) {
222 <pre>
223 <% $stmt |h %>
224 <% join(' | ',@query_params) |h %>
225 </pre>
226 % }
227
228 <& dumptable:start, qa => $qa, sth => $sth &>
229 % {
230 %   my $got;
231 %   while ($got= $sth->fetchrow_hashref()) {
232 <%perl>
233
234         my $f= $flows[$#flows];
235         if (    !$f ||
236                 $qa->{ShowStalls} ||
237                 grep { $f->{$_} ne $got->{$_} }
238                         qw(org_id org_price dst_id dst_price commodid)
239         ) {
240                 # Make a new flow rather than adding to the existing one
241
242                 $f= {
243                         Ix => scalar(@flows),
244                         %$got
245                 };
246                 $f->{"org_stallid"}= $f->{"dst_stallid"}= 'all'
247                         if !$qa->{ShowStalls};
248                 push @flows, $f;
249         }
250         foreach my $od (qw(org dst)) {
251                 $f->{"${od}Stalls"}{
252                         $got->{"${od}_stallname"}
253                     } =
254                         $got->{"${od}_qty_stall"}
255                     ;
256         }
257
258 </%perl>
259 <& dumptable:row, qa => $qa, sth => $sth, row => $f &>
260 %    }
261 <& dumptable:end, qa => $qa &>
262 % }
263
264 % if (@islandids==1) {
265 %       if (defined $islandids[0]) {
266 Searched for arbitrage trades only.
267 %       } else {
268 Searched for arbitrage trades only, in <% $archipelagoes[0] |h %>
269 <a href="docs#arbitrage">[?]</a>.
270 %       }
271 % }
272
273 <%perl>
274
275 my @sail_total;
276
277 if (!@flows) {
278         print 'No profitable trading opportunities were found.';
279         return;
280 }
281
282 foreach my $f (@flows) {
283
284         $f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
285                 ? $f->{'org_qty_agg'} : $f->{'dst_qty_agg'};
286         $f->{MaxProfit}= $f->{MaxQty} * $f->{'unitprofit'};
287         $f->{MaxCapital}= $f->{MaxQty} * $f->{'org_price'};
288
289         $f->{MaxMassSortKey}= $f->{MaxQty} * $f->{'unitmass'};
290         $f->{MaxVolumeSortKey}= $f->{MaxQty} * $f->{'unitvolume'};
291         foreach my $v (qw(Mass Volume)) {
292                 $f->{"Max$v"}= sprintf "%.1f", $f->{"Max${v}SortKey"} * 1e-6;
293         }
294
295         $f->{MarginSortKey}= sprintf "%d",
296                 $f->{'dst_price'} * 10000 / $f->{'org_price'};
297         $f->{Margin}= sprintf "%3.1f%%",
298                 $f->{'dst_price'} * 100.0 / $f->{'org_price'} - 100.0;
299
300         $f->{ExpectedUnitProfit}=
301                 $f->{'dst_price'} * (1.0 - $loss_per_league) ** $f->{'dist'}
302                 - $f->{'org_price'};
303
304         $dists{'org_id'}{'dst_id'}= $f->{'dist'};
305
306         my @uid= $f->{commodid};
307         foreach my $od (qw(org dst)) {
308                 push @uid,
309                         $f->{"${od}_id"},
310                         $f->{"${od}_price"};
311                 push @uid,
312                         $f->{"${od}_stallid"}
313                                 if $qa->{ShowStalls};
314         }
315         $f->{UidLong}= join '_', @uid;
316
317         my $base= 31;
318         my $cmpu= '';
319         map {
320                 my $uue= $_;
321                 my $first= $base;
322                 do {
323                         my $this= $uue % $base;
324 #print STDERR "uue=$uue this=$this ";
325                         $uue -= $this;
326                         $uue /= $base;
327                         $this += $first;
328                         $first= 0;
329                         $cmpu .= chr($this + ($this < 26 ? ord('a') :
330                                               $this < 52 ? ord('A')-26
331                                                          : ord('0')-52));
332 #print STDERR " uue=$uue this=$this cmpu=$cmpu\n";
333                         die "$cmpu $uue ?" if length $cmpu > 20;
334                 } while ($uue);
335                 $cmpu;
336         } @uid;
337         $f->{UidShort}= $cmpu;
338
339         if ($qa->{'debug'}) {
340                 my @outuid;
341                 $_= $f->{UidShort};
342                 my $mul;
343                 while (m/./) {
344                         my $v= m/^[a-z]/ ? ord($&)-ord('a') :
345                                m/^[A-Z]/ ? ord($&)-ord('A')+26 :
346                                m/^[0-9]/ ? ord($&)-ord('0')+52 :
347                                die "$_ ?";
348                         if ($v >= $base) {
349                                 push @outuid, 0;
350                                 $v -= $base;
351                                 $mul= 1;
352 #print STDERR "(next)\n";
353                         }
354                         die "$f->{UidShort} $_ ?" unless defined $mul;
355                         $outuid[$#outuid] += $v * $mul;
356
357 #print STDERR "$f->{UidShort}  $_  $&  v=$v  mul=$mul  ord()=".ord($&).
358 #                       "[vs.".ord('a').",".ord('A').",".ord('0')."]".
359 #                       "  outuid=@outuid\n";
360
361                         $mul *= $base;
362                         s/^.//;
363                 }
364                 my $recons_long= join '_', @outuid;
365                 $f->{UidLong} eq $recons_long or
366                         die "$f->{UidLong} = $f->{UidShort} = $recons_long ?";
367         }
368
369         if (defined $qa->{"R$f->{UidShort}"} &&
370             !defined $qa->{"T$f->{UidShort}"}) {
371                 $f->{Suppress}= 1;
372         } else {
373                 my $sfis= $ipair2subflowinfs{$f->{'org_id'},$f->{'dst_id'}};
374                 foreach my $sfi (@$sfis) {
375                         my $subflow= {
376                                 Flow => $f,
377                                 Org => $sfi->[0],
378                                 Dst => $sfi->[1],
379                                 Var => sprintf "f%ss%s_c%d_p%d_%d_p%d_%d",
380                                         $f->{Ix}, $sfi->[0],
381                                         $f->{'commodid'},
382                                         $sfi->[0], $f->{'org_price'},
383                                         $sfi->[1], $f->{'dst_price'}
384                         };
385                         push @{ $f->{Subflows} }, $subflow;
386                         push @subflows, $subflow;
387                 }
388         }
389 }
390 </%perl>
391
392 % my $optimise= $specific;
393 % if (!$optimise) {
394
395 <p>
396 % if (!$specific) {
397 Route contains archipelago(es), not just specific islands.
398 % }
399 Therefore, optimal voyage trade plan not calculated.
400
401 % } else { # ========== OPTMISATION ==========
402 <%perl>
403
404 my $cplex= "
405 Maximize
406
407   totalprofit:
408 ";
409
410 my %stall_poe_limits;
411
412 foreach my $sf (@subflows) {
413         my $eup= $sf->{Flow}{ExpectedUnitProfit};
414         $eup *= (1.0-$loss_per_delay_slot) ** $sf->{Org};
415         $cplex .= sprintf "
416                 %+.20f %s", $eup, $sf->{Var};
417         if ($qa->{ShowStalls}>=2) {
418                 my $stall= $sf->{Flow}{'dst_stallid'};
419                 push @{ $stall_poe_limits{$stall} }, $sf;
420         }
421 }
422 $cplex .= "
423
424 Subject To
425 ";
426
427 my %avail_lims;
428 foreach my $flow (@flows) {
429         next if $flow->{Suppress};
430         foreach my $od (qw(org dst)) {
431                 my $limname= join '_', (
432                         $od,
433                         'i'.$flow->{"${od}_id"},
434                         'c'.$flow->{'commodid'},
435                         $flow->{"${od}_price"},
436                         $flow->{"${od}_stallid"},
437                 );
438
439                 push @{ $avail_lims{$limname}{SubflowVars} },
440                         map { $_->{Var} } @{ $flow->{Subflows} };
441                 $avail_lims{$limname}{Qty}= $flow->{"${od}_qty_agg"};
442         }
443 }
444 foreach my $limname (sort keys %avail_lims) {
445         my $c= $avail_lims{$limname};
446         $cplex .=
447                 sprintf("    %-30s","$limname:")." ".
448                         join("+", @{ $c->{SubflowVars} }).
449                         " <= ".$c->{Qty}."\n";
450 }
451
452 foreach my $ci (0..($#islandids-1)) {
453         my @rel_subflows;
454
455         foreach my $f (@flows) {
456                 next if $f->{Suppress};
457                 my @relsubflow= grep {
458                         $_->{Org} <= $ci &&
459                         $_->{Dst} > $ci;
460                 } @{ $f->{Subflows} };
461                 next unless @relsubflow;
462                 die unless @relsubflow == 1;
463                 push @rel_subflows, @relsubflow;
464 #print " RELEVANT $ci $relsubflow[0]->{Var} ";
465         }
466 #print " RELEVANT $ci COUNT ".scalar(@rel_subflows)."  ";
467         if (!@rel_subflows) {
468                 foreach my $mv (qw(mass volume)) {
469                         $sail_total[$ci]{$mv}= 0;
470                 }
471                 next;
472         }
473
474         my $applylimit= sub {
475                 my ($mv, $f2val) = @_;
476                 my $max= $routeparams->{"Max".ucfirst $mv};
477                 $max= 1e9 unless defined $max;
478 #print " DEFINED MAX $mv $max ";
479                 $cplex .= "
480    ". sprintf("%-10s","${mv}_$ci:")." ".
481                 join(" + ", map {
482 #print " PART MAX $_->{Var} $_->{Flow}{Ix} ";
483                         $f2val->($_->{Flow}) .' '. $_->{Var};
484                 } @rel_subflows).
485                 " <= $max";
486         };
487
488         $applylimit->('mass',    sub { $_[0]{'unitmass'}  *1e-3 });
489         $applylimit->('volume',  sub { $_[0]{'unitvolume'}*1e-3 });
490         $applylimit->('capital', sub { $_[0]{'org_price'}       });
491         $cplex.= "\n";
492 }
493
494 if ($qa->{ShowStalls}>=2) {
495         my $stallpoe= $dbh->prepare(<<END);
496 SELECT max(qty*price) FROM buy WHERE stallid=?
497 END
498         foreach my $stallid (sort { $a <=> $b } keys %stall_poe_limits) {
499                 $stallpoe->execute($stallid);
500                 my ($lim)= $stallpoe->fetchrow_array();
501                 $stallpoe->finish();
502                 $cplex.= "
503     ". sprintf("%-15s","poe_$stallid:")." ".
504                 join(" + ", map {
505                         sprintf "%d %s", $_->{Flow}{'dst_price'}, $_->{Var};
506                 } @{ $stall_poe_limits{$stallid} }).
507                 " <= $lim";
508         }
509         $cplex.= "\n";
510 }
511
512 $cplex.= "
513 Bounds
514         ".(join "
515         ", map { "$_->{Var} >= 0" } @subflows)."
516
517 ";
518
519 $cplex.= "
520 Integer
521         ".(join "
522         ", map { $_->{Var} } @subflows)."
523
524 End
525 ";
526
527 if ($qa->{'debug'}) {
528 </%perl>
529 <pre>
530 <% $cplex |h %>
531 </pre>
532 <%perl>
533 }
534
535 {
536         my $input= pipethrough_prep();
537         print $input $cplex or die $!;
538         my $output= pipethrough_run_along($input, undef, 'glpsol',
539                 qw(glpsol --tmlim 5 --memlim 5 --intopt --cuts --bfs
540                           --cpxlp /dev/stdin -o /dev/stdout));
541         print "<pre>\n" if $qa->{'debug'};
542         my $found_section= 0;
543         my $glpsol_out= '';
544         my $continuation='';
545         while (<$output>) {
546                 $glpsol_out.= $_;
547                 print encode_entities($_) if $qa->{'debug'};
548                 if (m/^\s*No\.\s+(Row|Column) name\s+(?:St\s+)?Activity\s/) {
549                         die "$_ $found_section ?" if $found_section>0;
550                         $found_section= 1;
551                         next;
552                 }
553                 if (m/^Objective:\s+totalprofit = (\d+(?:\.\d*)?) /) {
554                         $expected_total_profit= $1;
555                 }
556                 next unless $found_section==1;
557                 if (!length $continuation) {
558                         next if !$continuation &&  m/^[- ]+$/;
559                         if (!/\S/) {
560                                 $found_section= 0;
561                                 next;
562                         }
563                         if (m/^ \s* \d+ \s+ \w+ $/x) {
564                                 $continuation= $&;
565                                 next;
566                         }
567                 }
568                 $_= $continuation.$_;
569                 $continuation= '';
570                 my ($varname, $qty) = m/^
571                         \s* \d+ \s+
572                         (\w+) \s+ (?: [A-Z*]+ \s+ )?
573                         ([+-e0-9.]+) \s
574                         /x or die "$cplex \n==\n $glpsol_out $_ ?";
575                 if ($varname =~ m/^f(\d+)s(\d+)_/) {
576                         my ($ix,$orgix) = ($1,$2);
577                         my $flow= $flows[$ix] or die;
578                         my @relsubflow= grep { $_->{Org} == $orgix }
579                                 @{ $flow->{Subflows} };
580                         die "$ix $orgix @relsubflow" unless @relsubflow == 1;
581                         my $sf= $relsubflow[0];
582                         $sf->{OptQty}= $qty;
583                         $sf->{OptProfit}= $qty * $flow->{'unitprofit'};
584                         $sf->{OptCapital}= $qty * $flow->{'org_price'};
585                 } elsif ($varname =~ m/^(mass|volume)_(\d+)$/) {
586                         my ($mv,$ix) = ($1,$2);
587                         $sail_total[$ix]{$mv}= $qty;
588                 }
589         }
590         print "</pre>\n" if $qa->{'debug'};
591         my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n ";
592         pipethrough_run_finish($output,$prerr);
593         map { defined $_->{OptQty} or die "$prerr $_->{Flow}{Ix}" } @subflows;
594         defined $expected_total_profit or die "$prerr ?";
595 };
596
597 $addcols->({ DoReverse => 1, TotalSubflows => 1, Special => sub {
598         my ($flow,$col,$v,$spec) = @_;
599         if ($flow->{ExpectedUnitProfit} < 0) {
600                 $spec->{Span}= 3;
601                 $spec->{String}= '(Small margin)';
602                 $spec->{Align}= 'align=center';
603         }
604 } }, qw(
605                 OptQty
606         ));
607 $addcols->({ Total => 0, DoReverse => 1, TotalSubflows => 1 }, qw(
608                 OptCapital OptProfit
609         ));
610
611 </%perl>
612
613 % } # ========== OPTIMISATION ==========
614
615 % if (!printable($m)) {
616 <h2>Contents</h2>
617 <ul>
618 % if ($optimise) {
619  <li><a href="#plan">Voyage trading plan</a>
620   <ul>
621    <li><a href="#summary">Summary statistics</a>
622    <li>Printable:
623          <input type=submit name=printable_pdf value="PDF">
624          <input type=submit name=printable_html value="HTML">
625          <input type=submit name=printable_ps value="PostScript">
626          <input type=submit name=printable_pdf2 value="PDF 2-up">
627          <input type=submit name=printable_ps2 value="PostScript 2-up">
628   </ul>
629 % }
630  <li><a href="#dataage">Data age summary</a>
631  <li><a href="#trades">Relevant trades</a>
632 </ul>
633 % } else {
634 %       my @tl= gmtime $now or die $!;
635 <p>
636 Generated by YARRG at <strong><%
637         sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC",
638                 $tl[5]+1900, @tl[4,3,2,1,0]
639                         |h %></strong>.
640 % }
641
642 % if ($optimise) { # ========== TRADING PLAN ==========
643 %
644 % my $iquery= $dbh->prepare('SELECT islandname FROM islands
645 %                               WHERE islandid = ?');
646 % my %da_ages;
647 % my $total_total= 0;
648 % my $total_dist= 0;
649 %
650 <h2><a name="plan">Voyage trading plan</a></h2>
651
652 <table class="data" rules=groups <% printable($m) ? 'width=100%' : '' %> >
653 % my $tbody= sub {
654 %       if (!printable($m)) { return '<tbody>'; }
655 %#  return "<tr><td colspan=7><hr>";
656 %       my ($c)= qw(40 00)[$_[0]];
657 %       return "<tr><td bgcolor=\"#${c}${c}${c}\" height=1 colspan=7>";
658 % };
659 %
660 % foreach my $i (0..$#islandids) {
661 <% $tbody->(1) %>
662 <tr><td colspan=4>
663 %       $iquery->execute($islandids[$i]);
664 %       my ($islandname) = $iquery->fetchrow_array();
665 %       if (!$i) {
666 <strong>Start at <% $islandname |h %></strong>
667 %       } else {
668 %               my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
669 %               $total_dist += $this_dist;
670 <%perl>
671                 my $total_value= 0;
672                 foreach my $sf (@subflows) {
673                         next unless $sf->{Org} < $i && $sf->{Dst} >= $i;
674                         $total_value +=
675                                 $sf->{OptQty} * $sf->{Flow}{'dst_price'};
676                 }
677 </%perl>
678 <strong>Sail to <% $islandname |h %></strong>
679 - <% $this_dist |h %> leagues,
680  <% $total_value %>poe at risk
681  </td>
682 %       }
683 <%perl>
684      my $age_reported= 0;
685      my %flowlists;
686      #print "<tr><td colspan=6>" if $qa->{'debug'};
687      foreach my $od (qw(org dst)) {
688         #print " [[ i $i od $od " if $qa->{'debug'};
689         foreach my $sf (@subflows) {
690                 my $f= $sf->{Flow};
691                 next unless $sf->{ucfirst $od} == $i;
692                 #print " FLOW $f->{Ix} SUB #$sf->{Org}..$sf->{Dst} "
693                 #       if $qa->{'debug'};
694                 next unless $sf->{OptQty};
695                 my $arbitrage= $f->{'org_id'} == $f->{'dst_id'};
696                 die if $arbitrage and $sf->{Org} != $sf->{Dst};
697                 my $price= $f->{"${od}_price"};
698                 my $stallname= $f->{"${od}_stallname"};
699                 my $todo= \$flowlists{$od}{
700                                 $f->{'commodname'},
701                                 (sprintf "%07d", ($od eq 'dst' ?
702                                                 9999999-$price : $price)),
703                                 $stallname
704                         };
705                 $$todo= {
706                         Qty => 0,
707                         orgArbitrage => 0,
708                         dstArbitrage => 0,
709                 } unless $$todo;
710                 $$todo->{'commodname'}= $f->{'commodname'};
711                 $$todo->{'stallname'}= $stallname;
712                 $$todo->{Price}= $price;
713                 $$todo->{Timestamp}= $f->{"${od}_timestamp"};
714                 $$todo->{Qty} += $sf->{OptQty};
715                 $$todo->{Total}= $$todo->{Price} * $$todo->{Qty};
716                 $$todo->{Stalls}= $f->{"${od}Stalls"};
717                 $$todo->{"${od}Arbitrage"}= 1 if $arbitrage;
718         }
719         #print "]] " if $qa->{'debug'};
720      }
721      #print "</tr>" if $qa->{'debug'};
722
723      my ($total, $total_to_show);
724      my $dline= 0;
725      my $show_total= sub {
726         my ($totaldesc, $sign) = @_;
727         if (defined $total) {
728                 die if defined $total_to_show;
729                 $total_total += $sign * $total;
730                 $total_to_show= [ $totaldesc, $total ];
731                 $total= undef;
732         }
733         $dline= 0;
734      };
735      my $show_total_now= sub {
736         my ($xinfo) = @_;
737         return unless defined $total_to_show;
738         my ($totaldesc,$totalwas) = @$total_to_show;
739 </%perl>
740 <tr>
741 <td colspan=1>
742 <td colspan=2><% $xinfo %>
743 <td colspan=2 align=right><% $totaldesc %>
744 <td align=right><% $totalwas |h %> total
745 <%perl>
746         $total_to_show= undef;
747      };
748 </%perl>
749 %    my $show_flows= sub {
750 %       my ($od,$arbitrage,$collectdeliver) = @_;
751 %       my $todo= $flowlists{$od};
752 %       return unless $todo;
753 %       foreach my $tkey (sort keys %$todo) {
754 %               my $t= $todo->{$tkey};
755 %               next if $t->{"${od}Arbitrage"} != $arbitrage;
756 %               $show_total_now->('');
757 %               if (!$age_reported++) {
758 %                       my $age= $now - $t->{Timestamp};
759 %                       my $cellid= "da_${i}";
760 %                       $da_ages{$cellid}= $age;
761 <td colspan=2>\
762 (Data age: <span id="<% $cellid %>"><% prettyprint_age($age) %></span>)
763 %               } elsif (!defined $total) {
764 %                       $total= 0;
765 <% $tbody->(0) %>
766 %               }
767 %               $total += $t->{Total};
768 %               my $span= 0 + keys %{ $t->{Stalls} };
769 %               my $td= "td rowspan=$span";
770 % tr_datarow($m,$dline);
771 <<% $td %>><% $collectdeliver %>
772 <<% $td %>><% $t->{'commodname'} |h %>
773 %
774 %               my @stalls= sort keys %{ $t->{Stalls} };
775 %               my $pstall= sub {
776 %                       my $name= $stalls[$_[0]];
777 <td><% $name |h %>
778 %               };
779 %
780 %               $pstall->(0);
781 <<% $td %> align=right><% $t->{Price} |h %> poe ea.
782 <<% $td %> align=right><% $t->{Qty} |h %> unit(s)
783 <<% $td %> align=right><% $t->{Total} |h %> total
784 %
785 %               foreach my $stallix (1..$#stalls) {
786 % tr_datarow($m,$dline);
787 %                       $pstall->($stallix);
788 %               }
789 %
790 %               $dline ^= 1;
791 %       }
792 %    };
793 <%perl>
794
795      $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
796      $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
797      $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1);
798      $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1);
799      my $totals= '';
800      if ($i < $#islandids) {
801         $totals .=      "In hold $sail_total[$i]{mass}kg,".
802                         " $sail_total[$i]{volume} l";
803         my $delim= '; spare ';
804         my $domv= sub {
805                 my ($max, $got, $units) = @_;
806                 return unless defined $max;
807                 $totals .= $delim;
808                 $totals .= sprintf "%g %s", ($max-$got), $units;
809                 $delim= ', ';
810         };
811         $domv->($routeparams->{MaxMass},   $sail_total[$i]{mass},   'kg');
812         $domv->($routeparams->{MaxVolume}, $sail_total[$i]{volume}, 'l');
813         $totals .= ".\n";
814      }
815      $show_total_now->($totals);
816 }
817 </%perl><a name="summary"></a>
818 <% $tbody->(1) %><tr>
819 <td colspan=2>Total distance: <% $total_dist %> leagues.
820 <td colspan=3 align=right>Overall net cash flow
821 <td align=right><strong><%
822   $total_total < 0 ? -$total_total." loss" : $total_total." gain"
823  %></strong>
824 </table>
825 <& query_age:dataages, id2age => \%da_ages &>
826 Expected average profit:
827  approx. <strong><% sprintf "%d", $expected_total_profit %></strong> poe
828  (considering expected losses, but ignoring rum consumed)
829 %
830 % } # ========== TRADING PLAN ==========
831
832 % if (!printable($m)) {
833 <h2><a name="dataage">Data age summary</a></h2>
834 <%perl>
835         my $sth_i= $dbh->prepare(<<END);
836                 SELECT archipelago, islandid, islandname, timestamp
837                         FROM uploads NATURAL JOIN islands
838                         WHERE islandid = ?
839 END
840         my $sth_a= $dbh->prepare(<<END);
841                 SELECT archipelago, islandid, islandname, timestamp
842                         FROM uploads NATURAL JOIN islands
843                         WHERE archipelago = ?
844                         ORDER BY islandname
845 END
846         my $ix=$#islandids;
847         my $sth_current;
848         my %idone;
849         my $fetchrow= sub {
850                 for (;;) {
851                         if ($sth_current) {
852                                 my $row= $sth_current->fetchrow_hashref();
853                                 if ($row) {
854                                         next if $idone{$row->{'islandid'}}++;
855                                         return $row;
856                                 }
857                         }
858                         return undef if $ix < 0;
859                         my $iid= $islandids[$ix];
860                         if (defined $iid) {
861                                 $sth_i->execute($iid);
862                                 $sth_current= $sth_i;
863                         } else {
864                                 my $arch= $archipelagoes[$ix];
865                                 die unless defined $arch && length $arch;
866                                 $sth_a->execute($arch);
867                                 $sth_current= $sth_a;
868                         }
869                         $ix--;
870                 }
871         };
872 </%perl>
873 <&| query_age:agestable, now => $now, fetchrow => $fetchrow &>
874 Islands shown in reverse order of visits.<br>
875 </&>
876 % }
877
878 % if (!printable($m)) {
879 %   my %ts_sortkeys;
880 %   {
881 %       my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
882 %       my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
883 <h2><a name="trades">Relevant trades</a></h2>
884 <table class="data" id="trades" rules=groups>
885 <colgroup span=1>
886 <colgroup span=2>
887 <% $qa->{ShowStalls} ? '<colgroup span=2>' : '' %>
888 <colgroup span=1>
889 <colgroup span=2>
890 <colgroup span=2>
891 <colgroup span=2>
892 <colgroup span=3>
893 <colgroup span=3>
894 %       if ($optimise) {
895 <colgroup span=3>
896 %       }
897 <tr>
898 <th>
899 <th<% $cdspan %>>Collect
900 <th<% $cdspan %>>Deliver
901 <th>
902 <th colspan=2>Collect
903 <th colspan=2>Deliver
904 <th colspan=2>Profit
905 <th colspan=3>Max
906 <th colspan=1>
907 <th colspan=2>Max
908 %       if ($optimise) {
909 <th colspan=3>Planned
910 %       }
911
912 <tr>
913 <th>
914 <th>Island <% $cdstall %>
915 <th>Island <% $cdstall %>
916 <th>Commodity
917 <th>Price
918 <th>Qty
919 <th>Price
920 <th>Qty
921 <th>Margin
922 <th>Unit
923 <th>Qty
924 <th>Capital
925 <th>Profit
926 <th>Dist
927 <th>Mass
928 <th>Vol
929 %       if ($optimise) {
930 <th>Qty
931 <th>Capital
932 <th>Profit
933 %       }
934 %   }
935
936 <tr id="trades_sort">
937 %   foreach my $col (@cols) {
938 <th>
939 %   }
940
941 %   foreach my $flowix (0..$#flows) {
942 %       my $flow= $flows[$flowix];
943 %       my $rowid= "id_row_$flow->{UidShort}";
944 <tr id="<% $rowid %>" class="datarow<% $flowix & 1 %>">
945 <td><input type=hidden   name=R<% $flow->{UidShort} %> value="">
946     <input type=checkbox name=T<% $flow->{UidShort} %> value=""
947        <% $flow->{Suppress} ? '' : 'checked' %> >
948 %       my $ci= 1;
949 %       while ($ci < @cols) {
950 %               my $col= $cols[$ci];
951 %               my $spec= {
952 %                       Span => 1,
953 %                       Align => ($col->{Text} ? '' : 'align=right')
954 %               };
955 %               my $cn= $col->{Name};
956 %               my $v;
957 %               if (!$col->{TotalSubflows}) {
958 %                       $v= $flow->{$cn};
959 %               } else {
960 %                       $v= 0;
961 %                       $v += $_->{$cn} foreach @{ $flow->{Subflows} };
962 %               }
963 %               if ($col->{Special}) { $col->{Special}($flow,$col,$v,$spec); }
964 %               $col->{Total} += $v
965 %                       if defined $col->{Total} and not $flow->{Suppress};
966 %               $v='' if !$col->{Text} && !$v;
967 %               my $sortkey= $col->{SortColKey} ?
968 %                       $flow->{$col->{SortColKey}} : $v;
969 %               $ts_sortkeys{$ci}{$rowid}= $sortkey;
970 <td <% $spec->{Span} ? "colspan=$spec->{Span}" : ''
971  %> <% $spec->{Align}
972  %>><% exists $spec->{String} ? $spec->{String} : $v |h %>
973 %               $ci += $spec->{Span};
974 %       }
975 %   }
976 <tr id="trades_total">
977 <th>
978 <th colspan=2>Total
979 %   foreach my $ci (3..$#cols) {
980 %       my $col= $cols[$ci];
981 <td align=right>
982 %       if (defined $col->{Total}) {
983 <% $col->{Total} |h %>
984 %       }
985 %   }
986 </table>
987
988 <&| tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
989         throw => 'trades_sort', tbrow => 'trades_total' &>
990   ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;
991 </&tabsort>
992 <p>
993 <input type=submit name=update value="Update">
994
995 % } # !printable
996
997 <%init>
998 use CommodsWeb;
999 use Commods;
1000 </%init>