Files
Operation-Blue-Laminate-v2/BlueLaminate/BlueLaminate.Tests/Tradeups/TradeupSelectorTests.cs
2026-06-02 13:31:27 -05:00

205 lines
7.5 KiB
C#

using BlueLaminate.Core.Tradeups;
using Xunit;
namespace BlueLaminate.Tests.Tradeups;
public class TradeupSelectorTests
{
private const decimal Bucket = 0.005m;
private static SelectableInput Item(decimal fraction, decimal price)
=> new(fraction, new InputListing(
SkinId: 1,
MarketHashName: "Test Skin",
Marketplace: "test",
InspectLink: null,
ExternalId: "0",
FloatValue: fraction,
Price: price));
[Fact]
public void Cheapest_full_selection_is_the_ten_cheapest_copies()
{
// 12 copies priced 1..12 at assorted fractions. With no binding float target the
// cheapest ten (cost 55) must be attainable at some summed-fraction bucket.
var pool = new List<SelectableInput>();
for (var i = 1; i <= 12; i++)
{
pool.Add(Item(fraction: 0.01m * i, price: i));
}
var selection = TradeupSelector.Solve(pool, contractSize: 10, Bucket);
var selections = selection.Selections().ToList();
Assert.NotEmpty(selections);
var cheapest = selections.MinBy(s => s.Cost);
Assert.Equal(55m, cheapest.Cost);
var picks = cheapest.Picks.ToList();
Assert.Equal(10, picks.Count);
Assert.Equal(Enumerable.Range(1, 10).Select(i => (decimal)i), picks.Select(p => p.Price).OrderBy(p => p));
}
[Fact]
public void Reported_average_fraction_is_a_conservative_upper_bound()
{
var pool = new List<SelectableInput>();
for (var i = 0; i < 10; i++)
{
pool.Add(Item(fraction: 0.123m, price: 1m));
}
var only = Assert.Single(TradeupSelector.Solve(pool, 10, Bucket).Selections());
var trueAverage = only.Picks.ToList().Average(p => p.FloatValue);
Assert.True(only.AverageFraction >= trueAverage,
$"bucketed average {only.AverageFraction} should round up from true {trueAverage}");
// 0.123 rounds up to the 0.125 bucket (0.005 grid).
Assert.Equal(0.125m, only.AverageFraction);
}
[Fact]
public void Selecting_cheaper_low_float_copies_lowers_the_attainable_average()
{
// Cheap high-float copies vs. pricier low-float copies. A lower average is only
// reachable by paying for the low-float set, so a low-average selection must cost
// more than the unconstrained cheapest.
var pool = new List<SelectableInput>();
for (var i = 0; i < 10; i++)
{
pool.Add(Item(fraction: 0.60m, price: 1m)); // cheap, bad float
}
for (var i = 0; i < 10; i++)
{
pool.Add(Item(fraction: 0.10m, price: 5m)); // pricey, good float
}
var selections = TradeupSelector.Solve(pool, 10, Bucket).Selections().ToList();
var cheapest = selections.MinBy(s => s.Cost);
Assert.Equal(10m, cheapest.Cost); // ten cheap copies
Assert.Equal(0.60m, cheapest.AverageFraction);
var lowestFloat = selections.MinBy(s => s.AverageFraction);
Assert.Equal(0.10m, lowestFloat.AverageFraction);
Assert.Equal(50m, lowestFloat.Cost); // forced onto the pricey low-float set
}
[Fact]
public void No_full_selection_when_pool_is_too_small()
{
var pool = Enumerable.Range(0, 5).Select(i => Item(0.2m, i + 1)).ToList();
Assert.Empty(TradeupSelector.Solve(pool, 10, Bucket).Selections());
}
private static TradeupSelector.RewardItem Reward(int bucket, double reward, decimal price = 0m)
=> new(bucket, reward, new InputListing(1, "x", "m", null, "0", 0.1m, price));
[Fact]
public void MaxReward_picks_the_highest_reward_set_within_the_cap()
{
// Six items; pick 3 maximising reward with bucket sum ≤ 6. The three best rewards
// (9, 8, 7) sit at buckets 2,2,2 = 6 ≤ cap, so they win.
var items = new[]
{
Reward(2, 9), Reward(2, 8), Reward(2, 7),
Reward(1, 1), Reward(1, 2), Reward(1, 3),
};
var picks = TradeupSelector.SolveMaxReward(items, contractSize: 3, capBucket: 6);
Assert.NotNull(picks);
Assert.Equal(3, picks!.ToList().Count);
}
[Fact]
public void MaxReward_respects_the_float_cap_even_at_the_cost_of_reward()
{
// The fat-reward items sit at high buckets; the cap forces the low-bucket set.
var items = new[]
{
Reward(5, 100, price: 50m), // too high-float to fit under a tight cap
Reward(1, 5, price: 1m),
Reward(1, 4, price: 1m),
Reward(1, 3, price: 1m),
};
var picks = TradeupSelector.SolveMaxReward(items, contractSize: 3, capBucket: 3);
Assert.NotNull(picks);
var chosen = picks!.ToList();
Assert.Equal(3, chosen.Count);
Assert.All(chosen, p => Assert.Equal(1m, p.Price)); // the three low-float copies
}
[Fact]
public void MaxReward_returns_null_when_the_contract_cannot_be_filled_under_the_cap()
{
// Only two items fit under the cap, but three are required.
var items = new[] { Reward(1, 5), Reward(1, 4), Reward(9, 100) };
Assert.Null(TradeupSelector.SolveMaxReward(items, contractSize: 3, capBucket: 3));
}
}
public class OutputPriceBookTests
{
private static TradeupListingRow Row(WearBand band, bool st, decimal price)
{
// A float squarely inside the band, so Build files it where we expect.
var f = band switch
{
WearBand.FactoryNew => 0.03m,
WearBand.MinimalWear => 0.10m,
WearBand.FieldTested => 0.25m,
WearBand.WellWorn => 0.40m,
_ => 0.60m,
};
return new TradeupListingRow(1, "M4A4 | X-Ray", "csmoney", null, "0", st, false, f, price);
}
[Fact]
public void Liquid_band_uses_its_own_price()
{
// Ten MW listings -> the MW band is trusted at its own lowest ask.
var rows = Enumerable.Range(0, 10).Select(i => Row(WearBand.MinimalWear, true, 90m + i));
var book = TradeupListingData.Build(rows).OutputPrices;
var r = book.Resolve(1, statTrak: true, outputFloat: 0.10m, thinThreshold: 10);
Assert.Equal(OutputPriceBasis.Band, r.Basis);
Assert.Equal(90m, r.LowestAsk);
}
[Fact]
public void Thin_band_falls_back_to_the_skin_overall_floor()
{
// The X-Ray case: two outlier FN listings ($1287) but a liquid MW market (~$90).
// A produced FN output must price off the $90 floor, not the $1287 band outlier.
var rows = new List<TradeupListingRow>
{
Row(WearBand.FactoryNew, true, 1287m),
Row(WearBand.FactoryNew, true, 1290m),
};
rows.AddRange(Enumerable.Range(0, 10).Select(i => Row(WearBand.MinimalWear, true, 90m + i)));
var book = TradeupListingData.Build(rows).OutputPrices;
var r = book.Resolve(1, statTrak: true, outputFloat: 0.0695m, thinThreshold: 10);
Assert.Equal(OutputPriceBasis.Floor, r.Basis);
Assert.Equal(90m, r.LowestAsk); // skin-wide floor, not the $1287 FN outlier
Assert.Equal(2, r.BandLiquidity); // still reports the thin band count → triggers CSFloat
}
[Fact]
public void Unlisted_skin_resolves_to_none()
{
var book = TradeupListingData.Build(Array.Empty<TradeupListingRow>()).OutputPrices;
var r = book.Resolve(1, statTrak: false, outputFloat: 0.1m, thinThreshold: 10);
Assert.Equal(OutputPriceBasis.None, r.Basis);
Assert.Null(r.LowestAsk);
}
}