chiark / gitweb /
Do not display number available in trading plan
[ypp-sc-tools.web-live.git] / yarrg / web / routetrade
index 1924503b9ce706b888c2450de30403ba19f96a8b..4dbe36e95c533e95cfecce13497c3baba0df66b4 100644 (file)
  This Mason component is the core trade planner for a specific route.
 
 
  This Mason component is the core trade planner for a specific route.
 
 
+========== TODO ==========
+16:36 <ceb> alpha,byrne,papaya,turtle,jorvik,luthien is my example
+
+use POST for update.  Hrrm.
+
+LATER OR NOT AT ALL
+
+adjustable potential cost of losses (rather than fixed 1e-BIG per league)
+
+max volume/mass
+
+========== TODO ==========
+
 </%doc>
 <%args>
 $dbh
 </%doc>
 <%args>
 $dbh
@@ -39,8 +52,15 @@ $dbh
 @archipelagoes
 $qa
 </%args>
 @archipelagoes
 $qa
 </%args>
+<&| script &>
+  da_pageload= Date.now();
+</&script>
+
 <%perl>
 
 <%perl>
 
+my $now= time;
+my $loss_per_league= 1e-7;
+
 my @flow_conds;
 my @query_params;
 
 my @flow_conds;
 my @query_params;
 
@@ -55,18 +75,38 @@ my $sd_condition= sub {
        }
 };
 
        }
 };
 
