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
@islandids
@archipelagoes
$qa
+$max_mass
+$max_volume
+$lossperleaguepct
</%args>
<&| script &>
da_pageload= Date.now();
<%perl>
+my $loss_per_league= defined $lossperleaguepct ? $lossperleaguepct*0.01 : 1e-7;
+
my $now= time;
-my $loss_per_league= 1e-7;
my @flow_conds;
my @query_params;
+my %dists;
my $sd_condition= sub {
my ($bs, $ix) = @_;
if ($specific && !$confusing &&
# With a circular route, do not carry goods round the loop
- !($src_i==0 && $dst_i==$#islandids &&
+ !(($src_i==0 || $src_i==$#islandids) &&
+ $dst_i==$#islandids &&
$src_isle == $islandids[$dst_i])) {
if ($islandpair{$src_isle,$dst_isle}) {
$confusing= 1;
+print "confusing $src_i $src_isle $dst_i $dst_isle\n";
} else {
$islandpair{$src_isle,$dst_isle}=
[ $src_i, $dst_i ];
$sth->execute(@query_params);
my @flows;
+my $distquery= $dbh->prepare("
+ SELECT dist FROM dists WHERE aiid = ? AND biid = ?
+ ");
+my $distance= sub {
+ my ($from,$to)= @_;
+ my $d= $dists{$from}{$to};
+ return $d if defined $d;
+ $distquery->execute($from,$to);
+ $d = $distquery->fetchrow_array();
+ defined $d or die "$from $to ?";
+ $dists{$from}{$to}= $d;
+ return $d;
+};
+
my @cols= ({ NoSort => 1 });
my $addcols= sub {
qw( Margin
));
$addcols->({ DoReverse => 1 },
- qw( unitprofit MaxQty
- MaxCapital MaxProfit
+ qw( unitprofit MaxQty MaxCapital MaxProfit dist
));
+foreach my $v (qw(MaxMass MaxVolume)) {
+ $addcols->({
+ DoReverse => 1, Total => 0, SortColKey => "${v}SortKey" }, $v);
+}
</%perl>
% }
<%perl>
+
+if (!@flows) {
+ print 'No profitable trading opportunities were found.';
+ return;
+}
+
foreach my $f (@flows) {
$f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
$f->{MaxProfit}= $f->{MaxQty} * $f->{'unitprofit'};
$f->{MaxCapital}= $f->{MaxQty} * $f->{'org_price'};
+ $f->{MaxMassSortKey}= $f->{MaxQty} * $f->{'unitmass'};
+ $f->{MaxVolumeSortKey}= $f->{MaxQty} * $f->{'unitvolume'};
+ foreach my $v (qw(Mass Volume)) {
+ $f->{"Max$v"}= sprintf "%.1f", $f->{"Max${v}SortKey"} * 1e-6;
+ }
+
+ $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'};
+ $dists{'org_id'}{'dst_id'}= $f->{'dist'};
+
my @uid= $f->{commodid};
foreach my $od (qw(org dst)) {
push @uid,
<p>
% if (@islandids<=1) {
-Route is trivial.
+Route contains only one location.
% }
% if (!$specific) {
Route contains archipelago(es), not just specific islands.
Route is complex - it visits the same island several times
and isn't a simple loop.
% }
-Therefore, optimal trade pattern not calculated.
+Therefore, optimal voyage trade plan not calculated.
% } else { # ========== OPTMISATION ==========
<%perl>
Maximize
totalprofit:
- ".(join " +
+ ".(join "
", map {
- sprintf "%.20f %s", $_->{ExpectedUnitProfit}, $_->{Var}
+ sprintf "%+.20f %s", $_->{ExpectedUnitProfit}, $_->{Var}
} @flows)."
Subject To
foreach my $cstname (sort keys %avail_csts) {
my $c= $avail_csts{$cstname};
$cplex .= "
- ". sprintf("%-30s","$cstname:")." ".
+ ". sprintf("%-30s","$cstname:")." ".
join("+", @{ $c->{Flows} }).
" <= ".$c->{Qty}."\n";
}
+foreach my $ci (0..($#islandids-1)) {
+ my @rel_flows;
+ foreach my $f (@flows) {
+ next if $f->{Suppress};
+ next if $f->{'org_id'} == $f->{'dst_id'};
+ next unless grep { $f->{'org_id'} == $_ }
+ @islandids[0..$ci];
+ next unless grep { $f->{'dst_id'} == $_ }
+ @islandids[$ci+1..@islandids-1];
+ push @rel_flows, $f;
+print " RELEVANT $ci $f->{Ix} ";
+ }
+print " RELEVANT $ci COUNT ".scalar(@rel_flows)." ";
+ next unless @rel_flows;
+ foreach my $mv (qw(mass volume)) {
+ my $max_vn= "max_$mv";
+ my $max= $mv eq 'mass' ? $max_mass : $max_volume;
+ next unless defined $max;
+print " DEFINED MAX $mv $max ";
+ $cplex .= "
+ ". sprintf("%-10s","${mv}_$ci:")." ".
+ join(" + ", map { ($_->{"unit$mv"}*1e-3).' f'.$_->{Ix} } @rel_flows).
+ " <= $max";
+ }
+ $cplex.= "\n";
+}
+
$cplex.= "
Bounds
".(join "
", map { "$_->{Var} >= 0" } @flows)."
+";
+
+$cplex.= "
+Integer
+ ".(join "
+ ", map { "f$_" } (0..$#flows))."
+
End
";
qw(glpsol --cpxlp /dev/stdin -o /dev/stdout));
print "<pre>\n" if $qa->{'debug'};
my $found_section= 0;
+ my $glpsol_out= '';
while (<$output>) {
+ $glpsol_out.= $_;
print encode_entities($_) if $qa->{'debug'};
- if (m/^\s*No\.\s+Column name\s+St\s+Activity\s/) {
+ if (m/^\s*No\.\s+Column name\s+(?:St\s+)?Activity\s/) {
die if $found_section>0;
$found_section= 1;
next;
$flow->{OptCapital}= $flow->{OptQty} * $flow->{'org_price'};
}
print "</pre>\n" if $qa->{'debug'};
- pipethrough_run_finish($output, 'glpsol');
- die unless $found_section;
+ my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n ";
+ pipethrough_run_finish($output,$prerr);
+ die $prerr unless $found_section;
};
-$addcols->({ DoReverse => 1 }, qw(
+$addcols->({ DoReverse => 1, Special => sub {
+ my ($flow,$col,$v,$spec) = @_;
+ if ($flow->{ExpectedUnitProfit} < 0) {
+ $spec->{Span}= 3;
+ $spec->{String}= '(Small margin)';
+ $spec->{Align}= 'align=center';
+ }
+} }, qw(
OptQty
));
$addcols->({ Total => 0, DoReverse => 1 }, qw(
<colgroup span=2>
<colgroup span=2>
<colgroup span=3>
+<colgroup span=3>
% if ($optimise) {
<colgroup span=3>
% }
<th colspan=2>Deliver
<th colspan=2>Profit
<th colspan=3>Max
+<th colspan=1>
+<th colspan=2>Max
% if ($optimise) {
<th colspan=3>Planned
% }
<th>Qty
<th>Capital
<th>Profit
+<th>Dist
+<th>Mass
+<th>Vol
% if ($optimise) {
<th>Qty
<th>Capital
<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 $ci= 1;
+% while ($ci < @cols) {
% my $col= $cols[$ci];
+% my $spec= {
+% Span => 1,
+% Align => ($col->{Text} ? '' : 'align=right')
+% };
% my $v= $flow->{$col->{Name}};
-% $col->{Total} += $v if defined $col->{Total};
+% if ($col->{Special}) { $col->{Special}($flow,$col,$v,$spec); }
+% $col->{Total} += $v
+% if defined $col->{Total} and not $flow->{Suppress};
% $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 %>
+<td <% $spec->{Span} ? "colspan=$spec->{Span}" : ''
+ %> <% $spec->{Align}
+ %>><% exists $spec->{String} ? $spec->{String} : $v |h %>
+% $ci += $spec->{Span};
% }
% }
<tr id="trades_total">
% }
</table>
-<& tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
+<&| 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>
+</&tabsort>
<input type=submit name=update value="Update">
% my $iquery= $dbh->prepare('SELECT islandname FROM islands
% WHERE islandid = ?');
% my %da_ages;
+% my $total_total= 0;
+% my $total_dist= 0;
%
<h1>Voyage trading plan</h1>
-<table>
+<table rules=groups>
% foreach my $i (0..$#islandids) {
-<tr><td colspan=3><strong>
+<tbody>
+<tr><td colspan=3>
% $iquery->execute($islandids[$i]);
% my ($islandname) = $iquery->fetchrow_array();
% if (!$i) {
-Start at <% $islandname |h %>
+<strong>Start at <% $islandname |h %></strong>
% } 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"};
+% my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
+% $total_dist += $this_dist;
+<strong>Sail to <% $islandname |h %></strong>
+- <% $this_dist |h %> leagues </td>
% }
-% if (%todo && !$age_reported++) {
-% my $age= $now - (values %todo)[0]->{Timestamp};
-% my $cellid= "da_${i}";
-% $da_ages{$cellid}= $age;
-<td colspan=2>\
+<%perl>
+ my $age_reported= 0;
+ my %flowlists;
+ foreach my $od (qw(org dst)) {
+ foreach my $f (@flows) {
+ next if $f->{Suppress};
+ next unless $f->{"${od}_id"} == $islandids[$i];
+ next unless $f->{OptQty};
+ my $arbitrage= $f->{'org_id'} == $f->{'dst_id'};
+ my $loop= $islandids[0] == $islandids[-1] &&
+ ($i==0 || $i==$#islandids);
+ next if $loop and ($arbitrage ? $i :
+ !!$i == !!($od eq 'org'));
+ my $price= $f->{"${od}_price"};
+ my $stallname= $f->{"${od}_stallname"};
+ my $todo= \$flowlists{$od}{
+ $f->{'commodname'},
+ (sprintf "%07d", ($od eq 'dst' ?
+ 9999999-$price : $price)),
+ $stallname
+ };
+ $$todo= {
+ Qty => 0,
+ orgArbitrage => 0,
+ dstArbitrage => 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"};
+ $$todo->{"${od}Arbitrage"}= 1 if $arbitrage;
+ }
+ }
+
+ my $total;
+ my $dline= 0;
+ my $show_flows= sub {
+ my ($od,$arbitrage,$collectdeliver) = @_;
+</%perl>
+%
+% my $todo= $flowlists{$od};
+% return unless $todo;
+% foreach my $tkey (sort keys %$todo) {
+% my $t= $todo->{$tkey};
+% next if $t->{"${od}Arbitrage"} != $arbitrage;
+% if (!$age_reported++) {
+% my $age= $now - $t->{Timestamp};
+% my $cellid= "da_${i}";
+% $da_ages{$cellid}= $age;
+<td colspan=3>\
(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};
+% } elsif (!defined $total) {
+% $total= 0;
+<tbody>
+% }
% $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 %>><% $collectdeliver %>
<<% $td %>><% $t->{'commodname'} |h %>
%
% my @stalls= sort keys %{ $t->{Stalls} };
% my $pstall= sub {
% my $name= $stalls[$_[0]];
-% my $avail= $t->{Stalls}{$name};
-<td align=right><% $avail |h %> <% $od eq 'org' ? 'avail.' : 'wanted' %>
<td><% $name |h %>
% };
%
% $pstall->(0);
-<<% $td %> align=right><% $t->{Price} |h %> each
+<<% $td %> align=right><% $t->{Price} |h %> poe ea.
<<% $td %> align=right><% $t->{Qty} |h %> unit(s)
<<% $td %> align=right><% $t->{Total} |h %> total
%
%
% $dline ^= 1;
% }
-% if (%todo) {
+% };
+% my $show_total= sub {
+% my ($totaldesc, $sign)= @_;
+% if (defined $total) {
<tr>
-<td colspan=5><td align=right><% $od eq 'org' ? 'Outlay' : 'Proceeds' %>
+<td colspan=3>
+<td colspan=2 align=right><% $totaldesc %>
<td align=right><% $total |h %> total
+% $total_total += $sign * $total;
% }
-% }
-% }
+% $total= undef;
+% $dline= 0;
+<%perl>
+ };
+
+ $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
+ $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
+ $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1);
+ $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1);
+
+}
+</%perl>
+<tbody><tr>
+<td colspan=2>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>
</table>
<& query_age:dataages, id2age => \%da_ages &>
%