chiark / gitweb /
Merge branch 'stable-3.x'
[ypp-sc-tools.main.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 <%perl>
265
266 my @sail_total;
267
268 if (!@flows) {
269         print 'No profitable trading opportunities were found.';
270         return;
271 }
272
273 foreach my $f (@flows) {
274
275         $f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
276                 ? $f->{'org_qty_agg'} : $f->{'dst_qty_agg'};
277         $f->{MaxProfit}= $f->{MaxQty} * $f->{'unitprofit'};
278         $f->{MaxCapital}= $f->{MaxQty} * $f->{'org_price'};
279
280         $f->{MaxMassSortKey}= $f->{MaxQty} * $f->{'unitmass'};
281         $f->{MaxVolumeSortKey}= $f->{MaxQty} * $f->{'unitvolume'};
282         foreach my $v (qw(Mass Volume)) {
283                 $f->{"Max$v"}= sprintf "%.1f", $f->{"Max${v}SortKey"} * 1e-6;
284         }
285
286         my $sfis= $ipair2subflowinfs{$f->{'org_id'},$f->{'dst_id'}};
287         foreach my $sfi (@$sfis) {
288                 my $subflow= {
289                         Flow => $f,
290                         Org => $sfi->[0],
291                         Dst => $sfi->[1],
292                         Var => sprintf "f%ss%s_c%d_p%d_%d_p%d_%d",
293                                 $f->{Ix}, $sfi->[0],
294                                 $f->{'commodid'},
295                                 $sfi->[0], $f->{'org_price'},
296                                 $sfi->[1], $f->{'dst_price'}
297                 };
298                 push @{ $f->{Subflows} }, $subflow;
299                 push @subflows, $subflow;
300         }
301
302         $f->{MarginSortKey}= sprintf "%d",
303                 $f->{'dst_price'} * 10000 / $f->{'org_price'};
304         $f->{Margin}= sprintf "%3.1f%%",
305                 $f->{'dst_price'} * 100.0 / $f->{'org_price'} - 100.0;
306
307         $f->{ExpectedUnitProfit}=
308                 $f->{'dst_price'} * (1.0 - $loss_per_league) ** $f->{'dist'}
309                 - $f->{'org_price'};
310
311         $dists{'org_id'}{'dst_id'}= $f->{'dist'};
312
313         my @uid= $f->{commodid};
314         foreach my $od (qw(org dst)) {
315                 push @uid,
316                         $f->{"${od}_id"},
317                         $f->{"${od}_price"};
318                 push @uid,
319                         $f->{"${od}_stallid"}
320                                 if $qa->{ShowStalls};
321         }
322         $f->{UidLong}= join '_', @uid;
323
324         my $base= 31;
325         my $cmpu= '';
326         map {
327                 my $uue= $_;
328                 my $first= $base;
329                 do {
330                         my $this= $uue % $base;
331 #print STDERR "uue=$uue this=$this ";
332                         $uue -= $this;
333                         $uue /= $base;
334                         $this += $first;
335                         $first= 0;
336                         $cmpu .= chr($this + ($this < 26 ? ord('a') :
337                                               $this < 52 ? ord('A')-26
338                                                          : ord('0')-52));
339 #print STDERR " uue=$uue this=$this cmpu=$cmpu\n";
340                         die "$cmpu $uue ?" if length $cmpu > 20;
341                 } while ($uue);
342                 $cmpu;
343         } @uid;
344         $f->{UidShort}= $cmpu;
345
346         if ($qa->{'debug'}) {
347                 my @outuid;
348                 $_= $f->{UidShort};
349                 my $mul;
350                 while (m/./) {
351                         my $v= m/^[a-z]/ ? ord($&)-ord('a') :
352                                m/^[A-Z]/ ? ord($&)-ord('A')+26 :
353                                m/^[0-9]/ ? ord($&)-ord('0')+52 :
354                                die "$_ ?";
355                         if ($v >= $base) {
356                                 push @outuid, 0;
357                                 $v -= $base;
358                                 $mul= 1;
359 #print STDERR "(next)\n";
360                         }
361                         die "$f->{UidShort} $_ ?" unless defined $mul;
362                         $outuid[$#outuid] += $v * $mul;
363
364 #print STDERR "$f->{UidShort}  $_  $&  v=$v  mul=$mul  ord()=".ord($&).
365 #                       "[vs.".ord('a').",".ord('A').",".ord('0')."]".
366 #                       "  outuid=@outuid\n";
367
368                         $mul *= $base;
369                         s/^.//;
370                 }
371                 my $recons_long= join '_', @outuid;
372                 $f->{UidLong} eq $recons_long or
373                         die "$f->{UidLong} = $f->{UidShort} = $recons_long ?";
374         }
375
376         if (defined $qa->{"R$f->{UidShort}"} &&
377             !defined $qa->{"T$f->{UidShort}"}) {
378                 $f->{Suppress}= 1;
379         }
380
381 }
382 </%perl>
383
384 % my $optimise= $specific;
385 % if (!$optimise) {
386
387 <p>
388 % if (!$specific) {
389 Route contains archipelago(es), not just specific islands.
390 % }
391 Therefore, optimal voyage trade plan not calculated.
392
393 % } else { # ========== OPTMISATION ==========
394 <%perl>
395
396 my $cplex= "
397 Maximize
398
399   totalprofit:
400 ";
401
402 foreach my $sf (@subflows) {
403         my $eup= $sf->{Flow}{ExpectedUnitProfit};
404         $eup *= (1.0-$loss_per_delay_slot) ** $sf->{Org};
405         $cplex .= sprintf "
406                 %+.20f %s", $eup, $sf->{Var};
407 }
408 $cplex .= "
409
410 Subject To
411 ";
412
413 my %avail_lims;
414 foreach my $flow (@flows) {
415         if ($flow->{Suppress}) {
416                 foreach my $sf (@{ $flow->{Subflows} }) {
417                         $cplex .= "
418    $sf->{Var} = 0";
419                 }
420                 next;
421         }
422         foreach my $od (qw(org dst)) {
423                 my $limname= join '_', (
424                         $od,
425                         'i'.$flow->{"${od}_id"},
426                         'c'.$flow->{'commodid'},
427                         $flow->{"${od}_price"},
428                         $flow->{"${od}_stallid"},
429                 );
430
431                 push @{ $avail_lims{$limname}{SubflowVars} },
432                         map { $_->{Var} } @{ $flow->{Subflows} };
433                 $avail_lims{$limname}{Qty}= $flow->{"${od}_qty_agg"};
434         }
435 }
436 foreach my $limname (sort keys %avail_lims) {
437         my $c= $avail_lims{$limname};
438         $cplex .=
439                 sprintf("    %-30s","$limname:")." ".
440                         join("+", @{ $c->{SubflowVars} }).
441                         " <= ".$c->{Qty}."\n";
442 }
443
444 foreach my $ci (0..($#islandids-1)) {
445         my @rel_subflows;
446
447         foreach my $f (@flows) {
448                 next if $f->{Suppress};
449                 my @relsubflow= grep {
450                         $_->{Org} <= $ci &&
451                         $_->{Dst} > $ci;
452                 } @{ $f->{Subflows} };
453                 next unless @relsubflow;
454                 die unless @relsubflow == 1;
455                 push @rel_subflows, @relsubflow;
456 #print " RELEVANT $ci $relsubflow[0]->{Var} ";
457         }
458 #print " RELEVANT $ci COUNT ".scalar(@rel_subflows)."  ";
459         if (!@rel_subflows) {
460                 foreach my $mv (qw(mass volume)) {
461                         $sail_total[$ci]{$mv}= 0;
462                 }
463                 next;
464         }
465
466         my $applylimit= sub {
467                 my ($mv, $f2val) = @_;
468                 my $max= $routeparams->{"Max".ucfirst $mv};
469                 $max= 1e9 unless defined $max;
470 #print " DEFINED MAX $mv $max ";
471                 $cplex .= "
472    ". sprintf("%-10s","${mv}_$ci:")." ".
473                 join(" + ", map {
474 #print " PART MAX $_->{Var} $_->{Flow}{Ix} ";
475                         $f2val->($_->{Flow}) .' '. $_->{Var};
476                 } @rel_subflows).
477                 " <= $max";
478         };
479
480         $applylimit->('mass',    sub { $_[0]{'unitmass'}  *1e-3 });
481         $applylimit->('volume',  sub { $_[0]{'unitvolume'}*1e-3 });
482         $applylimit->('capital', sub { $_[0]{'org_price'}       });
483         $cplex.= "\n";
484 }
485
486 $cplex.= "
487 Bounds
488         ".(join "
489         ", map { "$_->{Var} >= 0" } @subflows)."
490
491 ";
492
493 $cplex.= "
494 Integer
495         ".(join "
496         ", map { $_->{Var} } @subflows)."
497
498 End
499 ";
500
501 if ($qa->{'debug'}) {
502 </%perl>
503 <pre>
504 <% $cplex |h %>
505 </pre>
506 <%perl>
507 }
508
509 {
510         my $input= pipethrough_prep();
511         print $input $cplex or die $!;
512         my $output= pipethrough_run_along($input, undef, 'glpsol',
513                 qw(glpsol --tmlim 2 --memlim 5 --intopt --cuts --bfs
514                           --cpxlp /dev/stdin -o /dev/stdout));
515         print "<pre>\n" if $qa->{'debug'};
516         my $found_section= 0;
517         my $glpsol_out= '';
518         my $continuation='';
519         while (<$output>) {
520                 $glpsol_out.= $_;
521                 print encode_entities($_) if $qa->{'debug'};
522                 if (m/^\s*No\.\s+(Row|Column) name\s+(?:St\s+)?Activity\s/) {
523                         die "$_ $found_section ?" if $found_section>0;
524                         $found_section= 1;
525                         next;
526                 }
527                 if (m/^Objective:\s+totalprofit = (\d+\.\d*) /) {
528                         $expected_total_profit= $1;
529                 }
530                 next unless $found_section==1;
531                 if (!length $continuation) {
532                         next if !$continuation &&  m/^[- ]+$/;
533                         if (!/\S/) {
534                                 $found_section= 0;
535                                 next;
536                         }
537                         if (m/^ \s* \d+ \s+ \w+ $/x) {
538                                 $continuation= $&;
539                                 next;
540                         }
541                 }
542                 $_= $continuation.$_;
543                 $continuation= '';
544                 my ($varname, $qty) = m/^
545                         \s* \d+ \s+
546                         (\w+) \s+ (?: [A-Z*]+ \s+ )?
547                         ([0-9.]+) \s
548                         /x or die "$_ ?";
549                 if ($varname =~ m/^f(\d+)s(\d+)_/) {
550                         my ($ix,$orgix) = ($1,$2);
551                         my $flow= $flows[$ix] or die;
552                         my @relsubflow= grep { $_->{Org} == $orgix }
553                                 @{ $flow->{Subflows} };
554                         die "$ix $orgix @relsubflow" unless @relsubflow == 1;
555                         my $sf= $relsubflow[0];
556                         $sf->{OptQty}= $qty;
557                         $sf->{OptProfit}= $qty * $flow->{'unitprofit'};
558                         $sf->{OptCapital}= $qty * $flow->{'org_price'};
559                 } elsif ($varname =~ m/^(mass|volume)_(\d+)$/) {
560                         my ($mv,$ix) = ($1,$2);
561                         $sail_total[$ix]{$mv}= $qty;
562                 }
563         }
564         print "</pre>\n" if $qa->{'debug'};
565         my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n ";
566         pipethrough_run_finish($output,$prerr);
567         map { defined $_->{OptQty} or die "$prerr $_->{Flow}{Ix}" } @subflows;
568 };
569
570 $addcols->({ DoReverse => 1, TotalSubflows => 1, Special => sub {
571         my ($flow,$col,$v,$spec) = @_;
572         if ($flow->{ExpectedUnitProfit} < 0) {
573                 $spec->{Span}= 3;
574                 $spec->{String}= '(Small margin)';
575                 $spec->{Align}= 'align=center';
576         }
577 } }, qw(
578                 OptQty
579         ));
580 $addcols->({ Total => 0, DoReverse => 1, TotalSubflows => 1 }, qw(
581                 OptCapital OptProfit
582         ));
583
584 </%perl>
585
586 % } # ========== OPTIMISATION ==========
587
588 % if (!printable($m)) {
589 <h2>Contents</h2>
590 <ul>
591 % if ($optimise) {
592  <li><a href="#plan">Voyage trading plan</a>
593   <ul>
594    <li><a href="#summary">Summary statistics</a>
595    <li>Printable:
596          <input type=submit name=printable_pdf value="PDF">
597          <input type=submit name=printable_html value="HTML">
598          <input type=submit name=printable_ps value="PostScript">
599          <input type=submit name=printable_pdf2 value="PDF 2-up">
600          <input type=submit name=printable_ps2 value="PostScript 2-up">
601   </ul>
602 % }
603  <li><a href="#dataage">Data age summary</a>
604  <li><a href="#trades">Relevant trades</a>
605 </ul>
606 % } else {
607 %       my @tl= gmtime $now or die $!;
608 <p>
609 Generated by YARRG at <strong><%
610         sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC",
611                 $tl[5]+1900, @tl[4,3,2,1,0]
612                         |h %></strong>.
613 % }
614
615 % if ($optimise) { # ========== TRADING PLAN ==========
616 %
617 % my $iquery= $dbh->prepare('SELECT islandname FROM islands
618 %                               WHERE islandid = ?');
619 % my %da_ages;
620 % my $total_total= 0;
621 % my $total_dist= 0;
622 %
623 <h2><a name="plan">Voyage trading plan</a></h2>
624
625 <table rules=groups <% printable($m) ? 'width=100%' : '' %> >
626 % my $tbody= sub {
627 %       if (!printable($m)) { return '<tbody>'; }
628 %#  return "<tr><td colspan=7><hr>";
629 %       my ($c)= qw(40 00)[$_[0]];
630 %       return "<tr><td bgcolor=\"#${c}${c}${c}\" height=1 colspan=7>";
631 % };
632 %
633 % foreach my $i (0..$#islandids) {
634 <% $tbody->(1) %>
635 <tr><td colspan=4>
636 %       $iquery->execute($islandids[$i]);
637 %       my ($islandname) = $iquery->fetchrow_array();
638 %       if (!$i) {
639 <strong>Start at <% $islandname |h %></strong>
640 %       } else {
641 %               my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
642 %               $total_dist += $this_dist;
643 <%perl>
644                 my $total_value= 0;
645                 foreach my $sf (@subflows) {
646                         next unless $sf->{Org} < $i && $sf->{Dst} >= $i;
647                         $total_value +=
648                                 $sf->{OptQty} * $sf->{Flow}{'dst_price'};
649                 }
650 </%perl>
651 <strong>Sail to <% $islandname |h %></strong>
652 - <% $this_dist |h %> leagues,
653  <% $total_value %>poe at risk
654  </td>
655 %       }
656 <%perl>
657      my $age_reported= 0;
658      my %flowlists;
659      #print "<tr><td colspan=6>" if $qa->{'debug'};
660      foreach my $od (qw(org dst)) {
661         #print " [[ i $i od $od " if $qa->{'debug'};
662         foreach my $sf (@subflows) {
663                 my $f= $sf->{Flow};
664                 next if $f->{Suppress};
665                 next unless $sf->{ucfirst $od} == $i;
666                 #print " FLOW $f->{Ix} SUB #$sf->{Org}..$sf->{Dst} "
667                 #       if $qa->{'debug'};
668                 next unless $sf->{OptQty};
669                 my $arbitrage= $f->{'org_id'} == $f->{'dst_id'};
670                 die if $arbitrage and $sf->{Org} != $sf->{Dst};
671                 my $price= $f->{"${od}_price"};
672                 my $stallname= $f->{"${od}_stallname"};
673                 my $todo= \$flowlists{$od}{
674                                 $f->{'commodname'},
675                                 (sprintf "%07d", ($od eq 'dst' ?
676                                                 9999999-$price : $price)),
677                                 $stallname
678                         };
679                 $$todo= {
680                         Qty => 0,
681                         orgArbitrage => 0,
682                         dstArbitrage => 0,
683                 } unless $$todo;
684                 $$todo->{'commodname'}= $f->{'commodname'};
685                 $$todo->{'stallname'}= $stallname;
686                 $$todo->{Price}= $price;
687                 $$todo->{Timestamp}= $f->{"${od}_timestamp"};
688                 $$todo->{Qty} += $sf->{OptQty};
689                 $$todo->{Total}= $$todo->{Price} * $$todo->{Qty};
690                 $$todo->{Stalls}= $f->{"${od}Stalls"};
691                 $$todo->{"${od}Arbitrage"}= 1 if $arbitrage;
692         }
693         #print "]] " if $qa->{'debug'};
694      }
695      #print "</tr>" if $qa->{'debug'};
696
697      my ($total, $total_to_show);
698      my $dline= 0;
699      my $show_total= sub {
700         my ($totaldesc, $sign) = @_;
701         if (defined $total) {
702                 die if defined $total_to_show;
703                 $total_total += $sign * $total;
704                 $total_to_show= [ $totaldesc, $total ];
705                 $total= undef;
706         }
707         $dline= 0;
708      };
709      my $show_total_now= sub {
710         my ($xinfo) = @_;
711         return unless defined $total_to_show;
712         my ($totaldesc,$totalwas) = @$total_to_show;
713 </%perl>
714 <tr>
715 <td colspan=1>
716 <td colspan=2><% $xinfo %>
717 <td colspan=2 align=right><% $totaldesc %>
718 <td align=right><% $totalwas |h %> total
719 <%perl>
720         $total_to_show= undef;
721      };
722 </%perl>
723 %    my $show_flows= sub {
724 %       my ($od,$arbitrage,$collectdeliver) = @_;
725 %       my $todo= $flowlists{$od};
726 %       return unless $todo;
727 %       foreach my $tkey (sort keys %$todo) {
728 %               my $t= $todo->{$tkey};
729 %               next if $t->{"${od}Arbitrage"} != $arbitrage;
730 %               $show_total_now->('');
731 %               if (!$age_reported++) {
732 %                       my $age= $now - $t->{Timestamp};
733 %                       my $cellid= "da_${i}";
734 %                       $da_ages{$cellid}= $age;
735 <td colspan=2>\
736 (Data age: <span id="<% $cellid %>"><% prettyprint_age($age) %></span>)
737 %               } elsif (!defined $total) {
738 %                       $total= 0;
739 <% $tbody->(0) %>
740 %               }
741 %               $total += $t->{Total};
742 %               my $span= 0 + keys %{ $t->{Stalls} };
743 %               my $td= "td rowspan=$span";
744 % tr_datarow($m,$dline);
745 <<% $td %>><% $collectdeliver %>
746 <<% $td %>><% $t->{'commodname'} |h %>
747 %
748 %               my @stalls= sort keys %{ $t->{Stalls} };
749 %               my $pstall= sub {
750 %                       my $name= $stalls[$_[0]];
751 <td><% $name |h %>
752 %               };
753 %
754 %               $pstall->(0);
755 <<% $td %> align=right><% $t->{Price} |h %> poe ea.
756 <<% $td %> align=right><% $t->{Qty} |h %> unit(s)
757 <<% $td %> align=right><% $t->{Total} |h %> total
758 %
759 %               foreach my $stallix (1..$#stalls) {
760 % tr_datarow($m,$dline);
761 %                       $pstall->($stallix);
762 %               }
763 %
764 %               $dline ^= 1;
765 %       }
766 %    };
767 <%perl>
768
769      $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
770      $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
771      $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1);
772      $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1);
773      my $totals= '';
774      if ($i < $#islandids) {
775         $totals .=      "In hold $sail_total[$i]{mass}kg,".
776                         " $sail_total[$i]{volume} l";
777         my $delim= '; spare ';
778         my $domv= sub {
779                 my ($max, $got, $units) = @_;
780                 return unless defined $max;
781                 $totals .= $delim;
782                 $totals .= sprintf "%g %s", ($max-$got), $units;
783                 $delim= ', ';
784         };
785         $domv->($routeparams->{MaxMass},   $sail_total[$i]{mass},   'kg');
786         $domv->($routeparams->{MaxVolume}, $sail_total[$i]{volume}, 'l');
787         $totals .= ".\n";
788      }
789      $show_total_now->($totals);
790 }
791 </%perl><a name="summary"></a>
792 <% $tbody->(1) %><tr>
793 <td colspan=2>Total distance: <% $total_dist %> leagues.
794 <td colspan=3 align=right>Overall net cash flow
795 <td align=right><strong><%
796   $total_total < 0 ? -$total_total." loss" : $total_total." gain"
797  %></strong>
798 </table>
799 <& query_age:dataages, id2age => \%da_ages &>
800 Expected average profit:
801  approx. <strong><% sprintf "%d", $expected_total_profit %></strong> poe
802  (considering expected losses, but ignoring rum consumed)
803 %
804 % } # ========== TRADING PLAN ==========
805
806 % if (!printable($m)) {
807 <h2><a name="dataage">Data age summary</a></h2>
808 <%perl>
809         my $sth_i= $dbh->prepare(<<END);
810                 SELECT archipelago, islandid, islandname, timestamp
811                         FROM uploads NATURAL JOIN islands
812                         WHERE islandid = ?
813 END
814         my $sth_a= $dbh->prepare(<<END);
815                 SELECT archipelago, islandid, islandname, timestamp
816                         FROM uploads NATURAL JOIN islands
817                         WHERE archipelago = ?
818                         ORDER BY islandname
819 END
820         my $ix=$#islandids;
821         my $sth_current;
822         my %idone;
823         my $fetchrow= sub {
824                 for (;;) {
825                         if ($sth_current) {
826                                 my $row= $sth_current->fetchrow_hashref();
827                                 if ($row) {
828                                         next if $idone{$row->{'islandid'}}++;
829                                         return $row;
830                                 }
831                         }
832                         return undef if $ix < 0;
833                         my $iid= $islandids[$ix];
834                         if (defined $iid) {
835                                 $sth_i->execute($iid);
836                                 $sth_current= $sth_i;
837                         } else {
838                                 my $arch= $archipelagoes[$ix];
839                                 die unless defined $arch && length $arch;
840                                 $sth_a->execute($arch);
841                                 $sth_current= $sth_a;
842                         }
843                         $ix--;
844                 }
845         };
846 </%perl>
847 <&| query_age:agestable, now => $now, fetchrow => $fetchrow &>
848 Islands shown in reverse order of visits.<br>
849 </&>
850 % }
851
852 % if (!printable($m)) {
853 %   my %ts_sortkeys;
854 %   {
855 %       my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
856 %       my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
857 <h2><a name="trades">Relevant trades</a></h2>
858 <table id="trades" rules=groups>
859 <colgroup span=1>
860 <colgroup span=2>
861 <% $qa->{ShowStalls} ? '<colgroup span=2>' : '' %>
862 <colgroup span=1>
863 <colgroup span=2>
864 <colgroup span=2>
865 <colgroup span=2>
866 <colgroup span=3>
867 <colgroup span=3>
868 %       if ($optimise) {
869 <colgroup span=3>
870 %       }
871 <tr>
872 <th>
873 <th<% $cdspan %>>Collect
874 <th<% $cdspan %>>Deliver
875 <th>
876 <th colspan=2>Collect
877 <th colspan=2>Deliver
878 <th colspan=2>Profit
879 <th colspan=3>Max
880 <th colspan=1>
881 <th colspan=2>Max
882 %       if ($optimise) {
883 <th colspan=3>Planned
884 %       }
885
886 <tr>
887 <th>
888 <th>Island <% $cdstall %>
889 <th>Island <% $cdstall %>
890 <th>Commodity
891 <th>Price
892 <th>Qty
893 <th>Price
894 <th>Qty
895 <th>Margin
896 <th>Unit
897 <th>Qty
898 <th>Capital
899 <th>Profit
900 <th>Dist
901 <th>Mass
902 <th>Vol
903 %       if ($optimise) {
904 <th>Qty
905 <th>Capital
906 <th>Profit
907 %       }
908 %   }
909
910 <tr id="trades_sort">
911 %   foreach my $col (@cols) {
912 <th>
913 %   }
914
915 %   foreach my $flowix (0..$#flows) {
916 %       my $flow= $flows[$flowix];
917 %       my $rowid= "id_row_$flow->{UidShort}";
918 <tr id="<% $rowid %>" class="datarow<% $flowix & 1 %>">
919 <td><input type=hidden   name=R<% $flow->{UidShort} %> value="">
920     <input type=checkbox name=T<% $flow->{UidShort} %> value=""
921        <% $flow->{Suppress} ? '' : 'checked' %> >
922 %       my $ci= 1;
923 %       while ($ci < @cols) {
924 %               my $col= $cols[$ci];
925 %               my $spec= {
926 %                       Span => 1,
927 %                       Align => ($col->{Text} ? '' : 'align=right')
928 %               };
929 %               my $cn= $col->{Name};
930 %               my $v;
931 %               if (!$col->{TotalSubflows}) {
932 %                       $v= $flow->{$cn};
933 %               } else {
934 %                       $v= 0;
935 %                       $v += $_->{$cn} foreach @{ $flow->{Subflows} };
936 %               }
937 %               if ($col->{Special}) { $col->{Special}($flow,$col,$v,$spec); }
938 %               $col->{Total} += $v
939 %                       if defined $col->{Total} and not $flow->{Suppress};
940 %               $v='' if !$col->{Text} && !$v;
941 %               my $sortkey= $col->{SortColKey} ?
942 %                       $flow->{$col->{SortColKey}} : $v;
943 %               $ts_sortkeys{$ci}{$rowid}= $sortkey;
944 <td <% $spec->{Span} ? "colspan=$spec->{Span}" : ''
945  %> <% $spec->{Align}
946  %>><% exists $spec->{String} ? $spec->{String} : $v |h %>
947 %               $ci += $spec->{Span};
948 %       }
949 %   }
950 <tr id="trades_total">
951 <th>
952 <th colspan=2>Total
953 %   foreach my $ci (3..$#cols) {
954 %       my $col= $cols[$ci];
955 <td align=right>
956 %       if (defined $col->{Total}) {
957 <% $col->{Total} |h %>
958 %       }
959 %   }
960 </table>
961
962 <&| tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
963         throw => 'trades_sort', tbrow => 'trades_total' &>
964   ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;
965 </&tabsort>
966
967 <input type=submit name=update value="Update">
968
969 % } # !printable
970
971 <%init>
972 use CommodsWeb;
973 use Commods;
974 </%init>