chiark / gitweb /
Better doc
[ypp-sc-tools.db-live.git] / yarrg / web / routetrade
index 4134020..03ef3e1 100644 (file)
@@ -39,6 +39,8 @@ $dbh
 @archipelagoes
 $qa
 $routeparams
+$reset_suppressions
+$quri
 </%args>
 <& query_age:pageload &>
 
@@ -47,6 +49,9 @@ $routeparams
 my $loss_per_league= defined $routeparams->{LossPerLeaguePct}
        ? $routeparams->{LossPerLeaguePct}*0.01 : 1e-7;
 my $loss_per_delay_slot= 1e-8;
+my $max_gems= 24;
+
+my $minprofit= $routeparams->{MinProfit} || 0;
 
 my $now= time;
 
@@ -143,6 +148,7 @@ my $stmt= "
                commods.ordval                                  ordval,
                commods.posinclass                              posinclass,
                commods.commodclassid                           commodclassid,
+               commods.flags                                   flags,
                dist                                            dist,
                buy.price - sell.price                          unitprofit
        FROM commods
@@ -275,13 +281,21 @@ Searched for arbitrage trades only, in <% $archipelagoes[0] |h %>
 
 <%perl>
 
-my @sail_total;
-
 if (!@flows) {
        print 'No profitable trading opportunities were found.';
        return;
 }
 
