X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~yarrgweb/git?p=ypp-sc-tools.db-live.git;a=blobdiff_plain;f=yarrg%2Fweb%2Froutetrade;h=0da1fd80a81929454605e46df2064d56d26d3083;hp=56a1c38ff1a137ef8a2e618eb994c2af167948b5;hb=555b3391b3cd9967a29b219fff242b583137d2b8;hpb=4d67a78cf9ce16640830d627b407b97810d25bf9 diff --git a/yarrg/web/routetrade b/yarrg/web/routetrade index 56a1c38..0da1fd8 100644 --- a/yarrg/web/routetrade +++ b/yarrg/web/routetrade @@ -32,25 +32,15 @@ This Mason component is the core trade planner for a specific route. -========== TODO ========== -16:36 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 ========== - <%args> $dbh @islandids @archipelagoes $qa +$max_mass +$max_volume +$lossperleaguepct <&| script &> da_pageload= Date.now(); @@ -58,11 +48,13 @@ $qa <%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) = @_; @@ -98,10 +90,12 @@ foreach my $src_i (0..$#islandids) { 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 ]; @@ -172,6 +166,20 @@ 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 { @@ -201,9 +209,12 @@ $addcols->({ DoReverse => 1, SortColKey => 'MarginSortKey' }, 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); +} @@ -252,6 +263,14 @@ $addcols->({ DoReverse => 1 }, % } <%perl> + +my @total_massvol; + +if (!@flows) { + print 'No profitable trading opportunities were found.'; + return; +} + foreach my $f (@flows) { $f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'} @@ -259,6 +278,12 @@ foreach my $f (@flows) { $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%%", @@ -268,6 +293,8 @@ foreach my $f (@flows) { $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, @@ -344,7 +371,7 @@ die "$cmpu $uue ?" if length $cmpu > 20;

% if (@islandids<=1) { -Route is trivial. +Route contains only one location. % } % if (!$specific) { Route contains archipelago(es), not just specific islands. @@ -353,7 +380,7 @@ 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> @@ -362,9 +389,9 @@ my $cplex= " Maximize totalprofit: - ".(join " + + ".(join " ", map { - sprintf "%.20f %s", $_->{ExpectedUnitProfit}, $_->{Var} + sprintf "%+.20f %s", $_->{ExpectedUnitProfit}, $_->{Var} } @flows)." Subject To @@ -395,16 +422,50 @@ foreach my $flow (@flows) { 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; + $max= 1e9 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 "; @@ -423,32 +484,65 @@ if ($qa->{'debug'}) { qw(glpsol --cpxlp /dev/stdin -o /dev/stdout)); print "

\n" if $qa->{'debug'};
 	my $found_section= 0;
+	my $glpsol_out= '';
+	my $continuation='';
 	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;
+		if (m/^\s*No\.\s+(Row|Column) name\s+(?:St\s+)?Activity\s/) {
+			die "$_ $found_section ?" if $found_section>0;
 			$found_section= 1;
 			next;
 		}
 		next unless $found_section==1;
-		next if m/^[- ]+$/;
-		if (!/\S/) {
-			$found_section= 2;
-			next;
+		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.]+) \s
+			/x or die "$_ ?";
+		if ($varname =~ m/^f(\d+)$/) {
+			my ($ix) = $1;
+			my $flow= $flows[$ix] or die;
+			$flow->{OptQty}= $qty;
+			$flow->{OptProfit}= $flow->{'unitprofit'} * $qty;
+			$flow->{OptCapital}= $flow->{OptQty} *
+				$flow->{'org_price'};
+		} elsif ($varname =~ m/^(mass|volume)_(\d+)$/) {
+			my ($mv,$ix) = ($1,$2);
+			$total_massvol[$ix]{$mv}= $qty;
 		}
-		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'}; - 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); + map { defined $_->{OptQty} or die "$prerr $_->{Ix}" } @flows; +# map { defined +# die $prerr if grep { ! } @flows; +# map { die +# die $prerr if map { }; -$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( @@ -472,6 +566,7 @@ $addcols->({ Total => 0, DoReverse => 1 }, qw( + % if ($optimise) { % } @@ -484,6 +579,8 @@ $addcols->({ Total => 0, DoReverse => 1 }, qw( Deliver Profit Max + +Max % if ($optimise) { Planned % } @@ -502,6 +599,9 @@ $addcols->({ Total => 0, DoReverse => 1 }, qw( Qty Capital Profit +Dist +Mass +Vol % if ($optimise) { Qty Capital @@ -521,15 +621,25 @@ $addcols->({ Total => 0, DoReverse => 1 }, qw( {UidShort} %> value=""> {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; -{Text} ? '' : 'align=right' %>><% $v |h %> +{Span} ? "colspan=$spec->{Span}" : '' + %> <% $spec->{Align} + %>><% exists $spec->{String} ? $spec->{String} : $v |h %> +% $ci += $spec->{Span}; % } % } @@ -544,15 +654,10 @@ $addcols->({ Total => 0, DoReverse => 1 }, qw( % } -<& 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; - + @@ -561,69 +666,120 @@ $addcols->({ Total => 0, DoReverse => 1 }, qw( % my $iquery= $dbh->prepare('SELECT islandname FROM islands % WHERE islandid = ?'); % my %da_ages; +% my $total_total= 0; +% my $total_dist= 0; %

Voyage trading plan

- +
% foreach my $i (0..$#islandids) { - + % } -% if (%todo && !$age_reported++) { -% my $age= $now - (values %todo)[0]->{Timestamp}; -% my $cellid= "da_${i}"; -% $da_ages{$cellid}= $age; - + +% } % $total += $t->{Total}; % my $span= 0 + keys %{ $t->{Stalls} }; % my $td= "td rowspan=$span"; -<<% $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}; - - +
+
% $iquery->execute($islandids[$i]); % my ($islandname) = $iquery->fetchrow_array(); % if (!$i) { -Start at <% $islandname |h %> +Start at <% $islandname |h %> % } else { -Sail to <% $islandname |h %> -% } - -% 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; +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, $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; + +
+<% $xinfo %> +<% $totaldesc %> +<% $totalwas |h %> total +<%perl> + $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}; +% my $cellid= "da_${i}"; +% $da_ages{$cellid}= $age; +\ (Data age: <% prettyprint_age($age) %>) -% } -% my $total= 0; -% my $dline= 0; -% foreach my $tkey (sort keys %todo) { -% my $t= $todo{$tkey}; +% } elsif (!defined $total) { +% $total= 0; +
<% $avail |h %> <% $od eq 'org' ? 'avail.' : 'wanted' %> <% $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 % @@ -634,13 +790,38 @@ Sail to <% $islandname |h %> % % $dline ^= 1; % } -% if (%todo) { -
<% $od eq 'org' ? 'Outlay' : 'Proceeds' %> -<% $total |h %> total -% } -% } -% } +% }; +<%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); + my $totals= ''; + if ($i < $#islandids) { + $totals .= "In hold $total_massvol[$i]{mass} kg,". + " $total_massvol[$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->($max_mass, $total_massvol[$i]{mass}, 'kg'); + $domv->($max_volume, $total_massvol[$i]{volume}, 'l'); + $totals .= ".\n"; + } + $show_total_now->($totals); +} + +
Total distance: <% $total_dist %> leagues. +Overall net cash flow +<% + $total_total < 0 ? -$total_total." loss" : $total_total." gain" + %>
<& query_age:dataages, id2age => \%da_ages &> %