<%doc> This is part of the YARRG website. YARRG is a tool and website for assisting players of Yohoho Puzzle Pirates. Copyright (C) 2009 Ian Jackson Copyright (C) 2009 Clare Boothby YARRG's client code etc. is covered by the ordinary GNU GPL (v3 or later). The YARRG website is covered by the GNU Affero GPL v3 or later, which basically means that every installation of the website will let you download the source. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Yohoho and Puzzle Pirates are probably trademarks of Three Rings and are used without permission. This program is not endorsed or sponsored by Three Rings. This Mason component is the core trade planner for a specific route. <%args> $dbh @islandids @archipelagoes $qa $routeparams $reset_suppressions $quri <& query_age:pageload &> <%perl> 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; my @flow_conds; my @query_params; my %dists; my $expected_total_profit; my $sd_condition= sub { my ($bs, $ix) = @_; my $islandid= $islandids[$ix]; if (defined $islandid) { return "${bs}.islandid = $islandid"; } else { push @query_params, $archipelagoes[$ix]; return "${bs}_islands.archipelago = ?"; } }; my $specific= !grep { !defined $_ } @islandids; my %ipair2subflowinfs; # $ipair2subflowinfs{$orgi,$dsti}= [ [$orgix,$distix], ... ] my @subflows; # $subflows[0]{Flow} = { ... } # $subflows[0]{Org} = $orgix # $subflows[0]{Dst} = $dstix foreach my $org_i (0..$#islandids) { my $org_isle= $islandids[$org_i]; my $org_cond= $sd_condition->('sell',$org_i); my @dst_conds; foreach my $dst_i ($org_i..$#islandids) { my $dst_isle= $islandids[$dst_i]; # Don't ever consider sailing things round the houses: next if defined $dst_isle and grep { $dst_isle == $_ } @islandids[$org_i..$dst_i-1]; next if defined $org_isle and grep { $org_isle == $_ } @islandids[$org_i+1..$dst_i]; my $dst_cond; if ($dst_i==$org_i and !defined $org_isle) { # we always want arbitrage, but mentioning an arch # once shouldn't produce intra-arch trades $dst_cond= "sell.islandid = buy.islandid"; } else { $dst_cond= $sd_condition->('buy',$dst_i); } push @dst_conds, $dst_cond; if ($specific) { push @{ $ipair2subflowinfs{$org_isle,$dst_isle} }, [ $org_i, $dst_i ]; } } push @flow_conds, "$org_cond AND ( ".join(" OR ",@dst_conds)." )"; } my $stmt= " SELECT sell_islands.islandname org_name, sell_islands.islandid org_id, sell.price org_price, 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.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.unitmass unitmass, commods.unitvolume unitvolume, commods.ordval ordval, commods.posinclass posinclass, commods.commodclassid commodclassid, commods.flags flags, dist dist, buy.price - sell.price unitprofit FROM commods 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 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 $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 { 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 dist )); foreach my $v (qw(MaxMass MaxVolume)) { $addcols->({ DoReverse => 1, Total => 0, SortColKey => "${v}SortKey" }, $v); } % if ($qa->{'debug'}) {
<% $stmt |h %>
<% join(' | ',@query_params) |h %>
% } <& dumptable:start, qa => $qa, sth => $sth &> % { % 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), %$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"} ; } <& dumptable:row, qa => $qa, sth => $sth, row => $f &> % } <& dumptable:end, qa => $qa &> % } % if (@islandids==1) { % if (defined $islandids[0]) { Searched for arbitrage trades only. % } else { Searched for arbitrage trades only, in <% $archipelagoes[0] |h %> [?]. % } % } <%perl> 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'} ? $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'}; $opportunity_value{ $oppo_key->($f) } += $f->{MaxProfit}; 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 ?"; } } foreach my $f (@flows) { 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= { Flow => $f, Org => $sfi->[0], Dst => $sfi->[1], Var => sprintf "f%ss%s_c%d_p%d_%d_p%d_%d", $f->{Ix}, $sfi->[0], $f->{'commodid'}, $sfi->[0], $f->{'org_price'}, $sfi->[1], $f->{'dst_price'} }; push @{ $f->{Subflows} }, $subflow; push @subflows, $subflow; } } } % my $optimise= 1; % my $opt_how; % 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) {

Therefore, optimal voyage trade plan not calculated. % } else { # ========== OPTMISATION ========== <%perl> my $cplex= " 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 .= " Subject To "; my %avail_lims; foreach my $flow (@flows) { next if $flow->{Suppress}; foreach my $od (qw(org dst)) { my $limname= join '_', ( $od, 'i'.$flow->{"${od}_id"}, 'c'.$flow->{'commodid'}, $flow->{"${od}_price"}, $flow->{"${od}_stallid"}, ); push @{ $avail_lims{$limname}{SubflowVars} }, map { $_->{Var} } @{ $flow->{Subflows} }; $avail_lims{$limname}{Qty}= $flow->{"${od}_qty_agg"}; } } foreach my $limname (sort keys %avail_lims) { my $c= $avail_lims{$limname}; $cplex .= sprintf(" %-30s","$limname:")." ". join("+", @{ $c->{SubflowVars} }). " <= ".$c->{Qty}."\n"; } foreach my $ci (0..($#islandids-1)) { my @rel_subflows; foreach my $f (@flows) { next if $f->{Suppress}; my @relsubflow= grep { $_->{Org} <= $ci && $_->{Dst} > $ci; } @{ $f->{Subflows} }; next unless @relsubflow; die unless @relsubflow == 1; push @rel_subflows, @relsubflow; #print " RELEVANT $ci $relsubflow[0]->{Var} "; } #print " RELEVANT $ci COUNT ".scalar(@rel_subflows)." "; if (!@rel_subflows) { foreach my $mv (qw(mass volume capital)) { $sail_total[$ci]{$mv}= 0; } next; } my $applylimit= sub { my ($mv, $f2val) = @_; my $max= $routeparams->{"Max".ucfirst $mv}; $max= 1e9 unless defined $max; #print " DEFINED MAX $mv $max "; $cplex .= " ". sprintf("%-10s","${mv}_$ci:")." ". join(" + ", map { #print " PART MAX $_->{Var} $_->{Flow}{Ix} "; $f2val->($_->{Flow}) .' '. $_->{Var}; } @rel_subflows). " <= $max"; }; $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(< $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"; } $cplex.= " Bounds ".(join " ", map { "$_->{Var} >= 0" } @subflows)." "; $cplex.= " Integer ".(join " ", map { $_->{Var} } @subflows)." End "; if ($qa->{'debug'}) {

<% $cplex |h %>
<%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 5 --memlim 20), @opts, qw( --cpxlp /dev/stdin -o /dev/stdout)); if ($qa->{'debug'}) { print "

@opts

\n
\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'};
		if (m/^\s*No\.\s+(Row|Column) name\s+(?:St\s+)?Activity\s/) {
			die "$_ $found_section ?" if $found_section>0;
			$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;
		}
		next unless $found_section==1;
		if (!length $continuation) {
			next if !$continuation &&  m/^[- ]+$/;
			if (!/\S/) {
				$found_section= 0;
				next;
			}
			if (m/^ \s* \d+ \s+ \w+ $/x) {
				$continuation= $&;
				next;
			}
		}
		$_= $continuation.$_;
		$continuation= '';
		my ($varname, $qty) = m/^
			\s* \d+ \s+
			(\w+) \s+ (?: [A-Z*]+ \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);
			my $flow= $flows[$ix] or die;
			my @relsubflow= grep { $_->{Org} == $orgix }
				@{ $flow->{Subflows} };
			die "$ix $orgix @relsubflow" unless @relsubflow == 1;
			my $sf= $relsubflow[0];
			$sf->{OptQty}= $qty;
			$sf->{OptProfit}= $qty * $flow->{'unitprofit'};
			$sf->{OptCapital}= $qty * $flow->{'org_price'};
		} elsif ($varname =~ m/^(mass|volume|capital)_(\d+)$/) {
			my ($mv,$ix) = ($1,$2);
			$sail_total[$ix]{$mv}= $qty;
		}
	}
	print "
