implement scorer
This commit is contained in:
@@ -3,6 +3,7 @@ package ru.trader.analysis;
|
||||
import ru.trader.core.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class FilteredMarket {
|
||||
@@ -37,8 +38,13 @@ public class FilteredMarket {
|
||||
}
|
||||
|
||||
public Stream<Offer> getOffers(OFFER_TYPE offerType, Item item){
|
||||
return market.getStat(offerType, item).getOffers().stream()
|
||||
.filter(o -> !filter.isFiltered(o.getVendor(), true));
|
||||
NavigableSet<Offer> offers = market.getStat(offerType, item).getOffers();
|
||||
Stream<Offer> res;
|
||||
if (offerType.getOrder() > 0)
|
||||
res = offers.stream();
|
||||
else
|
||||
res = offers.descendingSet().stream();
|
||||
return res.filter(o -> !filter.isFiltered(o.getVendor(), true));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package ru.trader.analysis;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import ru.trader.core.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class Scorer {
|
||||
@@ -11,9 +13,11 @@ public class Scorer {
|
||||
private final FilteredMarket market;
|
||||
private final Profile profile;
|
||||
|
||||
private int ordersCount = 5;
|
||||
private final double avgProfit;
|
||||
private final double avgDistance;
|
||||
|
||||
private double avgProfit;
|
||||
private int ordersCount = 5;
|
||||
private double distanceRate = 1;
|
||||
|
||||
public Scorer(FilteredMarket market, Profile profile) {
|
||||
this.market = market;
|
||||
@@ -22,6 +26,7 @@ public class Scorer {
|
||||
buyOffers = new HashMap<>(100, 0.9f);
|
||||
market.getItems().parallelStream().forEach(this::fillOffers);
|
||||
avgProfit = computeAvgProfit();
|
||||
avgDistance = computeAvgDistance();
|
||||
}
|
||||
|
||||
private void fillOffers(Item item){
|
||||
@@ -35,22 +40,33 @@ public class Scorer {
|
||||
}
|
||||
}
|
||||
|
||||
public void setOrdersCount(int ordersCount) {
|
||||
this.ordersCount = ordersCount;
|
||||
}
|
||||
|
||||
private double computeAvgProfit(){
|
||||
OptionalDouble avg = sellOffers.values().stream()
|
||||
.flatMap(this::mapToOrder)
|
||||
.mapToDouble(Order::getProfit)
|
||||
.mapToDouble(o -> o.getProfit() / profile.getShip().getCargo())
|
||||
.average();
|
||||
return avg.orElse(0);
|
||||
}
|
||||
|
||||
private double computeAvgDistance(){
|
||||
OptionalDouble res = market.getVendors().mapToDouble(Vendor::getDistance).average();
|
||||
return res.orElse(0);
|
||||
}
|
||||
|
||||
public void setOrdersCount(int ordersCount) {
|
||||
this.ordersCount = ordersCount;
|
||||
}
|
||||
|
||||
public void setDistanceRate(double distanceRate) {
|
||||
this.distanceRate = distanceRate;
|
||||
}
|
||||
|
||||
public double getAvgProfit() {
|
||||
return avgProfit;
|
||||
}
|
||||
|
||||
public Score getScore(Vendor vendor){
|
||||
Stream<Order> sellOrders = vendor.getAllSellOffers().stream().flatMap(this::mapToOrder);
|
||||
Stream<Order> buyOrders = vendor.getAllBuyOffers().stream().flatMap(this::mapToOrder);
|
||||
return new Score(sellOrders, buyOrders);
|
||||
return new Score(vendor);
|
||||
}
|
||||
|
||||
private Stream<Order> mapToOrder(Offer offer) {
|
||||
@@ -69,33 +85,59 @@ public class Scorer {
|
||||
return Stream.of(order);
|
||||
}
|
||||
|
||||
public class Score {
|
||||
private double sellProfit;
|
||||
private double buyProfit;
|
||||
public class Score implements Comparable<Score> {
|
||||
private final Vendor vendor;
|
||||
private final DoubleSummaryStatistics sellStat;
|
||||
private final DoubleSummaryStatistics buyStat;
|
||||
private double score;
|
||||
|
||||
public Score(Stream<Order> sell, Stream<Order> buy) {
|
||||
sellProfit = computeProfits(sell);
|
||||
buyProfit = computeProfits(buy);
|
||||
public Score(Vendor vendor) {
|
||||
this.vendor = vendor;
|
||||
Stream<Order> sell = vendor.getAllSellOffers().stream().flatMap(Scorer.this::mapToOrder);
|
||||
Stream<Order> buy = vendor.getAllBuyOffers().stream().flatMap(Scorer.this::mapToOrder);
|
||||
|
||||
long count = sell.limit(ordersCount).count();
|
||||
computeScore(count);
|
||||
sellStat = computeProfits(sell);
|
||||
buyStat = computeProfits(buy);
|
||||
|
||||
computeScore();
|
||||
}
|
||||
|
||||
private double computeProfits(Stream<Order> orders) {
|
||||
OptionalDouble profit = orders.sorted(Comparator.<Order>reverseOrder())
|
||||
.limit(ordersCount)
|
||||
.mapToDouble(Order::getProfit)
|
||||
.average();
|
||||
return profit.orElse(0);
|
||||
public double getSellProfit() {
|
||||
return sellStat.getAverage();
|
||||
}
|
||||
|
||||
private void computeScore(long sellOrdersCount){
|
||||
score = (sellProfit + buyProfit)/2;
|
||||
if (sellOrdersCount < ordersCount){
|
||||
score =- Math.abs(avgProfit-sellProfit) * (ordersCount - sellOrdersCount) / score;
|
||||
}
|
||||
public double getBuyProfit() {
|
||||
return buyStat.getAverage();
|
||||
}
|
||||
|
||||
public double getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
private DoubleSummaryStatistics computeProfits(Stream<Order> orders) {
|
||||
return orders.sorted(Comparator.<Order>reverseOrder())
|
||||
.limit(ordersCount)
|
||||
.collect(Collectors.summarizingDouble(o -> o.getProfit() / profile.getShip().getCargo()));
|
||||
}
|
||||
|
||||
private void computeScore(){
|
||||
score = (getSellProfit() + getBuyProfit())/2;
|
||||
score -= distanceRate * avgProfit * (vendor.getDistance() - avgDistance) / avgDistance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Score{" +
|
||||
"vendor=" + vendor.getPlace()+"("+vendor+")"+
|
||||
", sellStat=" + sellStat +
|
||||
", buyStat=" + buyStat +
|
||||
", score=" + score +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull Score other) {
|
||||
return Double.compare(score, other.score);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,44 @@ package ru.trader.core;
|
||||
public class Profile {
|
||||
|
||||
private double balance;
|
||||
private int jumps;
|
||||
private Ship ship;
|
||||
private boolean refill;
|
||||
|
||||
public Profile(Ship ship) {
|
||||
this.ship = ship;
|
||||
refill = true;
|
||||
}
|
||||
|
||||
public double getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
public void setBalance(double balance) {
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
public int getJumps() {
|
||||
return jumps;
|
||||
}
|
||||
|
||||
public void setJumps(int jumps) {
|
||||
this.jumps = jumps;
|
||||
}
|
||||
|
||||
public Ship getShip() {
|
||||
return ship;
|
||||
}
|
||||
|
||||
public void setShip(Ship ship) {
|
||||
this.ship = ship;
|
||||
}
|
||||
|
||||
public boolean withRefill() {
|
||||
return refill;
|
||||
}
|
||||
|
||||
public void setRefill(boolean refill) {
|
||||
this.refill = refill;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
package ru.trader.core;
|
||||
|
||||
public class Ship {
|
||||
|
||||
private int cargo;
|
||||
private double engine;
|
||||
private Engine engine;
|
||||
private double tank;
|
||||
private double mass;
|
||||
|
||||
public Ship(int cargo, double engine) {
|
||||
this.cargo = cargo;
|
||||
this.engine = engine;
|
||||
public Ship() {
|
||||
//Default sidewinder
|
||||
this.mass = 44.9;
|
||||
this.cargo = 4;
|
||||
this.tank = 2;
|
||||
this.engine = new Engine(2, 'E');
|
||||
}
|
||||
|
||||
public static Ship copyOf(Ship other){
|
||||
return new Ship(other.cargo, other.engine);
|
||||
Ship copy = new Ship();
|
||||
copy.mass = other.mass;
|
||||
copy.cargo = other.cargo;
|
||||
copy.tank = other.tank;
|
||||
copy.engine = other.getEngine();
|
||||
return copy;
|
||||
}
|
||||
|
||||
public int getCargo() {
|
||||
@@ -22,12 +31,163 @@ public class Ship {
|
||||
this.cargo = cargo;
|
||||
}
|
||||
|
||||
public double getEngine() {
|
||||
public Engine getEngine() {
|
||||
return engine;
|
||||
}
|
||||
|
||||
public void setEngine(double engine) {
|
||||
public void setEngine(Engine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
public double getTank() {
|
||||
return tank;
|
||||
}
|
||||
|
||||
public void setTank(double tank) {
|
||||
this.tank = tank;
|
||||
}
|
||||
|
||||
public double getMass() {
|
||||
return mass;
|
||||
}
|
||||
|
||||
public void setMass(double mass) {
|
||||
this.mass = mass;
|
||||
}
|
||||
|
||||
public double getLadenMass(){
|
||||
return mass+tank+cargo;
|
||||
}
|
||||
|
||||
public double getLadenMass(double fuel){
|
||||
return mass+fuel+cargo;
|
||||
}
|
||||
|
||||
//Laden fuel cost
|
||||
public double getFuelCost(double distance){
|
||||
return engine.getFuelCost(distance, getLadenMass());
|
||||
}
|
||||
|
||||
public double getFuelCost(double fuel, double distance){
|
||||
return engine.getFuelCost(distance, getLadenMass(fuel));
|
||||
}
|
||||
|
||||
//Jump range with full fuel tank
|
||||
public double getJumpRange(){
|
||||
return getJumpRange(tank);
|
||||
}
|
||||
|
||||
//Laden jump range
|
||||
public double getJumpRange(double fuel){
|
||||
return engine.getJumpRange(fuel, getLadenMass(fuel));
|
||||
}
|
||||
|
||||
public double getFullTankJumpRange(){
|
||||
double fuel = tank;
|
||||
double range = 0;
|
||||
while (fuel > 0){
|
||||
double distance = engine.getJumpRange(fuel, getLadenMass(fuel));
|
||||
range += distance;
|
||||
fuel -= engine.getFuelCost(distance, getLadenMass(fuel));
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Ship{" +
|
||||
"cargo=" + cargo +
|
||||
", engine=" + engine +
|
||||
", tank=" + tank +
|
||||
", mass=" + mass +
|
||||
", maxDist=" + getJumpRange() +
|
||||
", fullTankDist=" + getFullTankJumpRange() +
|
||||
'}';
|
||||
}
|
||||
|
||||
//FSD multiplier by FSD Rating A,B,C ... etc * 0.001
|
||||
//http://elite-dangerous.wikia.com/wiki/Frame_Shift_Drive
|
||||
private final static double[] FSD_MULT = {0.012,0.010,0.008,0.010,0.011};
|
||||
//FSD power multiplier by FSD Class 1,2,3 ... etc
|
||||
private final static double[] FSD_POWER_MULT = {0,0,2.00,2.15,2.30,2.45,2.60,2.75,2.90};
|
||||
//FSD Optimal Mass [class][rating]
|
||||
private final static double[][] FSD_OPT_MASS = {
|
||||
{},
|
||||
{},
|
||||
{90.0, 75.0, 60.0, 54.0, 48.0},
|
||||
{150.0, 125.0, 100.0, 90.0, 80.0},
|
||||
{525.0, 438.0, 350.0, 315.0, 280.0},
|
||||
{1,050.0, 875.0, 700.0, 630.0, 560.0},
|
||||
{1,800.0, 1,500.0, 1,200.0, 1,080.0, 960.0}
|
||||
};
|
||||
//FSD Max fuel per jump [class][rating]
|
||||
private final static double[][] FSD_MAX_FUEL= {
|
||||
{},
|
||||
{},
|
||||
{0.90, 0.80, 0.60, 0.60, 0.60},
|
||||
{1.80, 1.50, 1.20, 1.20, 1.20},
|
||||
{3.00, 2.50, 2.00, 2.00, 2.00},
|
||||
{5.00, 4.10, 3.30, 3.30, 3.30},
|
||||
{8.00, 6.60, 5.30, 5.30, 5.30}
|
||||
};
|
||||
|
||||
private class Engine {
|
||||
private int rating;
|
||||
private int clazz;
|
||||
|
||||
private Engine(int clazz, char rating) {
|
||||
setRating(rating);
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
public char getRating() {
|
||||
return (char)(rating + 'A');
|
||||
}
|
||||
|
||||
public void setRating(char rating) {
|
||||
this.rating = rating - 'A';
|
||||
}
|
||||
|
||||
public int getClazz() {
|
||||
return clazz;
|
||||
}
|
||||
|
||||
public void setClazz(int clazz) {
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
public double getOptMass() {
|
||||
return FSD_OPT_MASS[clazz][rating];
|
||||
}
|
||||
|
||||
public double getMaxFuel() {
|
||||
return FSD_MAX_FUEL[clazz][rating];
|
||||
}
|
||||
|
||||
public double getMultiplier(){
|
||||
return FSD_MULT[rating];
|
||||
}
|
||||
|
||||
public double getPowMultiplier(){
|
||||
return FSD_POWER_MULT[clazz];
|
||||
}
|
||||
|
||||
//https://forums.frontier.co.uk/showthread.php?p=643461#post643461
|
||||
//Fuel Cost = Coefficient * (Distance * (Mass / Optimised Mass))^Power
|
||||
public double getFuelCost(double distance, double mass){
|
||||
return getMultiplier() * Math.pow(distance * (mass / getOptMass()), getPowMultiplier());
|
||||
}
|
||||
|
||||
public double getJumpRange(double fuel, double mass){
|
||||
return Math.pow(Math.min(fuel, getMaxFuel())/getMultiplier(), 1/getPowMultiplier())*getOptMass()/mass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ""+clazz+getRating()+
|
||||
" {optMass="+getOptMass()+
|
||||
", fuelPJ="+getMaxFuel()+"}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
116
core/src/test/java/ru/trader/analysis/ScorerTest.java
Normal file
116
core/src/test/java/ru/trader/analysis/ScorerTest.java
Normal file
@@ -0,0 +1,116 @@
|
||||
package ru.trader.analysis;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import ru.trader.core.*;
|
||||
import ru.trader.store.simple.Store;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
|
||||
public class ScorerTest extends Assert {
|
||||
private final static Logger LOG = LoggerFactory.getLogger(ScorerTest.class);
|
||||
|
||||
private Market world;
|
||||
private FilteredMarket fWorld;
|
||||
|
||||
private Place breksta;
|
||||
private Place bhadaba;
|
||||
private Place lhs1541;
|
||||
private Place itza;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
InputStream is = getClass().getResourceAsStream("/test3.xml");
|
||||
world = Store.loadFromFile(is);
|
||||
breksta = world.get("Breksta");
|
||||
bhadaba = world.get("Bhadaba");
|
||||
lhs1541 = world.get("LHS 1541");
|
||||
itza = world.get("Itza");
|
||||
|
||||
MarketFilter filter = new MarketFilter();
|
||||
fWorld = new FilteredMarket(world, filter);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScore() throws Exception {
|
||||
Vendor grantTerminal = breksta.get("Grant Terminal");
|
||||
Vendor perezMarket = breksta.get("Perez market");
|
||||
Vendor kandelRing = bhadaba.get("Kandel Ring");
|
||||
Vendor robertsHub = bhadaba.get("Roberts Hub");
|
||||
Vendor cabreraDock = lhs1541.get("Cabrera Dock");
|
||||
Vendor hallerPort = lhs1541.get("Haller Port");
|
||||
Vendor luikenPort = itza.get("Luiken Port");
|
||||
Ship ship = new Ship();
|
||||
ship.setCargo(100);
|
||||
Profile profile = new Profile(ship);
|
||||
LOG.info("Start score test, balance 10000000");
|
||||
profile.setBalance(10000000);
|
||||
Scorer scorer = new Scorer(fWorld, profile);
|
||||
if (LOG.isDebugEnabled()){
|
||||
fWorld.getVendors().map(scorer::getScore).sorted().forEach(s -> LOG.debug("{}", s));
|
||||
}
|
||||
|
||||
Scorer.Score gtScore = scorer.getScore(grantTerminal);
|
||||
Scorer.Score pmScore = scorer.getScore(perezMarket);
|
||||
assertTrue(gtScore.getSellProfit() < pmScore.getSellProfit());
|
||||
assertTrue(gtScore.getBuyProfit() > pmScore.getBuyProfit());
|
||||
assertTrue(gtScore.getScore() > pmScore.getScore());
|
||||
|
||||
Scorer.Score krScore = scorer.getScore(kandelRing);
|
||||
Scorer.Score rhScore = scorer.getScore(robertsHub);
|
||||
assertTrue(krScore.getScore() < pmScore.getScore());
|
||||
assertTrue(krScore.getScore() < rhScore.getScore());
|
||||
assertTrue(krScore.getScore() < gtScore.getScore());
|
||||
|
||||
Scorer.Score cdScore = scorer.getScore(cabreraDock);
|
||||
Scorer.Score hpScore = scorer.getScore(hallerPort);
|
||||
Scorer.Score lpScore = scorer.getScore(luikenPort);
|
||||
assertTrue(hpScore.getScore() > pmScore.getScore());
|
||||
assertTrue(hpScore.getScore() > krScore.getScore());
|
||||
assertTrue(hpScore.getScore() > gtScore.getScore());
|
||||
assertTrue(hpScore.getScore() > lpScore.getScore());
|
||||
assertTrue(hpScore.getScore() > cdScore.getScore());
|
||||
assertTrue(cdScore.getScore() > pmScore.getScore());
|
||||
assertTrue(cdScore.getScore() > krScore.getScore());
|
||||
assertTrue(cdScore.getScore() > gtScore.getScore());
|
||||
assertTrue(cdScore.getScore() > lpScore.getScore());
|
||||
|
||||
LOG.info("Start score test, balance 50000");
|
||||
profile.setBalance(50000);
|
||||
Scorer scorer2 = new Scorer(fWorld, profile);
|
||||
if (LOG.isDebugEnabled()){
|
||||
fWorld.getVendors().map(scorer2::getScore).sorted().forEach(s -> LOG.debug("{}", s));
|
||||
}
|
||||
|
||||
gtScore = scorer2.getScore(grantTerminal);
|
||||
pmScore = scorer2.getScore(perezMarket);
|
||||
assertTrue(gtScore.getSellProfit() > pmScore.getSellProfit());
|
||||
assertTrue(gtScore.getBuyProfit() > pmScore.getBuyProfit());
|
||||
assertTrue(gtScore.getScore() > pmScore.getScore());
|
||||
|
||||
krScore = scorer2.getScore(kandelRing);
|
||||
rhScore = scorer2.getScore(robertsHub);
|
||||
cdScore = scorer2.getScore(cabreraDock);
|
||||
hpScore = scorer2.getScore(hallerPort);
|
||||
lpScore = scorer2.getScore(luikenPort);
|
||||
|
||||
assertTrue(lpScore.getScore() > pmScore.getScore());
|
||||
assertTrue(lpScore.getScore() > gtScore.getScore());
|
||||
assertTrue(lpScore.getScore() > krScore.getScore());
|
||||
assertTrue(lpScore.getScore() > rhScore.getScore());
|
||||
assertTrue(lpScore.getScore() > cdScore.getScore());
|
||||
assertTrue(lpScore.getScore() > hpScore.getScore());
|
||||
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
world = null;
|
||||
fWorld = null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user