X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~yarrgweb/git?a=blobdiff_plain;f=yarrg%2Fweb%2Froutetrade;h=397e3854372787170afe7e1861de55150aa6495a;hb=e088a29c1c037880074b5893831e6701273396d6;hp=407cc8c5e39b48bb28ac3a6466106e0c0c11afa6;hpb=0a273ee819a0c5807ed112051da10c19dfa09f70;p=ypp-sc-tools.db-live.git diff --git a/yarrg/web/routetrade b/yarrg/web/routetrade index 407cc8c..397e385 100644 --- a/yarrg/web/routetrade +++ b/yarrg/web/routetrade @@ -38,11 +38,23 @@ $dbh @islandids @archipelagoes $qa +$max_mass +$max_volume +$lossperleaguepct %args> +<&| script &> + da_pageload= Date.now(); +&script> + <%perl> +my $loss_per_league= defined $lossperleaguepct ? $lossperleaguepct*0.01 : 1e-7; + +my $now= time; + my @flow_conds; my @query_params; +my %dists; my $sd_condition= sub { my ($bs, $ix) = @_; @@ -55,18 +67,40 @@ 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) { + 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 $dst_isle= $islandids[$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; + + if ($specific && !$confusing && + # With a circular route, do not carry goods round the loop + !(($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 ]; + } + } } push @flow_conds, "$src_cond AND ( ".join(" @@ -75,36 +109,115 @@ foreach my $src_i (0..$#islandids) { } my $stmt= " - SELECT commods.commodname commodname, - commods.commodid commodid, - commods.unitmass mass, - commods.unitvolume volume, - sell_islands.islandid org_id, - sell_islands.islandname org_name, - sell.price org_price, - sum(sell.qty) org_qty, - buy_islands.islandid dst_id, - buy_islands.islandname dst_name, - buy.price dst_price, - sum(buy.qty) dst_qty + 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, + dist dist, + buy.price - sell.price unitprofit 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 - GROUP BY commodname, commods.commodid, - org_id, org_price, dst_id, dst_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); +} %perl> + % if ($qa->{'debug'}) {
<% $stmt |h %> @@ -112,8 +225,557 @@ $sth->execute(@query_params);% } -<& dumpqueryresults, sth =>$sth &> +<& 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), + 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> + +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->{'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, + $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) { + +
+% if (@islandids<=1) { +Route contains only one location. +% } +% 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 voyage trade plan not calculated. + +% } else { # ========== OPTMISATION ========== +<%perl> + +my $cplex= " +Maximize + + totalprofit: + ".(join " + ", map { + sprintf "%+.20f %s", $_->{ExpectedUnitProfit}, $_->{Var} + } @flows)." + +Subject To +"; + +my %avail_csts; +foreach my $flow (@flows) { + if ($flow->{Suppress}) { + $cplex .= " + $flow->{Var} = 0 +"; + next; + } + foreach my $od (qw(org dst)) { + my $cstname= join '_', ( + 'avail', + $flow->{'commodid'}, + $od, + $flow->{"${od}_id"}, + $flow->{"${od}_price"}, + $flow->{"${od}_stallid"}, + ); + + push @{ $avail_csts{$cstname}{Flows} }, $flow->{Var}; + $avail_csts{$cstname}{Qty}= $flow->{"${od}_qty_agg"}; + } +} +foreach my $cstname (sort keys %avail_csts) { + my $c= $avail_csts{$cstname}; + $cplex .= " + ". 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 +"; + +if ($qa->{'debug'}) { +%perl> +
+<% $cplex |h %> ++<%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 "
\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/) { + 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 "\n" if $qa->{'debug'}; + my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n "; + pipethrough_run_finish($output,$prerr); + die $prerr unless $found_section; +}; + +$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( + OptCapital OptProfit + )); + +%perl> + +% } # ========== OPTIMISATION ========== + +% my %ts_sortkeys; +% { +% my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : ''; +% my $cdstall= $qa->{ShowStalls} ? '
+ | >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 $v= $flow->{$col->{Name}}; +% 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 %> +% } +% } + |
+% $iquery->execute($islandids[$i]); +% my ($islandname) = $iquery->fetchrow_array(); +% if (!$i) { +Start at <% $islandname |h %> +% } else { +% my $this_dist= $distance->($islandids[$i-1],$islandids[$i]); +% $total_dist += $this_dist; +Sail to <% $islandname |h %> +- <% $this_dist |h %> leagues | +% } +<%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; +\ +(Data age: <% prettyprint_age($age) %>) +% } elsif (!defined $total) { +% $total= 0; + | ||||
<% $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) { + | |||||
+ | <% $totaldesc %> + | <% $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> + | |||
Total distance: <% $total_dist %> leagues. + | Overall net cash flow + | <% + $total_total < 0 ? -$total_total." loss" : $total_total." gain" + %> + |