\n" if $qa->{'debug'}; my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n "; 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->('Complex problem, downgraded'. ' to rounded-down LP.', qw( --nomip ))) {

Optimisation failed

The linear/mixed-integer optimisation failed. Please report this problem.
<% $cplex |h %>
<%perl> return; } $addcols->({ DoReverse => 1, TotalSubflows => 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, TotalSubflows => 1 }, qw( OptCapital OptProfit )); % } # ========== OPTIMISATION ========== % if (!printable($m)) {

Contents

% } else { % my @tl= gmtime $now or die $!;

Generated by YARRG at <% sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC", $tl[5]+1900, @tl[4,3,2,1,0] |h %>. % } % if ($optimise) { # ========== TRADING PLAN ========== <%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 my $tbody= sub { if (!printable($m)) { return ''; } my ($c)= qw(40 00)[$_[0]]; return ""; }; foreach my $i (0..$#islandids) { $plan_html .= $tbody->(1); $plan_html .= "\n"; $iquery->execute($islandids[$i]); my ($islandnamepr)= encode_entities( $iquery->fetchrow_array() ); if (!$i) { $plan_html .= < Start at $islandnamepr [what are these codes?] END } else { my $this_dist= $distance->($islandids[$i-1],$islandids[$i]); $total_dist += $this_dist; $plan_html .= < 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'}; } $plan_html .= <Sail to $islandnamepr - $this_dist leagues, $total_value poe at risk END } my $age_reported= 0; my %flowlists; #print "" if $qa->{'debug'}; foreach my $od (qw(org dst)) { #print " [[ i $i od $od " if $qa->{'debug'}; foreach my $sf (@subflows) { my $f= $sf->{Flow}; next unless $sf->{ucfirst $od} == $i; #print " FLOW $f->{Ix} SUB #$sf->{Org}..$sf->{Dst} " # if $qa->{'debug'}; next unless $sf->{OptQty}; my $arbitrage= $f->{'org_id'} == $f->{'dst_id'}; die if $arbitrage and $sf->{Org} != $sf->{Dst}; my $price= $f->{"${od}_price"}; my $stallname= $f->{"${od}_stallname"}; my $todo= \$flowlists{$od}{ (sprintf "%010d", $f->{'ordval'}), $f->{'commodname'}, (sprintf "%07d", ($od eq 'dst' ? 9999999-$price : $price)), $stallname }; $$todo= { Qty => 0, orgArbitrage => 0, dstArbitrage => 0, } unless $$todo; $$todo->{'commodid'}= $f->{'commodid'}; $$todo->{'commodname'}= $f->{'commodname'}; $$todo->{'posinclass'}= ''; my $incl= $f->{'posinclass'}; my $findclass= $dbh->prepare(<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'}; $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]." "; } } $$todo->{'posinclass'}= "

" .$abbrev."
"; } $$todo->{'stallname'}= $stallname; $$todo->{Price}= $price; $$todo->{Timestamp}= $f->{"${od}_timestamp"}; $$todo->{Qty} += $sf->{OptQty}; $$todo->{Total}= $$todo->{Price} * $$todo->{Qty}; $$todo->{Stalls}= $f->{"${od}Stalls"}; $$todo->{"${od}Arbitrage"}= 1 if $arbitrage; } #print "]] " if $qa->{'debug'}; } #print "" if $qa->{'debug'}; my ($total, $total_to_show); my $dline= 0; my $show_total= sub { my ($totaldesc, $sign) = @_; if (defined $total) { die if defined $total_to_show; $total_total += $sign * $total; $total_to_show= [ $totaldesc, $total ]; $total= undef; } $dline= 0; }; my $show_total_now= sub { my ($xinfo) = @_; return unless defined $total_to_show; my ($totaldesc,$totalwas) = @$total_to_show; $plan_html .= < $xinfo $totaldesc $totalwas total END $total_to_show= undef; }; 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 .= <(Data age: $agepr) 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) . <$collectdeliver <$td>$commodnamepr <$td>$t->{'posinclass'} END my @stalls= sort keys %{ $t->{Stalls} }; my $pstall= sub { my $namepr= encode_entities( $stalls[$_[0]] ); $plan_html .= <$namepr END }; $pstall->(0); $plan_html .= <$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); $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1); $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1); my $totals= ''; if ($i < $#islandids) { $totals .= "In hold $sail_total[$i]{mass}kg,". " $sail_total[$i]{volume} l"; my $delim= '; spare '; my $domv= sub { my ($max, $got, $units) = @_; return unless defined $max; $totals .= $delim; $totals .= sprintf "%g %s", ($max-$got), $units; $delim= ', '; }; $domv->($routeparams->{MaxMass}, $sail_total[$i]{mass}, 'kg'); $domv->($routeparams->{MaxVolume}, $sail_total[$i]{volume}, 'l'); $totals .= ".\n"; } $show_total_now->($totals); } 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) . < Total distance: $total_dist leagues. Overall net cash flow $cashflowpr END % if (!printable($m)) {

Summary

% }
Distance: <% $total_dist %> leagues, <% scalar(@islandids) %> island(s)
Planned net cash flow: <% $cashflowpr %>
Expected profit on average: approx. <% sprintf "%d", $expected_total_profit %> poe (considering expected losses, but ignoring rum consumed)
Capital required: <% $max_capital %> poe or less
Oldest market data used: <% prettyprint_age($oldest[0]) %> (<% $oldest[1] %>)
<% $opt_how %>

