chiark / gitweb /
Permit entry of loss per league
[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 $max_mass
42 $max_volume
43 $lossperleaguepct
44 </%args>
45 <&| script &>
46   da_pageload= Date.now();
47 </&script>
48
49 % if (defined $max_mass || defined $max_volume) {
50 <strong>WARNING - VESSEL CAPACITY LIMIT NOT YET IMPLEMENTED</strong>
51 <p>
52 % }
53 % if (defined $lossperleaguepct) {
54 <strong>WARNING - DEFINED LOSS PER LEAGUE NOT YET IMPLEMENTED</strong>
55 <% $lossperleaguepct |h %>
56 <p>
57 % }
58
59 <%perl>
60
61 my $now= time;
62 my $loss_per_league= 1e-7;
63
64 my @flow_conds;
65 my @query_params;
66 my %dists;
67
68 my $sd_condition= sub {
69         my ($bs, $ix) = @_;
70         my $islandid= $islandids[$ix];
71         if (defined $islandid) {
72                 return "${bs}.islandid = $islandid";
73         } else {
74                 push @query_params, $archipelagoes[$ix];
75                 return "${bs}_islands.archipelago = ?";
76         }
77 };
78
79 my %islandpair;
80 # $islandpair{$a,$b}= [ $start_island_ix, $end_island_ix ]
81
82 my $specific= !grep { !defined $_ } @islandids;
83 my $confusing= 0;
84
85 foreach my $src_i (0..$#islandids) {
86         my $src_isle= $islandids[$src_i];
87         my $src_cond= $sd_condition->('sell',$src_i);
88         my @dst_conds;
89         foreach my $dst_i ($src_i..$#islandids) {
90                 my $dst_isle= $islandids[$dst_i];
91                 my $dst_cond= $sd_condition->('buy',$dst_i);
92                 if ($dst_i==$src_i and !defined $src_isle) {
93                         # we always want arbitrage, but mentioning an arch
94                         # once shouldn't produce intra-arch trades
95                         $dst_cond=
96                                 "($dst_cond AND sell.islandid = buy.islandid)";
97                 }
98                 push @dst_conds, $dst_cond;
99
100                 if ($specific && !$confusing &&
101                     # With a circular route, do not carry goods round the loop
102                     !(($src_i==0 || $src_i==$#islandids) &&
103                       $dst_i==$#islandids &&
104                       $src_isle == $islandids[$dst_i])) {
105                         if ($islandpair{$src_isle,$dst_isle}) {
106                                 $confusing= 1;
107 print "confusing $src_i $src_isle  $dst_i $dst_isle\n";
108                         } else {
109                                 $islandpair{$src_isle,$dst_isle}=
110                                         [ $src_i, $dst_i ];
111                         }
112                 }
113         }
114         push @flow_conds, "$src_cond AND (
115                         ".join("
116                      OR ",@dst_conds)."
117                 )";
118 }
119
120 my $stmt= "             
121         SELECT  sell_islands.islandname                         org_name,
122                 sell_islands.islandid                           org_id,
123                 sell.price                                      org_price,
124                 sell.qty                                        org_qty_stall,
125                 sell_stalls.stallname                           org_stallname,
126                 sell.stallid                                    org_stallid,
127                 sell_uploads.timestamp                          org_timestamp,
128                 buy_islands.islandname                          dst_name,
129                 buy_islands.islandid                            dst_id,
130                 buy.price                                       dst_price,
131                 buy.qty                                         dst_qty_stall,
132                 buy_stalls.stallname                            dst_stallname,
133                 buy.stallid                                     dst_stallid,
134                 buy_uploads.timestamp                           dst_timestamp,
135 ".($qa->{ShowStalls} ? "
136                 sell.qty                                        org_qty_agg,
137                 buy.qty                                         dst_qty_agg,
138 " : "
139                 (SELECT sum(qty) FROM sell AS sell_agg
140                   WHERE sell_agg.commodid = commods.commodid
141                   AND   sell_agg.islandid = sell.islandid
142                   AND   sell_agg.price = sell.price)            org_qty_agg,
143                 (SELECT sum(qty) FROM buy AS buy_agg
144                   WHERE buy_agg.commodid = commods.commodid
145                   AND   buy_agg.islandid = buy.islandid
146                   AND   buy_agg.price = buy.price)              dst_qty_agg,
147 ")."
148                 commods.commodname                              commodname,
149                 commods.commodid                                commodid,
150                 commods.unitmass                                unitmass,
151                 commods.unitvolume                              unitvolume,
152                 dist                                            dist,
153                 buy.price - sell.price                          unitprofit
154         FROM commods
155         JOIN sell ON commods.commodid = sell.commodid
156         JOIN buy  ON commods.commodid = buy.commodid
157         JOIN islands AS sell_islands ON sell.islandid = sell_islands.islandid
158         JOIN islands AS buy_islands  ON buy.islandid  = buy_islands.islandid
159         JOIN uploads AS sell_uploads ON sell.islandid = sell_uploads.islandid
160         JOIN uploads AS buy_uploads  ON buy.islandid  = buy_uploads.islandid
161         JOIN stalls  AS sell_stalls  ON sell.stallid  = sell_stalls.stallid
162         JOIN stalls  AS buy_stalls   ON buy.stallid   = buy_stalls.stallid
163         JOIN dists ON aiid = sell.islandid AND biid = buy.islandid
164         WHERE   (
165                 ".join("
166            OR   ", @flow_conds)."
167         )
168           AND   buy.price > sell.price
169         ORDER BY org_name, dst_name, commodname, unitprofit DESC,
170                  org_price, dst_price DESC,
171                  org_stallname, dst_stallname
172      ";
173
174 my $sth= $dbh->prepare($stmt);
175 $sth->execute(@query_params);
176 my @flows;
177
178 my $distquery= $dbh->prepare("
179                 SELECT dist FROM dists WHERE aiid = ? AND biid = ?
180                 ");
181 my $distance= sub {
182         my ($from,$to)= @_;
183         my $d= $dists{$from}{$to};
184         return $d if defined $d;
185         $distquery->execute($from,$to);
186         $d = $distquery->fetchrow_array();
187         defined $d or die "$from $to ?";
188         $dists{$from}{$to}= $d;
189         return $d;
190 };
191
192 my @cols= ({ NoSort => 1 });
193
194 my $addcols= sub {
195         my $base= shift @_;
196         foreach my $name (@_) {
197                 my $col= { Name => $name, %$base };
198                 $col->{Numeric}=1 if !$col->{Text};
199                 push @cols, $col;
200         }
201 };
202
203 if ($qa->{ShowStalls}) {
204         $addcols->({ Text => 1 }, qw(
205                 org_name org_stallname
206                 dst_name dst_stallname
207         ));
208 } else {
209         $addcols->({Text => 1 }, qw(
210                 org_name dst_name
211         ));
212 }
213 $addcols->({ Text => 1 }, qw(commodname));
214 $addcols->({ DoReverse => 1 },
215         qw(     org_price org_qty_agg dst_price dst_qty_agg
216         ));
217 $addcols->({ DoReverse => 1, SortColKey => 'MarginSortKey' },
218         qw(     Margin
219         ));
220 $addcols->({ DoReverse => 1 },
221         qw(     unitprofit MaxQty MaxCapital MaxProfit dist
222         ));
223 foreach my $v (qw(MaxMass MaxVolume)) {
224    $addcols->({
225         DoReverse => 1, Total => 0, SortColKey => "${v}SortKey" }, $v);
226 }
227
228 </%perl>
229
230 % if ($qa->{'debug'}) {
231 <pre>
232 <% $stmt |h %>
233 <% join(' | ',@query_params) |h %>
234 </pre>
235 % }
236
237 <& dumptable:start, qa => $qa, sth => $sth &>
238 % {
239 %   my $got;
240 %   while ($got= $sth->fetchrow_hashref()) {
241 <%perl>
242
243         my $f= $flows[$#flows];
244         if (    !$f ||
245                 $qa->{ShowStalls} ||
246                 grep { $f->{$_} ne $got->{$_} }
247                         qw(org_id org_price dst_id dst_price commodid)
248         ) {
249                 # Make a new flow rather than adding to the existing one
250
251                 $f= {
252                         Ix => scalar(@flows),
253                         Var => "f".@flows,
254                         %$got
255                 };
256                 $f->{"org_stallid"}= $f->{"dst_stallid"}= 'all'
257                         if !$qa->{ShowStalls};
258                 push @flows, $f;
259         }
260         foreach my $od (qw(org dst)) {
261                 $f->{"${od}Stalls"}{
262                         $got->{"${od}_stallname"}
263                     } =
264                         $got->{"${od}_qty_stall"}
265                     ;
266         }
267
268 </%perl>
269 <& dumptable:row, qa => $qa, sth => $sth, row => $f &>
270 %    }
271 <& dumptable:end, qa => $qa &>
272 % }
273
274 <%perl>
275
276 if (!@flows) {
277         print 'No profitable trading opportunities were found.';
278         return;
279 }
280
281 foreach my $f (@flows) {
282
283         $f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
284                 ? $f->{'org_qty_agg'} : $f->{'dst_qty_agg'};
285         $f->{MaxProfit}= $f->{MaxQty} * $f->{'unitprofit'};
286         $f->{MaxCapital}= $f->{MaxQty} * $f->{'org_price'};
287
288         $f->{MaxMassSortKey}= $f->{MaxQty} * $f->{'unitmass'};
289         $f->{MaxVolumeSortKey}= $f->{MaxQty} * $f->{'unitvolume'};
290         foreach my $v (qw(Mass Volume)) {
291                 $f->{"Max$v"}= sprintf "%.1f", $f->{"Max${v}SortKey"} * 1e-6;
292         }
293
294         $f->{MarginSortKey}= sprintf "%d",
295                 $f->{'dst_price'} * 10000 / $f->{'org_price'};
296         $f->{Margin}= sprintf "%3.1f%%",
297                 $f->{'dst_price'} * 100.0 / $f->{'org_price'} - 100.0;
298
299         $f->{ExpectedUnitProfit}=
300                 $f->{'dst_price'} * (1.0 - $loss_per_league) ** $f->{'dist'}
301                 - $f->{'org_price'};
302
303         $dists{'org_id'}{'dst_id'}= $f->{'dist'};
304
305         my @uid= $f->{commodid};
306         foreach my $od (qw(org dst)) {
307                 push @uid,
308                         $f->{"${od}_id"},
309                         $f->{"${od}_price"};
310                 push @uid,
311                         $f->{"${od}_stallid"}
312                                 if $qa->{ShowStalls};
313         }
314         $f->{UidLong}= join '_', @uid;
315
316         my $base= 31;
317         my $cmpu= '';
318         map {
319                 my $uue= $_;
320                 my $first= $base;
321                 do {
322                         my $this= $uue % $base;
323 print STDERR "uue=$uue this=$this ";
324                         $uue -= $this;
325                         $uue /= $base;
326                         $this += $first;
327                         $first= 0;
328                         $cmpu .= chr($this + ($this < 26 ? ord('a') :
329                                               $this < 52 ? ord('A')-26
330                                                          : ord('0')-52));
331 print STDERR " uue=$uue this=$this cmpu=$cmpu\n";
332 die "$cmpu $uue ?" if length $cmpu > 20;
333                 } while ($uue);
334                 $cmpu;
335         } @uid;
336         $f->{UidShort}= $cmpu;
337
338         if ($qa->{'debug'}) {
339                 my @outuid;
340                 $_= $f->{UidShort};
341                 my $mul;
342                 while (m/./) {
343                         my $v= m/^[a-z]/ ? ord($&)-ord('a') :
344                                m/^[A-Z]/ ? ord($&)-ord('A')+26 :
345                                m/^[0-9]/ ? ord($&)-ord('0')+52 :
346                                die "$_ ?";
347                         if ($v >= $base) {
348                                 push @outuid, 0;
349                                 $v -= $base;
350                                 $mul= 1;
351 #print STDERR "(next)\n";
352                         }
353                         die "$f->{UidShort} $_ ?" unless defined $mul;
354                         $outuid[$#outuid] += $v * $mul;
355
356 #print STDERR "$f->{UidShort}  $_  $&  v=$v  mul=$mul  ord()=".ord($&).
357 #                       "[vs.".ord('a').",".ord('A').",".ord('0')."]".
358 #                       "  outuid=@outuid\n";
359
360                         $mul *= $base;
361                         s/^.//;
362                 }
363                 my $recons_long= join '_', @outuid;
364                 $f->{UidLong} eq $recons_long or
365                         die "$f->{UidLong} = $f->{UidShort} = $recons_long ?";
366         }
367
368         if (defined $qa->{"R$f->{UidShort}"} &&
369             !defined $qa->{"T$f->{UidShort}"}) {
370                 $f->{Suppress}= 1;
371         }
372
373 }
374 </%perl>
375
376 % my $optimise= $specific && !$confusing && @islandids>1;
377 % if (!$optimise) {
378
379 <p>
380 % if (@islandids<=1) {
381 Route is trivial.
382 % }
383 % if (!$specific) {
384 Route contains archipelago(es), not just specific islands.
385 % }
386 % if ($confusing) {
387 Route is complex - it visits the same island several times
388 and isn't a simple loop.
389 % }
390 Therefore, optimal voyage trade plan not calculated.
391
392 % } else { # ========== OPTMISATION ==========
393 <%perl>
394
395 my $cplex= "
396 Maximize
397
398   totalprofit:
399                   ".(join " +
400                   ", map {
401                         sprintf "%.20f %s", $_->{ExpectedUnitProfit}, $_->{Var}
402                         } @flows)."
403
404 Subject To
405 ";
406
407 my %avail_csts;
408 foreach my $flow (@flows) {
409         if ($flow->{Suppress}) {
410                 $cplex .= "
411    $flow->{Var} = 0
412 ";
413                 next;
414         }
415         foreach my $od (qw(org dst)) {
416                 my $cstname= join '_', (
417                         'avail',
418                         $flow->{'commodid'},
419                         $od,
420                         $flow->{"${od}_id"},
421                         $flow->{"${od}_price"},
422                         $flow->{"${od}_stallid"},
423                 );
424                         
425                 push @{ $avail_csts{$cstname}{Flows} }, $flow->{Var};
426                 $avail_csts{$cstname}{Qty}= $flow->{"${od}_qty_agg"};
427         }
428 }
429 foreach my $cstname (sort keys %avail_csts) {
430         my $c= $avail_csts{$cstname};
431         $cplex .= "
432    ".   sprintf("%-30s","$cstname:")." ".
433         join("+", @{ $c->{Flows} }).
434         " <= ".$c->{Qty}."\n";
435 }
436
437 $cplex.= "
438 Bounds
439         ".(join "
440         ", map { "$_->{Var} >= 0" } @flows)."
441
442 End
443 ";
444
445 if ($qa->{'debug'}) {
446 </%perl>
447 <pre>
448 <% $cplex |h %>
449 </pre>
450 <%perl>
451 }
452
453 {
454         my $input= pipethrough_prep();
455         print $input $cplex or die $!;
456         my $output= pipethrough_run_along($input, undef, 'glpsol',
457                 qw(glpsol --cpxlp /dev/stdin -o /dev/stdout));
458         print "<pre>\n" if $qa->{'debug'};
459         my $found_section= 0;
460         my $glpsol_out= '';
461         while (<$output>) {
462                 $glpsol_out.= $_;
463                 print encode_entities($_) if $qa->{'debug'};
464                 if (m/^\s*No\.\s+Column name\s+St\s+Activity\s/) {
465                         die if $found_section>0;
466                         $found_section= 1;
467                         next;
468                 }
469                 next unless $found_section==1;
470                 next if m/^[- ]+$/;
471                 if (!/\S/) {
472                         $found_section= 2;
473                         next;
474                 }
475                 my ($ix, $qty) =
476                         m/^\s*\d+\s+f(\d+)\s+\S+\s+(\d+)\s/ or die "$_ ?";
477                 my $flow= $flows[$ix] or die;
478                 $flow->{OptQty}= $qty;
479                 $flow->{OptProfit}= $flow->{'unitprofit'} * $qty;
480                 $flow->{OptCapital}= $flow->{OptQty} * $flow->{'org_price'};
481         }
482         print "</pre>\n" if $qa->{'debug'};
483         my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n ";
484         pipethrough_run_finish($output,$prerr);
485         die $prerr unless $found_section;
486 };
487
488 $addcols->({ DoReverse => 1 }, qw(
489                 OptQty
490         ));
491 $addcols->({ Total => 0, DoReverse => 1 }, qw(
492                 OptCapital OptProfit
493         ));
494
495 </%perl>
496
497 % } # ========== OPTIMISATION ==========
498
499 % my %ts_sortkeys;
500 % {
501 %       my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
502 %       my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
503 <table id="trades" rules=groups>
504 <colgroup span=1>
505 <colgroup span=2>
506 <% $qa->{ShowStalls} ? '<colgroup span=2>' : '' %>
507 <colgroup span=1>
508 <colgroup span=2>
509 <colgroup span=2>
510 <colgroup span=2>
511 <colgroup span=3>
512 <colgroup span=3>
513 %       if ($optimise) {
514 <colgroup span=3>
515 %       }
516 <tr class="spong">
517 <th>
518 <th<% $cdspan %>>Collect
519 <th<% $cdspan %>>Deliver
520 <th>
521 <th colspan=2>Collect
522 <th colspan=2>Deliver
523 <th colspan=2>Profit
524 <th colspan=3>Max
525 <th colspan=1>
526 <th colspan=2>Max
527 %       if ($optimise) {
528 <th colspan=3>Planned
529 %       }
530
531 <tr>
532 <th>
533 <th>Island <% $cdstall %>
534 <th>Island <% $cdstall %>
535 <th>Commodity
536 <th>Price
537 <th>Qty
538 <th>Price
539 <th>Qty
540 <th>Margin
541 <th>Unit
542 <th>Qty
543 <th>Capital
544 <th>Profit
545 <th>Dist
546 <th>Mass
547 <th>Vol
548 %       if ($optimise) {
549 <th>Qty
550 <th>Capital
551 <th>Profit
552 %       }
553 % }
554
555 <tr id="trades_sort">
556 % foreach my $col (@cols) {
557 <th>
558 % }
559
560 % foreach my $flowix (0..$#flows) {
561 %       my $flow= $flows[$flowix];
562 %       my $rowid= "id_row_$flow->{UidShort}";
563 <tr id="<% $rowid %>" class="datarow<% $flowix & 1 %>">
564 <td><input type=hidden   name=R<% $flow->{UidShort} %> value="">
565     <input type=checkbox name=T<% $flow->{UidShort} %> value=""
566        <% $flow->{Suppress} ? '' : 'checked' %> >
567 %       foreach my $ci (1..$#cols) {
568 %               my $col= $cols[$ci];
569 %               my $v= $flow->{$col->{Name}};
570 %               $col->{Total} += $v
571 %                       if defined $col->{Total} and not $flow->{Suppress};
572 %               $v='' if !$col->{Text} && !$v;
573 %               my $sortkey= $col->{SortColKey} ?
574 %                       $flow->{$col->{SortColKey}} : $v;
575 %               $ts_sortkeys{$ci}{$rowid}= $sortkey;
576 <td <% $col->{Text} ? '' : 'align=right' %>><% $v |h %>
577 %       }
578 % }
579 <tr id="trades_total">
580 <th>
581 <th colspan=2>Total
582 % foreach my $ci (3..$#cols) {
583 %       my $col= $cols[$ci];
584 <td align=right>
585 %       if (defined $col->{Total}) {
586 <% $col->{Total} |h %>
587 %       }
588 % }
589 </table>
590
591 <&| tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
592         throw => 'trades_sort', tbrow => 'trades_total' &>
593   ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;
594 </&tabsort>
595
596 <input type=submit name=update value="Update">
597
598 % if ($optimise) { # ========== TRADING PLAN ==========
599 %
600 % my $iquery= $dbh->prepare('SELECT islandname FROM islands
601 %                               WHERE islandid = ?');
602 % my %da_ages;
603 % my $total_total= 0;
604 % my $total_dist= 0;
605 %
606 <h1>Voyage trading plan</h1>
607 <table rules=groups>
608 % foreach my $i (0..$#islandids) {
609 <tbody>
610 <tr><td colspan=3>
611 %       $iquery->execute($islandids[$i]);
612 %       my ($islandname) = $iquery->fetchrow_array();
613 %       if (!$i) {
614 <strong>Start at <% $islandname |h %></strong>
615 %       } else {
616 %               my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
617 %               $total_dist += $this_dist;
618 <strong>Sail to <% $islandname |h %></strong>
619 - <% $this_dist |h %> leagues </td>
620 %       }
621 <%perl>
622      my $age_reported= 0;
623      my %flowlists;
624      foreach my $od (qw(org dst)) {
625         foreach my $f (@flows) {
626                 next if $f->{Suppress};
627                 next unless $f->{"${od}_id"} == $islandids[$i];
628                 next unless $f->{OptQty};
629                 my $arbitrage= $f->{'org_id'} == $f->{'dst_id'};
630                 my $loop= $islandids[0] == $islandids[-1] &&
631                           ($i==0 || $i==$#islandids);
632                 next if $loop and ($arbitrage ? $i :
633                         !!$i == !!($od eq 'org'));
634                 my $price= $f->{"${od}_price"};
635                 my $stallname= $f->{"${od}_stallname"};
636                 my $todo= \$flowlists{$od}{
637                                 $f->{'commodname'},
638                                 (sprintf "%07d", ($od eq 'dst' ?
639                                                 9999999-$price : $price)),
640                                 $stallname
641                         };
642                 $$todo= {
643                         Qty => 0,
644                         orgArbitrage => 0,
645                         dstArbitrage => 0,
646                 } unless $$todo;
647                 $$todo->{'commodname'}= $f->{'commodname'};
648                 $$todo->{'stallname'}= $stallname;
649                 $$todo->{Price}= $price;
650                 $$todo->{Timestamp}= $f->{"${od}_timestamp"};
651                 $$todo->{Qty} += $f->{OptQty};
652                 $$todo->{Total}= $$todo->{Price} * $$todo->{Qty};
653                 $$todo->{Stalls}= $f->{"${od}Stalls"};
654                 $$todo->{"${od}Arbitrage"}= 1 if $arbitrage;
655         }
656      }
657
658      my $total;
659      my $dline= 0;
660      my $show_flows= sub {
661         my ($od,$arbitrage,$collectdeliver) = @_;
662 </%perl>
663 %
664 %       my $todo= $flowlists{$od};
665 %       return unless $todo;
666 %       foreach my $tkey (sort keys %$todo) {
667 %               my $t= $todo->{$tkey};
668 %               next if $t->{"${od}Arbitrage"} != $arbitrage;
669 %               if (!$age_reported++) {
670 %                       my $age= $now - $t->{Timestamp};
671 %                       my $cellid= "da_${i}";
672 %                       $da_ages{$cellid}= $age;
673 <td colspan=3>\
674 (Data age: <span id="<% $cellid %>"><% prettyprint_age($age) %></span>)
675 %               } elsif (!defined $total) {
676 %                       $total= 0;
677 <tbody>
678 %               }
679 %               $total += $t->{Total};
680 %               my $span= 0 + keys %{ $t->{Stalls} };
681 %               my $td= "td rowspan=$span";
682 <tr class="datarow<% $dline %>">
683 <<% $td %>><% $collectdeliver %>
684 <<% $td %>><% $t->{'commodname'} |h %>
685 %
686 %               my @stalls= sort keys %{ $t->{Stalls} };
687 %               my $pstall= sub {
688 %                       my $name= $stalls[$_[0]];
689 <td><% $name |h %>
690 %               };
691 %
692 %               $pstall->(0);
693 <<% $td %> align=right><% $t->{Price} |h %> poe ea.
694 <<% $td %> align=right><% $t->{Qty} |h %> unit(s)
695 <<% $td %> align=right><% $t->{Total} |h %> total
696 %
697 %               foreach my $stallix (1..$#stalls) {
698 <tr class="datarow<% $dline %>">
699 %                       $pstall->($stallix);
700 %               }
701 %
702 %               $dline ^= 1;
703 %       }
704 %    };
705 %    my $show_total= sub {
706 %       my ($totaldesc, $sign)= @_;
707 %       if (defined $total) {
708 <tr>
709 <td colspan=3>
710 <td colspan=2 align=right><% $totaldesc %>
711 <td align=right><% $total |h %> total
712 %               $total_total += $sign * $total;
713 %       }
714 %       $total= undef;
715 %       $dline= 0;
716 <%perl>
717      };
718
719      $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
720      $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
721      $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1);
722      $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1);
723
724 }
725 </%perl>
726 <tbody><tr>
727 <td colspan=2>Total distance: <% $total_dist %> leagues.
728 <td colspan=3 align=right>Overall net cash flow
729 <td align=right><strong><%
730   $total_total < 0 ? -$total_total." loss" : $total_total." gain"
731  %></strong>
732 </table>
733 <& query_age:dataages, id2age => \%da_ages &>
734 %
735 % } # ========== TRADING PLAN ==========
736
737 <%init>
738 use CommodsWeb;
739 use Commods;
740 </%init>