+my @sail_total;
+my %opportunity_value;
+
+my $oppo_key= sub {
+       my ($f) = @_;
+       return join '_', map { $f->{$_} } qw(org_id dst_id commodid);
+};
+
+my $any_previous_suppression= 0;
+
 foreach my $f (@flows) {
 
        $f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
@@ -306,6 +320,8 @@ foreach my $f (@flows) {
 
        $dists{'org_id'}{'dst_id'}= $f->{'dist'};
 
+       $opportunity_value{ $oppo_key->($f) } += $f->{MaxProfit};
+
        my @uid= $f->{commodid};
        foreach my $od (qw(org dst)) {
                push @uid,
@@ -368,11 +384,21 @@ foreach my $f (@flows) {
                $f->{UidLong} eq $recons_long or
                        die "$f->{UidLong} = $f->{UidShort} = $recons_long ?";
        }
+}
+
+foreach my $f (@flows) {
 
-       if (defined $qa->{"R$f->{UidShort}"} &&
-           !defined $qa->{"T$f->{UidShort}"}) {
-               $f->{Suppress}= 1;
+       if ($reset_suppressions || !defined $qa->{"R$f->{UidShort}"}) {
+               if ($opportunity_value{ $oppo_key->($f) } < $minprofit) {
+                       $f->{Suppress}= 1;
+               }
        } else {
+               if (!defined $qa->{"T$f->{UidShort}"}) {
+                       $any_previous_suppression= 1;
+                       $f->{Suppress}= 1;
+               }
+       }
+       if (!$f->{Suppress}) {
                my $sfis= $ipair2subflowinfs{$f->{'org_id'},$f->{'dst_id'}};
                foreach my $sfi (@$sfis) {
                        my $subflow= {
@@ -392,13 +418,25 @@ foreach my $f (@flows) {
 }
 </%perl>
 
-% my $optimise= $specific;
-% if (!$optimise) {
+% my $optimise= 1;
+% my $opt_how;
 
-<p>
 % if (!$specific) {
+%      $optimise= 0;
 Route contains archipelago(es), not just specific islands.
+% } elsif (!@subflows) {
+%      $optimise= 0;
+%      if ($any_previous_suppression) {
+All available trades deselected.
+%      } else {
+No available trades meet the specified minimum trade value, so
+all available trades deselected.
+%      }
 % }
+
+% if (!$optimise) {
+
+<p>
 Therefore, optimal voyage trade plan not calculated.
 
 % } else { # ========== OPTMISATION ==========
@@ -410,11 +448,17 @@ Maximize
   totalprofit:
 ";
 
+my %stall_poe_limits;
+
 foreach my $sf (@subflows) {
        my $eup= $sf->{Flow}{ExpectedUnitProfit};
        $eup *= (1.0-$loss_per_delay_slot) ** $sf->{Org};
        $cplex .= sprintf "
                %+.20f %s", $eup, $sf->{Var};
+       if ($qa->{ShowStalls}>=2) {
+               my $stall= $sf->{Flow}{'dst_stallid'};
+               push @{ $stall_poe_limits{$stall} }, $sf;
+       }
 }
 $cplex .= "
 
@@ -462,7 +506,7 @@ foreach my $ci (0..($#islandids-1)) {
        }
 #print " RELEVANT $ci COUNT ".scalar(@rel_subflows)."  ";
        if (!@rel_subflows) {
-               foreach my $mv (qw(mass volume)) {
+               foreach my $mv (qw(mass volume capital)) {
                        $sail_total[$ci]{$mv}= 0;
                }
                next;
@@ -485,6 +529,32 @@ foreach my $ci (0..($#islandids-1)) {
        $applylimit->('mass',    sub { $_[0]{'unitmass'}  *1e-3 });
        $applylimit->('volume',  sub { $_[0]{'unitvolume'}*1e-3 });
        $applylimit->('capital', sub { $_[0]{'org_price'}       });
+
+       my @gem_subflows= grep { $_->{Flow}{flags} =~ m/g/ } @rel_subflows;
+       if (@gem_subflows) {
+               $cplex .= "
+   ". sprintf("%-10s","gems_$ci:")." ".
+               join(" + ", map { $_->{Var} } @gem_subflows). " <= $max_gems";
+       }
+
+       $cplex.= "\n";
+}
+
+if ($qa->{ShowStalls}>=2) {
+       my $stallpoe= $dbh->prepare(<<END);
+SELECT max(qty*price) FROM buy WHERE stallid=?
+END
+       foreach my $stallid (sort { $a <=> $b } keys %stall_poe_limits) {
+               $stallpoe->execute($stallid);
+               my ($lim)= $stallpoe->fetchrow_array();
+               $stallpoe->finish();
+               $cplex.= "
+    ". sprintf("%-15s","poe_$stallid:")." ".
+               join(" + ", map {
+                       sprintf "%d %s", $_->{Flow}{'dst_price'}, $_->{Var};
+               } @{ $stall_poe_limits{$stallid} }).
+               " <= $lim";
+       }
        $cplex.= "\n";
 }
 
@@ -511,16 +581,23 @@ if ($qa->{'debug'}) {
 <%perl>
 }
 
-{
+my $try_solve= sub {
+       my ($how, @opts) = @_;
        my $input= pipethrough_prep();
        print $input $cplex or die $!;
        my $output= pipethrough_run_along($input, undef, 'glpsol',
-               qw(glpsol --tmlim 2 --memlim 5 --intopt --cuts --bfs
-                         --cpxlp /dev/stdin -o /dev/stdout));
-       print "<pre>\n" if $qa->{'debug'};
+               qw(glpsol --tmlim 5 --memlim 20), @opts,
+               qw( --cpxlp /dev/stdin -o /dev/stdout));
+       if ($qa->{'debug'}) {
+               print "<h3>@opts</h3>\n<pre>\n";
+       }
+       $expected_total_profit= undef;
+       $_->{OptQty}= undef foreach @subflows;
        my $found_section= 0;
        my $glpsol_out= '';
        my $continuation='';
+       my $timelimit= 0;
+       my $somemip= 0;
        while (<$output>) {
                $glpsol_out.= $_;
                print encode_entities($_) if $qa->{'debug'};
@@ -529,6 +606,14 @@ if ($qa->{'debug'}) {
                        $found_section= 1;
                        next;
                }
+               if ((m/^Integer optimization begins/ .. 0) &&
+                   m/^\+ \s* \d+\: \s* mip \s* = \s* \d/) {
+                       $somemip= 1;
+                       next;
+               }
+               if (m/^TIME LIMIT EXCEEDED/) {
+                       $timelimit= 1;
+               }
                if (m/^Objective:\s+totalprofit = (\d+(?:\.\d*)?) /) {
                        $expected_total_profit= $1;
                }
@@ -549,7 +634,7 @@ if ($qa->{'debug'}) {
                my ($varname, $qty) = m/^
                        \s* \d+ \s+
                        (\w+) \s+ (?: [A-Z*]+ \s+ )?
-                       ([+-e0-9.]+) \s
+                       ([-+0-9]+)(?: [.e][-+e0-9.]* )? \s
                        /x or die "$cplex \n==\n $glpsol_out $_ ?";
                if ($varname =~ m/^f(\d+)s(\d+)_/) {
                        my ($ix,$orgix) = ($1,$2);
@@ -561,7 +646,7 @@ if ($qa->{'debug'}) {
                        $sf->{OptQty}= $qty;
                        $sf->{OptProfit}= $qty * $flow->{'unitprofit'};
                        $sf->{OptCapital}= $qty * $flow->{'org_price'};
-               } elsif ($varname =~ m/^(mass|volume)_(\d+)$/) {
+               } elsif ($varname =~ m/^(mass|volume|capital)_(\d+)$/) {
                        my ($mv,$ix) = ($1,$2);
                        $sail_total[$ix]{$mv}= $qty;
                }
@@ -571,8 +656,28 @@ if ($qa->{'debug'}) {
        pipethrough_run_finish($output,$prerr);
        map { defined $_->{OptQty} or die "$prerr $_->{Flow}{Ix}" } @subflows;
        defined $expected_total_profit or die "$prerr ?";
+       return 0 unless $somemip || !$timelimit;
+       $opt_how= $how;
+       return 1;
 };
 
+unless ($try_solve->('Optimisation successful',
+                    qw( --intopt --cuts --bfs )) or
+       $try_solve->('<strong>Complex problem, downgraded</strong>'.
+                    ' to rounded-down LP.',
+                    qw( --nomip ))) {
+</%perl>
+<h2>Optimisation failed</h2>
+The linear/mixed-integer optimisation failed.
+Please report this problem.
+
+<pre>
+<% $cplex |h %>
+</pre>
+<%perl>
+       return;
+}
+
 $addcols->({ DoReverse => 1, TotalSubflows => 1, Special => sub {
        my ($flow,$col,$v,$spec) = @_;
        if ($flow->{ExpectedUnitProfit} < 0) {
@@ -595,9 +700,9 @@ $addcols->({ Total => 0, DoReverse => 1, TotalSubflows => 1 }, qw(
 <h2>Contents</h2>
 <ul>
 % if ($optimise) {
+ <li><a href="#summary">Summary</a>
  <li><a href="#plan">Voyage trading plan</a>
   <ul>
-   <li><a href="#summary">Summary statistics</a>
    <li>Printable:
          <input type=submit name=printable_pdf value="PDF">
          <input type=submit name=printable_html value="HTML">
@@ -606,7 +711,7 @@ $addcols->({ Total => 0, DoReverse => 1, TotalSubflows => 1 }, qw(
          <input type=submit name=printable_ps2 value="PostScript 2-up">
   </ul>
 % }
- <li><a href="#dataage">Data age summary</a>
+ <li><a href="#dataage">Relevant data ages</a>
  <li><a href="#trades">Relevant trades</a>
 </ul>
 % } else {
@@ -619,47 +724,57 @@ Generated by YARRG at <strong><%
 % }
 
 % if ($optimise) { # ========== TRADING PLAN ==========
-%
-% my $iquery= $dbh->prepare('SELECT islandname FROM islands
-%                              WHERE islandid = ?');
-% my %da_ages;
-% my $total_total= 0;
-% my $total_dist= 0;
-%
-<h2><a name="plan">Voyage trading plan</a></h2>
-
-<table class="data" rules=groups <% printable($m) ? 'width=100%' : '' %> >
-% my $tbody= sub {
-%      if (!printable($m)) { return '<tbody>'; }
-%#  return "<tr><td colspan=7><hr>";
-%      my ($c)= qw(40 00)[$_[0]];
-%      return "<tr><td bgcolor=\"#${c}${c}${c}\" height=1 colspan=7>";
-% };
-%
-% foreach my $i (0..$#islandids) {
-<% $tbody->(1) %>
-<tr><td colspan=5>
-%      $iquery->execute($islandids[$i]);
-%      my ($islandname) = $iquery->fetchrow_array();
-%      if (!$i) {
-<strong>Start at <% $islandname |h %></strong>
-%      } else {
-%              my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
-%              $total_dist += $this_dist;
 <%perl>
+my $iquery= $dbh->prepare('SELECT islandname FROM islands
+                               WHERE islandid = ?');
+my %da_ages;
+my $total_total= 0;
+my $total_dist= 0;
+my @oldest= (-1, 'nowhere');
+my $plan_html= '';
+
+my $plan_table_info= printable($m) ? 'width=100%' : '';
+$plan_html .= <<END;
+<table class="data" rules=groups $plan_table_info >
+END
+
+my $tbody= sub {
+       if (!printable($m)) { return '<tbody>'; }
+       my ($c)= qw(40 00)[$_[0]];
+       return "<tr><td bgcolor=\"#${c}${c}${c}\" height=1 colspan=7>";
+};
+
+foreach my $i (0..$#islandids) {
+     $plan_html .= $tbody->(1);
+     $plan_html .= "<tr>\n";
+     $iquery->execute($islandids[$i]);
+     my ($islandnamepr)= encode_entities( $iquery->fetchrow_array() );
+       
+     if (!$i) {
+               $plan_html .= <<END;
+<td colspan=2>
+<strong>Start at $islandnamepr</strong>
+<td colspan=2><a href="docs#posinclass">[what are these codes?]</a>
+<td>
+END
+     } else {
+               my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
+               $total_dist += $this_dist;
+               $plan_html .= <<END;
+<td colspan=5>
+END
                my $total_value= 0;
                foreach my $sf (@subflows) {
                        next unless $sf->{Org} < $i && $sf->{Dst} >= $i;
                        $total_value +=
                                $sf->{OptQty} * $sf->{Flow}{'dst_price'};
                }
-</%perl>
-<strong>Sail to <% $islandname |h %></strong>
-- <% $this_dist |h %> leagues,
- <% $total_value %>poe at risk
+               $plan_html .= <<END;
+<strong>Sail to $islandnamepr</strong>
+- $this_dist leagues, $total_value poe at risk
  </td>
-%      }
-<%perl>
+END
+     }
      my $age_reported= 0;
      my %flowlists;
      #print "<tr><td colspan=7>" if $qa->{'debug'};
@@ -687,34 +802,34 @@ Generated by YARRG at <strong><%
                        orgArbitrage => 0,
                        dstArbitrage => 0,
                } unless $$todo;
+               $$todo->{'commodid'}= $f->{'commodid'};
                $$todo->{'commodname'}= $f->{'commodname'};
                $$todo->{'posinclass'}= '';
-               if ($f->{'posinclass'}) {
-                       my $findclass= $dbh->prepare(<<END);
+               my $incl= $f->{'posinclass'};
+
+               my $findclass= $dbh->prepare(<<END);
 SELECT commodclass, maxposinclass FROM commodclasses WHERE commodclassid = ?
 END
-                       $findclass->execute($f->{'commodclassid'});
-                       my $classinfo= $findclass->fetchrow_hashref();
-                       if ($classinfo->{'maxposinclass'} >= 8) {
+               $findclass->execute($f->{'commodclassid'});
+               my $classinfo= $findclass->fetchrow_hashref();
+               if ($classinfo) {
+                       my $clname= $classinfo->{'commodclass'};
+                       my $desc= encode_entities(sprintf "%s is under %s",
+                                       $f->{'commodname'}, $clname);
+                       my $abbrev= substr($clname,0,1);
+                       if ($incl) {
                                my $maxpic= $classinfo->{'maxposinclass'};
-                               my $inpic= $f->{'posinclass'};
-                               my @tmbs= qw(TT T M B BB);
-                               my @tmbds= (    'in Top fifth of list',
-                                               'in 2nd fifth of the list',
-                                               'in Middle fifth of the list',
-                                               'in 2nd-bottom fifth of list',
-                                               'in Bottom fifth of the list');
-                               my $tmbi= ($inpic+0.5)*$#tmbs/$maxpic;
-                               my $desc= (sprintf "%s is under %s,".
-                                       " commodity %d of %d;".
-                                       " i.e. %s or %s",
-                                       $f->{'commodname'},
-                                       $classinfo->{'commodclass'},
-                                       $inpic, $maxpic,
-                                       $tmbs[$tmbi], $tmbds[$tmbi]);
-                               $$todo->{'posinclass'}=
- "<div class=mouseover title=\"$desc\">$tmbs[$tmbi]</div>";
+                               $desc.= (sprintf ", commodity %d of %d",
+                                       $incl, $maxpic);
+                               if ($classinfo->{'maxposinclass'} >= 8) {
+                                       my @tmbs= qw(0 1 2 3 4 5 6 7 8 9);
+                                       my $tmbi= ($incl+0.5)*$#tmbs/$maxpic;
+                                       $abbrev.= " ".$tmbs[$tmbi]."&nbsp;";
+                               }
                        }
+                       $$todo->{'posinclass'}=
+                               "<div class=mouseover title=\"$desc\">"
+                               .$abbrev."</div>";
                }
                $$todo->{'stallname'}= $stallname;
                $$todo->{Price}= $price;
@@ -744,62 +859,72 @@ END
        my ($xinfo) = @_;
        return unless defined $total_to_show;
        my ($totaldesc,$totalwas) = @$total_to_show;
-</%perl>
+       $plan_html .= <<END;
 <tr>
 <td colspan=1>
-<td colspan=3><% $xinfo %>
-<td colspan=2 align=right><% $totaldesc %>
-<td align=right><% $totalwas |h %> total
-<%perl>
+<td colspan=3>$xinfo
+<td colspan=2 align=right>$totaldesc
+<td align=right>$totalwas total
+END
        $total_to_show= undef;
      };
-</%perl>
-%    my $show_flows= sub {
-%      my ($od,$arbitrage,$collectdeliver) = @_;
-%      my $todo= $flowlists{$od};
-%      return unless $todo;
-%      foreach my $tkey (sort keys %$todo) {
-%              my $t= $todo->{$tkey};
-%              next if $t->{"${od}Arbitrage"} != $arbitrage;
-%              $show_total_now->('');
-%              if (!$age_reported++) {
-%                      my $age= $now - $t->{Timestamp};
-%                      my $cellid= "da_${i}";
-%                      $da_ages{$cellid}= $age;
-<td colspan=2>\
-(Data age: <span id="<% $cellid %>"><% prettyprint_age($age) %></span>)
-%              } elsif (!defined $total) {
-%                      $total= 0;
-<% $tbody->(0) %>
-%              }
-%              $total += $t->{Total};
-%              my $span= 0 + keys %{ $t->{Stalls} };
-%              my $td= "td rowspan=$span";
-% tr_datarow($m,$dline);
-<<% $td %>><% $collectdeliver %>
-<<% $td %>><% $t->{'commodname'} |h %>
-<<% $td %>><% $t->{'posinclass'} %>
-%
-%              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_datarow($m,$dline);
-%                      $pstall->($stallix);
-%              }
-%
-%              $dline ^= 1;
-%      }
-%    };
-<%perl>
+     my $show_flows= sub {
+       my ($od,$arbitrage,$collectdeliver) = @_;
+       my $todo= $flowlists{$od};
+       return unless $todo;
+       foreach my $tkey (sort keys %$todo) {
+               my $t= $todo->{$tkey};
+               next if $t->{"${od}Arbitrage"} != $arbitrage;
+               $show_total_now->('');
+               if (!$age_reported++) {
+                       my $age= $now - $t->{Timestamp};
+                       @oldest= ($age,$islandnamepr) if $oldest[0] < $age;
+                       my $cellid= "da_${i}";
+                       my $agepr= prettyprint_age($age);
+                       $da_ages{$cellid}= $age;
+                       $plan_html .= <<END
+<td colspan=2>(Data age: <span id="$cellid">$agepr</span>)
+END
+               } elsif (!defined $total) {
+                       $total= 0;
+                       $plan_html .= $tbody->(0);
+               }
+               $total += $t->{Total};
+               my $span= 0 + keys %{ $t->{Stalls} };
+               my $td= "td rowspan=$span";
+               my %linkqf= (%{ $qa->{'baseqf'} }, %{ $qa->{'queryqf'} });
+               $linkqf{'query'}= 'commod';
+               $linkqf{'commodstring'}= $t->{'commodname'};
+               $linkqf{'commodid'}= $t->{'commodid'};
+               my $linkqfpr= encode_entities( $quri->(%linkqf) );
+               my $commodnamepr= encode_entities($t->{'commodname'});
+               $plan_html .= tr_datarow_s($m,$dline) . <<END;
+<$td>$collectdeliver
+<$td><a href="$linkqfpr">$commodnamepr</a>
+<$td>$t->{'posinclass'}
+END
+               my @stalls= sort keys %{ $t->{Stalls} };
+               my $pstall= sub {
+                       my $namepr= encode_entities( $stalls[$_[0]] );
+                       $plan_html .= <<END;
+<td>$namepr
+END
+               };
+
+               $pstall->(0);
+               $plan_html .= <<END;
+<$td align=right>$t->{Price} poe ea.
+<$td align=right>$t->{Qty} unit(s)
+<$td align=right>$t->{Total} total
+END
+               foreach my $stallix (1..$#stalls) {
+                       $plan_html .= tr_datarow_s($m,$dline);
+                       $pstall->($stallix);
+               }
+
+               $dline ^= 1;
+       }
+     };
 
      $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
      $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
@@ -823,23 +948,65 @@ END
      }
      $show_total_now->($totals);
 }
-</%perl><a name="summary"></a>
-<% $tbody->(1) %><tr>
-<td colspan=3>Total distance: <% $total_dist %> leagues.
+
+my $cashflowpr= $total_total < 0
+               ? -$total_total." loss"
+               : $total_total." gain";
+
+my $max_capital= 0;
+foreach my $cap (map { $_->{capital} } @sail_total) {
+       $max_capital= $cap if $cap > $max_capital;
+}
+
+$da_ages{'oldest'}= $oldest[0];
+
+$plan_html .= $tbody->(1) . <<END;
+<tr>
+<td colspan=3>Total distance: $total_dist leagues.
 <td colspan=3 align=right>Overall net cash flow
-<td align=right><strong><%
-  $total_total < 0 ? -$total_total." loss" : $total_total." gain"
- %></strong>
+<td align=right><strong>$cashflowpr</strong>
+</table>
+END
+
+</%perl>
+% if (!printable($m)) {
+<h2><a name="summary">Summary</a></h2>
+% }
+
+<table>
+<tr>
+ <td>Distance:
+ <td><strong><% $total_dist %></strong> leagues,
+     <strong><% scalar(@islandids) %></strong> island(s)
+<tr>
+ <td>Planned net cash flow:
+ <td><strong><% $cashflowpr %></strong>
+<tr>
+ <td>Expected profit on average: approx.
+ <td>
+  <strong><% sprintf "%d", $expected_total_profit %></strong> poe
+  (considering expected losses, but ignoring rum consumed)
+<tr>
+ <td>Capital required:
+ <td>
+  <strong><% $max_capital %></strong> poe or less
+<tr>
+ <td>Oldest market data used:
+ <td><strong id="oldest"><% prettyprint_age($oldest[0]) %></strong>
+     (<% $oldest[1] %>)
+<tr>
+ <td colspan=2><% $opt_how %>
 </table>
+<p>
+
+<h2><a name="plan">Voyage trading plan</a></h2>
+<% $plan_html %>
 <& query_age:dataages, id2age => \%da_ages &>
-Expected average profit:
- approx. <strong><% sprintf "%d", $expected_total_profit %></strong> poe
- (considering expected losses, but ignoring rum consumed)
 %
 % } # ========== TRADING PLAN ==========
 
 % if (!printable($m)) {
-<h2><a name="dataage">Data age summary</a></h2>
+<h2><a name="dataage">Relevant data ages</a></h2>
 <%perl>
        my $sth_i= $dbh->prepare(<<END);
                SELECT archipelago, islandid, islandname, timestamp