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