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