+my %islandpair;
+# $islandpair{$a,$b}= [ $start_island_ix, $end_island_ix ]
+
+my $specific= !grep { !defined $_ } @islandids;
+my $confusing= 0;
+
 foreach my $src_i (0..$#islandids) {
 foreach my $src_i (0..$#islandids) {
+       my $src_isle= $islandids[$src_i];
        my $src_cond= $sd_condition->('sell',$src_i);
        my @dst_conds;
        foreach my $dst_i ($src_i..$#islandids) {
        my $src_cond= $sd_condition->('sell',$src_i);
        my @dst_conds;
        foreach my $dst_i ($src_i..$#islandids) {
+               my $dst_isle= $islandids[$dst_i];
                my $dst_cond= $sd_condition->('buy',$dst_i);
                my $dst_cond= $sd_condition->('buy',$dst_i);
-               if ($dst_i==$src_i and !defined $islandids[$src_i]) {
+               if ($dst_i==$src_i and !defined $src_isle) {
                        # we always want arbitrage, but mentioning an arch
                        # once shouldn't produce intra-arch trades
                        $dst_cond=
                                "($dst_cond AND sell.islandid = buy.islandid)";
                }
                push @dst_conds, $dst_cond;
                        # we always want arbitrage, but mentioning an arch
                        # once shouldn't produce intra-arch trades
                        $dst_cond=
                                "($dst_cond AND sell.islandid = buy.islandid)";
                }
                push @dst_conds, $dst_cond;
+
+               if ($specific && !$confusing &&
+                   # With a circular route, do not carry goods round the loop
+                   !($src_i==0 && $dst_i==$#islandids &&
+                     $src_isle == $islandids[$dst_i])) {
+                       if ($islandpair{$src_isle,$dst_isle}) {
+                               $confusing= 1;
+                       } else {
+                               $islandpair{$src_isle,$dst_isle}=
+                                       [ $src_i, $dst_i ];
+                       }
+               }
        }
        push @flow_conds, "$src_cond AND (
                        ".join("
        }
        push @flow_conds, "$src_cond AND (
                        ".join("
@@ -78,37 +118,93 @@ my $stmt= "
        SELECT  sell_islands.islandname                         org_name,
                sell_islands.islandid                           org_id,
                sell.price                                      org_price,
        SELECT  sell_islands.islandname                         org_name,
                sell_islands.islandid                           org_id,
                sell.price                                      org_price,
-               sum(sell.qty)                                   org_qty,
+               sell.qty                                        org_qty_stall,
+               sell_stalls.stallname                           org_stallname,
+               sell.stallid                                    org_stallid,
+               sell_uploads.timestamp                          org_timestamp,
                buy_islands.islandname                          dst_name,
                buy_islands.islandid                            dst_id,
                buy.price                                       dst_price,
                buy_islands.islandname                          dst_name,
                buy_islands.islandid                            dst_id,
                buy.price                                       dst_price,
-               sum(buy.qty)                                    dst_qty,
+               buy.qty                                         dst_qty_stall,
+               buy_stalls.stallname                            dst_stallname,
+               buy.stallid                                     dst_stallid,
+               buy_uploads.timestamp                           dst_timestamp,
+".($qa->{ShowStalls} ? "
+               sell.qty                                        org_qty_agg,
+               buy.qty                                         dst_qty_agg,
+" : "
+               (SELECT sum(qty) FROM sell AS sell_agg
+                 WHERE sell_agg.commodid = commods.commodid
+                 AND   sell_agg.islandid = sell.islandid
+                 AND   sell_agg.price = sell.price)            org_qty_agg,
+               (SELECT sum(qty) FROM buy AS buy_agg
+                 WHERE buy_agg.commodid = commods.commodid
+                 AND   buy_agg.islandid = buy.islandid
+                 AND   buy_agg.price = buy.price)              dst_qty_agg,
+")."
                commods.commodname                              commodname,
                commods.commodid                                commodid,
                commods.commodname                              commodname,
                commods.commodid                                commodid,
-               commods.unitmass                                mass,
-               commods.unitvolume                              volume,
-               buy.price - sell.price                          unitprofit,
-               min(sell.qty,buy.qty)                           tqty,
-               min(sell.qty,buy.qty) * (buy.price-sell.price)  profit
+               commods.unitmass                                unitmass,
+               commods.unitvolume                              unitvolume,
+               dist                                            dist,
+               buy.price - sell.price                          unitprofit
        FROM commods
        FROM commods
-       JOIN buy  on commods.commodid = buy.commodid
-       JOIN sell on commods.commodid = sell.commodid
-       JOIN islands as sell_islands on sell.islandid = sell_islands.islandid
-       JOIN islands as buy_islands  on buy.islandid  = buy_islands.islandid
+       JOIN sell ON commods.commodid = sell.commodid
+       JOIN buy  ON commods.commodid = buy.commodid
+       JOIN islands AS sell_islands ON sell.islandid = sell_islands.islandid
+       JOIN islands AS buy_islands  ON buy.islandid  = buy_islands.islandid
+       JOIN uploads AS sell_uploads ON sell.islandid = sell_uploads.islandid
+       JOIN uploads AS buy_uploads  ON buy.islandid  = buy_uploads.islandid
+       JOIN stalls  AS sell_stalls  ON sell.stallid  = sell_stalls.stallid
+       JOIN stalls  AS buy_stalls   ON buy.stallid   = buy_stalls.stallid
+       JOIN dists ON aiid = sell.islandid AND biid = buy.islandid
        WHERE   (
                ".join("
           OR   ", @flow_conds)."
        )
          AND   buy.price > sell.price
        WHERE   (
                ".join("
           OR   ", @flow_conds)."
        )
          AND   buy.price > sell.price
-       GROUP BY commods.commodid, org_id, org_price, dst_id, dst_price
-       ORDER BY org_name, dst_name, profit DESC, commodname,
-                org_price, dst_price DESC
+       ORDER BY org_name, dst_name, commodname, unitprofit DESC,
+                org_price, dst_price DESC,
+                org_stallname, dst_stallname
      ";
 
 my $sth= $dbh->prepare($stmt);
 $sth->execute(@query_params);
 my @flows;
 
      ";
 
 my $sth= $dbh->prepare($stmt);
 $sth->execute(@query_params);
 my @flows;
 
+my @cols= ({ NoSort => 1 });
+
+my $addcols= sub {
+       my $base= shift @_;
+       foreach my $name (@_) {
+               my $col= { Name => $name, %$base };
+               $col->{Numeric}=1 if !$col->{Text};
+               push @cols, $col;
+       }
+};
+
+if ($qa->{ShowStalls}) {
+       $addcols->({ Text => 1 }, qw(
+               org_name org_stallname
+               dst_name dst_stallname
+       ));
+} else {
+       $addcols->({Text => 1 }, qw(
+               org_name dst_name
+       ));
+}
+$addcols->({ Text => 1 }, qw(commodname));
+$addcols->({ DoReverse => 1 },
+       qw(     org_price org_qty_agg dst_price dst_qty_agg
+       ));
+$addcols->({ DoReverse => 1, SortColKey => 'MarginSortKey' },
+       qw(     Margin
+       ));
+$addcols->({ DoReverse => 1 },
+       qw(     unitprofit MaxQty
+               MaxCapital MaxProfit
+       ));
+
 </%perl>
 
 % if ($qa->{'debug'}) {
 </%perl>
 
 % if ($qa->{'debug'}) {
@@ -118,18 +214,148 @@ my @flows;
 </pre>
 % }
 
 </pre>
 % }
 
+<& dumptable:start, qa => $qa, sth => $sth &>
 % {
 % {
-<& dumpqueryresults:start, sth => $sth &>
-%   my $flow;
-%   while ($flow= $sth->fetchrow_hashref()) {
-%      $flow->{Ix}= @flows;
-%      $flow->{Var}= "f$flow->{Ix}";
-%      push @flows, $flow;
-<& dumpqueryresults:row, sth => $sth, row => $flow &>
-%   }
-<& dumpqueryresults:end &>
+%   my $got;
+%   while ($got= $sth->fetchrow_hashref()) {
+<%perl>
+
+       my $f= $flows[$#flows];
+       if (    !$f ||
+               $qa->{ShowStalls} ||
+               grep { $f->{$_} ne $got->{$_} }
+                       qw(org_id org_price dst_id dst_price commodid)
+       ) {
+               # Make a new flow rather than adding to the existing one
+
+               $f= {
+                       Ix => scalar(@flows),
+                       Var => "f".@flows,
+                       %$got
+               };
+               $f->{"org_stallid"}= $f->{"dst_stallid"}= 'all'
+                       if !$qa->{ShowStalls};
+               push @flows, $f;
+       }
+       foreach my $od (qw(org dst)) {
+               $f->{"${od}Stalls"}{
+                       $got->{"${od}_stallname"}
+                   } =
+                       $got->{"${od}_qty_stall"}
+                   ;
+       }
+
+</%perl>
+<& dumptable:row, qa => $qa, sth => $sth, row => $f &>
+%    }
+<& dumptable:end, qa => $qa &>
+% }
+
+<%perl>
+foreach my $f (@flows) {
+
+       $f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
+               ? $f->{'org_qty_agg'} : $f->{'dst_qty_agg'};
+       $f->{MaxProfit}= $f->{MaxQty} * $f->{'unitprofit'};
+       $f->{MaxCapital}= $f->{MaxQty} * $f->{'org_price'};
+
+       $f->{MarginSortKey}= sprintf "%d",
+               $f->{'dst_price'} * 10000 / $f->{'org_price'};
+       $f->{Margin}= sprintf "%3.1f%%",
+               $f->{'dst_price'} * 100.0 / $f->{'org_price'} - 100.0;
+
+       $f->{ExpectedUnitProfit}=
+               $f->{'dst_price'} * (1.0 - $loss_per_league) ** $f->{'dist'}
+               - $f->{'org_price'};
+
+       my @uid= $f->{commodid};
+       foreach my $od (qw(org dst)) {
+               push @uid,
+                       $f->{"${od}_id"},
+                       $f->{"${od}_price"};
+               push @uid,
+                       $f->{"${od}_stallid"}
+                               if $qa->{ShowStalls};
+       }
+       $f->{UidLong}= join '_', @uid;
+
+       my $base= 31;
+       my $cmpu= '';
+       map {
+               my $uue= $_;
+               my $first= $base;
+               do {
+                       my $this= $uue % $base;
+print STDERR "uue=$uue this=$this ";
+                       $uue -= $this;
+                       $uue /= $base;
+                       $this += $first;
+                       $first= 0;
+                       $cmpu .= chr($this + ($this < 26 ? ord('a') :
+                                             $this < 52 ? ord('A')-26
+                                                        : ord('0')-52));
+print STDERR " uue=$uue this=$this cmpu=$cmpu\n";
+die "$cmpu $uue ?" if length $cmpu > 20;
+               } while ($uue);
+               $cmpu;
+       } @uid;
+       $f->{UidShort}= $cmpu;
+
+       if ($qa->{'debug'}) {
+               my @outuid;
+               $_= $f->{UidShort};
+               my $mul;
+               while (m/./) {
+                       my $v= m/^[a-z]/ ? ord($&)-ord('a') :
+                              m/^[A-Z]/ ? ord($&)-ord('A')+26 :
+                              m/^[0-9]/ ? ord($&)-ord('0')+52 :
+                              die "$_ ?";
+                       if ($v >= $base) {
+                               push @outuid, 0;
+                               $v -= $base;
+                               $mul= 1;
+#print STDERR "(next)\n";
+                       }
+                       die "$f->{UidShort} $_ ?" unless defined $mul;
+                       $outuid[$#outuid] += $v * $mul;
+
+#print STDERR "$f->{UidShort}  $_  $&  v=$v  mul=$mul  ord()=".ord($&).
+#                      "[vs.".ord('a').",".ord('A').",".ord('0')."]".
+#                      "  outuid=@outuid\n";
+
+                       $mul *= $base;
+                       s/^.//;
+               }
+               my $recons_long= join '_', @outuid;
+               $f->{UidLong} eq $recons_long or
+                       die "$f->{UidLong} = $f->{UidShort} = $recons_long ?";
+       }
+
+       if (defined $qa->{"R$f->{UidShort}"} &&
+           !defined $qa->{"T$f->{UidShort}"}) {
+               $f->{Suppress}= 1;
+       }
+
+}
+</%perl>
+
+% my $optimise= $specific && !$confusing && @islandids>1;
+% if (!$optimise) {
+
+<p>
+% if (@islandids<=1) {
+Route is trivial.
 % }
 % }
+% if (!$specific) {
+Route contains archipelago(es), not just specific islands.
+% }
+% if ($confusing) {
+Route is complex - it visits the same island several times
+and isn't a simple loop.
+% }
+Therefore, optimal trade pattern not calculated.
 
 
+% } else { # ========== OPTMISATION ==========
 <%perl>
 
 my $cplex= "
 <%perl>
 
 my $cplex= "
@@ -137,22 +363,33 @@ Maximize
 
   totalprofit:
                   ".(join " +
 
   totalprofit:
                   ".(join " +
-                  ", map { "$_->{profit} $_->{Var}" } @flows)."
+                  ", map {
+                       sprintf "%.20f %s", $_->{ExpectedUnitProfit}, $_->{Var}
+                       } @flows)."
 
 Subject To
 ";
 
 my %avail_csts;
 foreach my $flow (@flows) {
 
 Subject To
 ";
 
 my %avail_csts;
 foreach my $flow (@flows) {
+       if ($flow->{Suppress}) {
+               $cplex .= "
+   $flow->{Var} = 0
+";
+               next;
+       }
        foreach my $od (qw(org dst)) {
        foreach my $od (qw(org dst)) {
-               my $cstname= join '_',
+               my $cstname= join '_', (
                        'avail',
                        $flow->{'commodid'},
                        $od,
                        $flow->{"${od}_id"},
                        'avail',
                        $flow->{'commodid'},
                        $od,
                        $flow->{"${od}_id"},
-                       $flow->{"${od}_price"};
+                       $flow->{"${od}_price"},
+                       $flow->{"${od}_stallid"},
+               );
+                       
                push @{ $avail_csts{$cstname}{Flows} }, $flow->{Var};
                push @{ $avail_csts{$cstname}{Flows} }, $flow->{Var};
-               $avail_csts{$cstname}{Qty}= $flow->{"${od}_qty"};
+               $avail_csts{$cstname}{Qty}= $flow->{"${od}_qty_agg"};
        }
 }
 foreach my $cstname (sort keys %avail_csts) {
        }
 }
 foreach my $cstname (sort keys %avail_csts) {
@@ -170,13 +407,244 @@ Bounds
 
 End
 ";
 
 End
 ";
-# glpsol --cpxlp /dev/stdin <t.cplex -o /dev/stdout
 
 
+if ($qa->{'debug'}) {
 </%perl>
 <pre>
 <% $cplex |h %>
 </pre>
 </%perl>
 <pre>
 <% $cplex |h %>
 </pre>
+<%perl>
+}
+
+{
+       my $input= pipethrough_prep();
+       print $input $cplex or die $!;
+       my $output= pipethrough_run_along($input, undef, 'glpsol',
+               qw(glpsol --cpxlp /dev/stdin -o /dev/stdout));
+       print "<pre>\n" if $qa->{'debug'};
+       my $found_section= 0;
+       while (<$output>) {
+               print encode_entities($_) if $qa->{'debug'};
+               if (m/^\s*No\.\s+Column name\s+St\s+Activity\s/) {
+                       die if $found_section>0;
+                       $found_section= 1;
+                       next;
+               }
+               next unless $found_section==1;
+               next if m/^[- ]+$/;
+               if (!/\S/) {
+                       $found_section= 2;
+                       next;
+               }
+               my ($ix, $qty) =
+                       m/^\s*\d+\s+f(\d+)\s+\S+\s+(\d+)\s/ or die "$_ ?";
+               my $flow= $flows[$ix] or die;
+               $flow->{OptQty}= $qty;
+               $flow->{OptProfit}= $flow->{'unitprofit'} * $qty;
+               $flow->{OptCapital}= $flow->{OptQty} * $flow->{'org_price'};
+       }
+       print "</pre>\n" if $qa->{'debug'};
+       pipethrough_run_finish($output, 'glpsol');
+       die unless $found_section;
+};
+
+$addcols->({ DoReverse => 1 }, qw(
+               OptQty
+       ));
+$addcols->({ Total => 0, DoReverse => 1 }, qw(
+               OptCapital OptProfit
+       ));
+
+</%perl>
+
+% } # ========== OPTIMISATION ==========
+
+% my %ts_sortkeys;
+% {
+%      my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
+%      my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
+<table id="trades" rules=groups>
+<colgroup span=1>
+<colgroup span=2>
+<% $qa->{ShowStalls} ? '<colgroup span=2>' : '' %>
+<colgroup span=1>
+<colgroup span=2>
+<colgroup span=2>
+<colgroup span=2>
+<colgroup span=3>
+%      if ($optimise) {
+<colgroup span=3>
+%      }
+<tr class="spong">
+<th>
+<th<% $cdspan %>>Collect
+<th<% $cdspan %>>Deliver
+<th>
+<th colspan=2>Collect
+<th colspan=2>Deliver
+<th colspan=2>Profit
+<th colspan=3>Max
+%      if ($optimise) {
+<th colspan=3>Planned
+%      }
+
+<tr>
+<th>
+<th>Island <% $cdstall %>
+<th>Island <% $cdstall %>
+<th>Commodity
+<th>Price
+<th>Qty
+<th>Price
+<th>Qty
+<th>Margin
+<th>Unit
+<th>Qty
+<th>Capital
+<th>Profit
+%      if ($optimise) {
+<th>Qty
+<th>Capital
+<th>Profit
+%      }
+% }
+
+<tr id="trades_sort">
+% foreach my $col (@cols) {
+<th>
+% }
+
+% foreach my $flowix (0..$#flows) {
+%      my $flow= $flows[$flowix];
+%      my $rowid= "id_row_$flow->{UidShort}";
+<tr id="<% $rowid %>" class="datarow<% $flowix & 1 %>">
+<td><input type=hidden   name=R<% $flow->{UidShort} %> value="">
+    <input type=checkbox name=T<% $flow->{UidShort} %> value=""
+       <% $flow->{Suppress} ? '' : 'checked' %> >
+%      foreach my $ci (1..$#cols) {
+%              my $col= $cols[$ci];
+%              my $v= $flow->{$col->{Name}};
+%              $col->{Total} += $v if defined $col->{Total};
+%              $v='' if !$col->{Text} && !$v;
+%              my $sortkey= $col->{SortColKey} ?
+%                      $flow->{$col->{SortColKey}} : $v;
+%              $ts_sortkeys{$ci}{$rowid}= $sortkey;
+<td <% $col->{Text} ? '' : 'align=right' %>><% $v |h %>
+%      }
+% }
+<tr id="trades_total">
+<th>
+<th colspan=2>Total
+% foreach my $ci (3..$#cols) {
+%      my $col= $cols[$ci];
+<td align=right>
+%      if (defined $col->{Total}) {
+<% $col->{Total} |h %>
+%      }
+% }
+</table>
+
+<& tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
+       throw => 'trades_sort', tbrow => 'trades_total' &>
+<&| script &>
+  ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;
+  function all_onload() {
+    ts_onload__trades();
+  }
+  window.onload= all_onload;
+</&script>
+
+<input type=submit name=update value="Update">
+
+% if ($optimise) { # ========== TRADING PLAN ==========
+%
+% my $iquery= $dbh->prepare('SELECT islandname FROM islands
+%                              WHERE islandid = ?');
+% my %da_ages;
+%
+<h1>Voyage trading plan</h1>
+<table>
+% foreach my $i (0..$#islandids) {
+<tr><td colspan=3><strong>
+%      $iquery->execute($islandids[$i]);
+%      my ($islandname) = $iquery->fetchrow_array();
+%      if (!$i) {
+Start at <% $islandname |h %>
+%      } else {
+Sail to <% $islandname |h %>
+%      }
+</strong>
+%    my $age_reported= 0;
+%    foreach my $od (qw(dst org)) {
+%      my $sign= $od eq 'dst' ? -1 : +1;
+%      my %todo;
+%      foreach my $f (@flows) {
+%              next if $f->{Suppress};
+%              next unless $f->{"${od}_id"} == $islandids[$i];
+%              next unless $f->{OptQty};
+%              my $price= $f->{"${od}_price"};
+%              my $stallname= $f->{"${od}_stallname"};
+%              my $todo= \$todo{ $f->{'commodname'},
+%                                (sprintf "%07d", $price),
+%                                $stallname };
+%              $$todo= { Qty => 0 } unless $$todo;
+%              $$todo->{'commodname'}= $f->{'commodname'};
+%              $$todo->{'stallname'}= $stallname;
+%              $$todo->{Price}= $price;
+%              $$todo->{Timestamp}= $f->{"${od}_timestamp"};
+%              $$todo->{Qty} += $f->{OptQty};
+%              $$todo->{Total}= $$todo->{Price} * $$todo->{Qty};
+%              $$todo->{Stalls}= $f->{"${od}Stalls"};
+%      }
+%      if (%todo && !$age_reported++) {
+%              my $age= $now - (values %todo)[0]->{Timestamp};
+%              my $cellid= "da_${i}";
+%              $da_ages{$cellid}= $age;
+<td colspan=3 align=right>\
+(Data age: <span id="<% $cellid %>"><% prettyprint_age($age) %></span>)
+%      }
+%      my $total= 0;
+%      my $dline= 0;
+%      foreach my $tkey (sort keys %todo) {
+%              my $t= $todo{$tkey};
+%              $total += $t->{Total};
+%              my $span= 0 + keys %{ $t->{Stalls} };
+%              my $td= "td rowspan=$span";
+<tr class="datarow<% $dline %>">
+<<% $td %>><% $od eq 'org' ? 'Collect' : 'Deliver' %>
+<<% $td %>><% $t->{'commodname'} |h %>
+%
+%              my @stalls= sort keys %{ $t->{Stalls} };
+%              my $pstall= sub {
+%                      my $name= $stalls[$_[0]];
+<td><% $name |h %>
+%              };
+%
+%              $pstall->(0);
+<<% $td %> align=right><% $t->{Price} |h %> poe ea.
+<<% $td %> align=right><% $t->{Qty} |h %> unit(s)
+<<% $td %> align=right><% $t->{Total} |h %> total
+%
+%              foreach my $stallix (1..$#stalls) {
+<tr class="datarow<% $dline %>">
+%                      $pstall->($stallix);
+%              }
+%
+%              $dline ^= 1;
+%      }
+%      if (%todo) {
+<tr>
+<td colspan=4><td align=right><% $od eq 'org' ? 'Outlay' : 'Proceeds' %>
+<td align=right><% $total |h %> total
+%      }
+%    }
+% }
+</table>
+<& query_age:dataages, id2age => \%da_ages &>
+%
+% } # ========== TRADING PLAN ==========
 
 <%init>
 use CommodsWeb;
 
 <%init>
 use CommodsWeb;
+use Commods;
 </%init>
 </%init>