Voyage trading plan

<% $plan_html %> <& query_age:dataages, id2age => \%da_ages &> % % } # ========== TRADING PLAN ========== % if (!printable($m)) {

Relevant data ages

<%perl> my $sth_i= $dbh->prepare(<prepare(<fetchrow_hashref(); if ($row) { next if $idone{$row->{'islandid'}}++; return $row; } } return undef if $ix < 0; my $iid= $islandids[$ix]; if (defined $iid) { $sth_i->execute($iid); $sth_current= $sth_i; } else { my $arch= $archipelagoes[$ix]; die unless defined $arch && length $arch; $sth_a->execute($arch); $sth_current= $sth_a; } $ix--; } }; <&| query_age:agestable, now => $now, fetchrow => $fetchrow &> Islands shown in reverse order of visits.
% } % if (!printable($m)) { % my %ts_sortkeys; % { % my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : ''; % my $cdstall= $qa->{ShowStalls} ? 'Stall' : '';

Relevant trades

<% $qa->{ShowStalls} ? '' : '' %> % if ($optimise) { % } % foreach my $col (@cols) {
>Collect >Deliver Collect Deliver Profit Max Max % if ($optimise) { Planned % }
Island <% $cdstall %> Island <% $cdstall %> Commodity Price Qty Price Qty Margin Unit Qty Capital Profit Dist Mass Vol % if ($optimise) { Qty Capital Profit % } % }
% } % foreach my $flowix (0..$#flows) { % my $flow= $flows[$flowix]; % my $rowid= "id_row_$flow->{UidShort}";
{UidShort} %> value=""> {UidShort} %> value="" <% $flow->{Suppress} ? '' : 'checked' %> > % my $ci= 1; % while ($ci < @cols) { % my $col= $cols[$ci]; % my $spec= { % Span => 1, % Align => ($col->{Text} ? '' : 'align=right') % }; % my $cn= $col->{Name}; % my $v; % if (!$col->{TotalSubflows}) { % $v= $flow->{$cn}; % } else { % $v= 0; % $v += $_->{$cn} foreach @{ $flow->{Subflows} }; % } % 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; {Span} ? "colspan=$spec->{Span}" : '' %> <% $spec->{Align} %>><% exists $spec->{String} ? $spec->{String} : $v |h %> % $ci += $spec->{Span}; % } % }
Total % foreach my $ci (3..$#cols) { % my $col= $cols[$ci]; % if (defined $col->{Total}) { <% $col->{Total} |h %> % } % }
<&| tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow', throw => 'trades_sort', tbrow => 'trades_total' &> ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;

% } # !printable <%init> use CommodsWeb; use Commods;