chiark / gitweb /
1f8df2a5afdb6eafd5215a38f71cce74ad03dce5
[ypp-sc-tools.db-live.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   </ul>
600 % }
601  <li><a href="#dataage">Data age summary</a>
602  <li><a href="#trades">Relevant trades</a>
603 </ul>
604 % }
605
606 % if ($optimise) { # ========== TRADING PLAN ==========
607 %
608 % my $iquery= $dbh->prepare('SELECT islandname FROM islands
609 %                               WHERE islandid = ?');
610 % my %da_ages;
611 % my $total_total= 0;
612 % my $total_dist= 0;
613 %
614 <h2><a name="plan">Voyage trading plan</a></h2>
615
616 <table rules=groups>
617 % foreach my $i (0..$#islandids) {
618 <tbody>
619 <tr><td colspan=4>
620 %       $iquery->execute($islandids[$i]);
621 %       my ($islandname) = $iquery->fetchrow_array();
622 %       if (!$i) {
623 <strong>Start at <% $islandname |h %></strong>
624 %       } else {
625 %               my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
626 %               $total_dist += $this_dist;
627 <%perl>
628                 my $total_value= 0;
629                 foreach my $sf (@subflows) {
630                         next unless $sf->{Org} < $i && $sf->{Dst} >= $i;
631                         $total_value +=
632                                 $sf->{OptQty} * $sf->{Flow}{'dst_price'};
633                 }
634 </%perl>
635 <strong>Sail to <% $islandname |h %></strong>
636 - <% $this_dist |h %> leagues,
637  <% $total_value %>poe at risk
638  </td>
639 %       }
640 <%perl>
641      my $age_reported= 0;
642      my %flowlists;
643      #print "<tr><td colspan=6>" if $qa->{'debug'};
644      foreach my $od (qw(org dst)) {
645         #print " [[ i $i od $od " if $qa->{'debug'};
646         foreach my $sf (@subflows) {
647                 my $f= $sf->{Flow};
648                 next if $f->{Suppress};
649                 next unless $sf->{ucfirst $od} == $i;
650                 #print " FLOW $f->{Ix} SUB #$sf->{Org}..$sf->{Dst} "
651                 #       if $qa->{'debug'};
652                 next unless $sf->{OptQty};
653                 my $arbitrage= $f->{'org_id'} == $f->{'dst_id'};
654                 die if $arbitrage and $sf->{Org} != $sf->{Dst};
655                 my $price= $f->{"${od}_price"};
656                 my $stallname= $f->{"${od}_stallname"};
657                 my $todo= \$flowlists{$od}{
658                                 $f->{'commodname'},
659                                 (sprintf "%07d", ($od eq 'dst' ?
660                                                 9999999-$price : $price)),
661                                 $stallname
662                         };
663                 $$todo= {
664                         Qty => 0,
665                         orgArbitrage => 0,
666                         dstArbitrage => 0,
667                 } unless $$todo;
668                 $$todo->{'commodname'}= $f->{'commodname'};
669                 $$todo->{'stallname'}= $stallname;
670                 $$todo->{Price}= $price;
671                 $$todo->{Timestamp}= $f->{"${od}_timestamp"};
672                 $$todo->{Qty} += $sf->{OptQty};
673                 $$todo->{Total}= $$todo->{Price} * $$todo->{Qty};
674                 $$todo->{Stalls}= $f->{"${od}Stalls"};
675                 $$todo->{"${od}Arbitrage"}= 1 if $arbitrage;
676         }
677         #print "]] " if $qa->{'debug'};
678      }
679      #print "</tr>" if $qa->{'debug'};
680
681      my ($total, $total_to_show);
682      my $dline= 0;
683      my $show_total= sub {
684         my ($totaldesc, $sign) = @_;
685         if (defined $total) {
686                 die if defined $total_to_show;
687                 $total_total += $sign * $total;
688                 $total_to_show= [ $totaldesc, $total ];
689                 $total= undef;
690         }
691         $dline= 0;
692      };
693      my $show_total_now= sub {
694         my ($xinfo) = @_;
695         return unless defined $total_to_show;
696         my ($totaldesc,$totalwas) = @$total_to_show;
697 </%perl>
698 <tr>
699 <td colspan=1>
700 <td colspan=2><% $xinfo %>
701 <td colspan=2 align=right><% $totaldesc %>
702 <td align=right><% $totalwas |h %> total
703 <%perl>
704         $total_to_show= undef;
705      };
706 </%perl>
707 %    my $show_flows= sub {
708 %       my ($od,$arbitrage,$collectdeliver) = @_;
709 %       my $todo= $flowlists{$od};
710 %       return unless $todo;
711 %       foreach my $tkey (sort keys %$todo) {
712 %               my $t= $todo->{$tkey};
713 %               next if $t->{"${od}Arbitrage"} != $arbitrage;
714 %               $show_total_now->('');
715 %               if (!$age_reported++) {
716 %                       my $age= $now - $t->{Timestamp};
717 %                       my $cellid= "da_${i}";
718 %                       $da_ages{$cellid}= $age;
719 <td colspan=2>\
720 (Data age: <span id="<% $cellid %>"><% prettyprint_age($age) %></span>)
721 %               } elsif (!defined $total) {
722 %                       $total= 0;
723 <tbody>
724 %               }
725 %               $total += $t->{Total};
726 %               my $span= 0 + keys %{ $t->{Stalls} };
727 %               my $td= "td rowspan=$span";
728 <tr class="datarow<% $dline %>">
729 <<% $td %>><% $collectdeliver %>
730 <<% $td %>><% $t->{'commodname'} |h %>
731 %
732 %               my @stalls= sort keys %{ $t->{Stalls} };
733 %               my $pstall= sub {
734 %                       my $name= $stalls[$_[0]];
735 <td><% $name |h %>
736 %               };
737 %
738 %               $pstall->(0);
739 <<% $td %> align=right><% $t->{Price} |h %> poe ea.
740 <<% $td %> align=right><% $t->{Qty} |h %> unit(s)
741 <<% $td %> align=right><% $t->{Total} |h %> total
742 %
743 %               foreach my $stallix (1..$#stalls) {
744 <tr class="datarow<% $dline %>">
745 %                       $pstall->($stallix);
746 %               }
747 %
748 %               $dline ^= 1;
749 %       }
750 %    };
751 <%perl>
752
753      $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
754      $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
755      $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1);
756      $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1);
757      my $totals= '';
758      if ($i < $#islandids) {
759         $totals .=      "In hold $sail_total[$i]{mass}kg,".
760                         " $sail_total[$i]{volume} l";
761         my $delim= '; spare ';
762         my $domv= sub {
763                 my ($max, $got, $units) = @_;
764                 return unless defined $max;
765                 $totals .= $delim;
766                 $totals .= sprintf "%g %s", ($max-$got), $units;
767                 $delim= ', ';
768         };
769         $domv->($routeparams->{MaxMass},   $sail_total[$i]{mass},   'kg');
770         $domv->($routeparams->{MaxVolume}, $sail_total[$i]{volume}, 'l');
771         $totals .= ".\n";
772      }
773      $show_total_now->($totals);
774 }
775 </%perl><a name="summary"></a>
776 <tbody><tr>
777 <td colspan=2>Total distance: <% $total_dist %> leagues.
778 <td colspan=3 align=right>Overall net cash flow
779 <td align=right><strong><%
780   $total_total < 0 ? -$total_total." loss" : $total_total." gain"
781  %></strong>
782 </table>
783 <& query_age:dataages, id2age => \%da_ages &>
784 Expected average profit:
785  approx. <strong><% sprintf "%d", $expected_total_profit %></strong> poe
786  (considering expected losses, but ignoring rum consumed)
787 %
788 % } # ========== TRADING PLAN ==========
789
790 % if (!printable($m)) {
791 <h2><a name="dataage">Data age summary</a></h2>
792 <%perl>
793         my $sth_i= $dbh->prepare(<<END);
794                 SELECT archipelago, islandid, islandname, timestamp
795                         FROM uploads NATURAL JOIN islands
796                         WHERE islandid = ?
797 END
798         my $sth_a= $dbh->prepare(<<END);
799                 SELECT archipelago, islandid, islandname, timestamp
800                         FROM uploads NATURAL JOIN islands
801                         WHERE archipelago = ?
802                         ORDER BY islandname
803 END
804         my $ix=$#islandids;
805         my $sth_current;
806         my %idone;
807         my $fetchrow= sub {
808                 for (;;) {
809                         if ($sth_current) {
810                                 my $row= $sth_current->fetchrow_hashref();
811                                 if ($row) {
812                                         next if $idone{$row->{'islandid'}}++;
813                                         return $row;
814                                 }
815                         }
816                         return undef if $ix < 0;
817                         my $iid= $islandids[$ix];
818                         if (defined $iid) {
819                                 $sth_i->execute($iid);
820                                 $sth_current= $sth_i;
821                         } else {
822                                 my $arch= $archipelagoes[$ix];
823                                 die unless defined $arch && length $arch;
824                                 $sth_a->execute($arch);
825                                 $sth_current= $sth_a;
826                         }
827                         $ix--;
828                 }
829         };
830 </%perl>
831 <&| query_age:agestable, now => $now, fetchrow => $fetchrow &>
832 Islands shown in reverse order of visits.<br>
833 </&>
834 % } else {
835 %       my @tl= gmtime $now or die $!;
836 <p>
837 Generated by YARRG at <strong><%
838         sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC",
839                 $tl[5]+1900, @tl[4,3,2,1,0]
840                         |h %></strong>.
841 %#      
842 % }
843
844 % if (!printable($m)) {
845 %   my %ts_sortkeys;
846 %   {
847 %       my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
848 %       my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
849 <h2><a name="trades">Relevant trades</a></h2>
850 <table id="trades" rules=groups>
851 <colgroup span=1>
852 <colgroup span=2>
853 <% $qa->{ShowStalls} ? '<colgroup span=2>' : '' %>
854 <colgroup span=1>
855 <colgroup span=2>
856 <colgroup span=2>
857 <colgroup span=2>
858 <colgroup span=3>
859 <colgroup span=3>
860 %       if ($optimise) {
861 <colgroup span=3>
862 %       }
863 <tr>
864 <th>
865 <th<% $cdspan %>>Collect
866 <th<% $cdspan %>>Deliver
867 <th>
868 <th colspan=2>Collect
869 <th colspan=2>Deliver
870 <th colspan=2>Profit
871 <th colspan=3>Max
872 <th colspan=1>
873 <th colspan=2>Max
874 %       if ($optimise) {
875 <th colspan=3>Planned
876 %       }
877
878 <tr>
879 <th>
880 <th>Island <% $cdstall %>
881 <th>Island <% $cdstall %>
882 <th>Commodity
883 <th>Price
884 <th>Qty
885 <th>Price
886 <th>Qty
887 <th>Margin
888 <th>Unit
889 <th>Qty
890 <th>Capital
891 <th>Profit
892 <th>Dist
893 <th>Mass
894 <th>Vol
895 %       if ($optimise) {
896 <th>Qty
897 <th>Capital
898 <th>Profit
899 %       }
900 %   }
901
902 <tr id="trades_sort">
903 %   foreach my $col (@cols) {
904 <th>
905 %   }
906
907 %   foreach my $flowix (0..$#flows) {
908 %       my $flow= $flows[$flowix];
909 %       my $rowid= "id_row_$flow->{UidShort}";
910 <tr id="<% $rowid %>" class="datarow<% $flowix & 1 %>">
911 <td><input type=hidden   name=R<% $flow->{UidShort} %> value="">
912     <input type=checkbox name=T<% $flow->{UidShort} %> value=""
913        <% $flow->{Suppress} ? '' : 'checked' %> >
914 %       my $ci= 1;
915 %       while ($ci < @cols) {
916 %               my $col= $cols[$ci];
917 %               my $spec= {
918 %                       Span => 1,
919 %                       Align => ($col->{Text} ? '' : 'align=right')
920 %               };
921 %               my $cn= $col->{Name};
922 %               my $v;
923 %               if (!$col->{TotalSubflows}) {
924 %                       $v= $flow->{$cn};
925 %               } else {
926 %                       $v= 0;
927 %                       $v += $_->{$cn} foreach @{ $flow->{Subflows} };
928 %               }
929 %               if ($col->{Special}) { $col->{Special}($flow,$col,$v,$spec); }
930 %               $col->{Total} += $v
931 %                       if defined $col->{Total} and not $flow->{Suppress};
932 %               $v='' if !$col->{Text} && !$v;
933 %               my $sortkey= $col->{SortColKey} ?
934 %                       $flow->{$col->{SortColKey}} : $v;
935 %               $ts_sortkeys{$ci}{$rowid}= $sortkey;
936 <td <% $spec->{Span} ? "colspan=$spec->{Span}" : ''
937  %> <% $spec->{Align}
938  %>><% exists $spec->{String} ? $spec->{String} : $v |h %>
939 %               $ci += $spec->{Span};
940 %       }
941 %   }
942 <tr id="trades_total">
943 <th>
944 <th colspan=2>Total
945 %   foreach my $ci (3..$#cols) {
946 %       my $col= $cols[$ci];
947 <td align=right>
948 %       if (defined $col->{Total}) {
949 <% $col->{Total} |h %>
950 %       }
951 %   }
952 </table>
953
954 <&| tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
955         throw => 'trades_sort', tbrow => 'trades_total' &>
956   ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;
957 </&tabsort>
958
959 <input type=submit name=update value="Update">
960
961 % } # !printable
962
963 <%init>
964 use CommodsWeb;
965 use Commods;
966 </%init>