XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.Concurrent;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Planning;

namespace XFEEDToolBox.Core.Execution;

public sealed record AutomaticSupplyRequest(AutomaticTradeConfiguration Configuration,PlayerState Player,decimal Credits,decimal Reserve,
    IReadOnlyDictionary<long,IReadOnlyList<CommodityQuote>> ObservedQuotes)
{public IReadOnlyList<ConfirmedTradeReceipt> ConfirmedTrades{get;init;}=[];public IReadOnlySet<long> ExcludedSourceMarkets{get;init;}=new HashSet<long>();}

public sealed record AutomaticSupplyChoice(Station Source,TradeLeg Delivery,long Supply,long Demand,int FreeCapacity,
    double EmptyReturnMinutes,double RepositionMinutes,decimal EstimatedNetProfit,double EstimatedMinutes)
{
    public TradeOrder Order=>Delivery.Orders.Single();
    public double LoadFactor=>FreeCapacity>0?(double)Order.Quantity/FreeCapacity:0;
    public long StockFullLoads=>FreeCapacity>0?Supply/FreeCapacity:0;
    public decimal CreditsPerHour=>EstimatedMinutes>0?EstimatedNetProfit*60/(decimal)EstimatedMinutes:0;
    public string Basis=>$"按实际供需可装 {Order.Quantity:N0}/{FreeCapacity:N0} t({LoadFactor:P0});该商品库存 {Supply:N0} t。"
        +$"循环含空载返程 {EmptyReturnMinutes:F1} 分钟、载货交付 {Delivery.EstimatedMinutes:F1} 分钟"
        +(RepositionMinutes>0?$"及本次转场 {RepositionMinutes:F1} 分钟":"")+";不假定后续补货,到站与每轮交付后重新核对。";
}

/// <summary>Ranks every compatible CG supply station by a real bounded load and complete repeated-trip time.</summary>
public static class AutomaticSupplyPlanner
{
    public static MarketBook DeductConfirmedTrades(MarketBook book,IEnumerable<ConfirmedTradeReceipt> receipts)
    {
        var rows=book.Quotes.GroupBy(q=>q.MarketId).ToDictionary(g=>g.Key,g=>(IReadOnlyList<CommodityQuote>)g.ToArray());
        return book with{Quotes=DeductConfirmedTrades(rows,receipts).Values.SelectMany(q=>q).ToArray()};
    }

    public static MarketBook MergeObservations(MarketBook cache,IReadOnlyDictionary<long,IReadOnlyList<CommodityQuote>> observations)
    {
        var latest=observations.Where(p=>p.Value.Count>0).ToDictionary(p=>p.Key,p=>p.Value.Max(q=>q.CollectedAt));
        // A complete observed snapshot masks older cached commodities,
        // while genuinely newer public quotes can report changed stock.
        return cache with{Quotes=cache.Quotes.Where(q=>!latest.TryGetValue(q.MarketId,out var at)||q.CollectedAt>at)
            .Concat(observations.Values.SelectMany(rows=>rows)).ToArray()};
    }

    public static IReadOnlyDictionary<long,IReadOnlyList<CommodityQuote>> DeductConfirmedTrades(
        IReadOnlyDictionary<long,IReadOnlyList<CommodityQuote>> observations,IEnumerable<ConfirmedTradeReceipt> receipts)
    {
        var changes=receipts.Where(r=>r.ServerConfirmed&&r.ClientApplied).DistinctBy(r=>r.OperationKey)
            .GroupBy(r=>(r.MarketId,r.Commodity)).ToDictionary(g=>g.Key,g=>g.ToArray());
        return observations.ToDictionary(p=>p.Key,p=>(IReadOnlyList<CommodityQuote>)p.Value.Select(q=>
        {
            if(!changes.TryGetValue((q.MarketId,q.Commodity),out var trades))return q;
            var newer=trades.Where(r=>r.ConfirmedAt>q.CollectedAt).ToArray();
            var bought=newer.Where(r=>r.QuantityDelta>0).Sum(r=>(long)r.QuantityDelta);
            var sold=newer.Where(r=>r.QuantityDelta<0).Sum(r=>-(long)r.QuantityDelta);
            return q with{Supply=Math.Max(0,q.Supply-bought),Demand=Math.Max(0,q.Demand-sold)};
        }).ToArray());
    }

    public static IReadOnlyList<AutomaticSupplyChoice> Calculate(MarketBook book,PlannerRequest request,Station destination,
        IReadOnlyCollection<string> acceptedCommodities,int count=8,CancellationToken token=default,IProgress<TradePlanningProgress>? progress=null)
    {
        request.Validate();TradeRoutePlanner.RequireKnownShip(request);
        if(!TradeSearchConstraints.DestinationAllows(destination,request))return [];
        if(request.CurrentPosition is not{IsValid:true}||destination.Position is not{IsValid:true})throw new InvalidDataException("CG货源比较需要当前飞船与交付站的真实星系坐标。");
        if(!TradeRoutePlanner.IsStationCompatible(destination,request,false))throw new InvalidOperationException("CG交付站不满足实际飞船停机坪或访问条件。");
        if(count<1)throw new ArgumentOutOfRangeException(nameof(count));
        var allowed=acceptedCommodities.ToHashSet(StringComparer.Ordinal);
        var commodityFilter=TradeSearchConstraints.NormalizeCommodityFilter(request.CommunityGoalCommodityFilter);
        if(commodityFilter.Length>0)allowed.IntersectWith(commodityFilter);
        var center=request.SearchCenterPosition??request.CurrentPosition;
        // Resolve the newest observation before filtering it. A depleted,
        // restricted or stale latest quote must never resurrect older stock.
        var fresh=book.Quotes.GroupBy(q=>(q.MarketId,q.Commodity))
            .Select(g=>g.OrderByDescending(q=>q.CollectedAt).ThenBy(q=>q.Supply).First())
            .Where(q=>q.CollectedAt<=request.AsOf.AddMinutes(5)&&q.CollectedAt>=request.AsOf.AddMinutes(-request.MaxMarketAgeMinutes)&&!q.Illegal&&!q.Rare&&allowed.Contains(q.Commodity))
            .GroupBy(q=>q.MarketId).ToDictionary(g=>g.Key,g=>g.ToArray());
        if(!fresh.TryGetValue(destination.MarketId,out var destinationRows))return [];
        var buyers=destinationRows.Where(q=>q.SellPrice>0&&q.Demand>0&&q.SellPrice<=NativeTradeBatchPolicy.MaximumUnitPrice).ToDictionary(q=>q.Commodity);
        var sources=book.Stations.Where(s=>s.MarketId!=destination.MarketId&&s.Position is{IsValid:true}&&s.Position.DistanceTo(center)<=request.SearchRadiusLy
            &&s.Position.DistanceTo(destination.Position)<=request.MaxLegDistanceLy&&TradeRoutePlanner.IsStationCompatible(s,request,false)
            &&TradeSearchConstraints.SourceAllows(s,request)&&StationSourcePolicy.Allows(s,request.AllowedSourceKinds)&&fresh.ContainsKey(s.MarketId)).DistinctBy(s=>s.MarketId).ToArray();
        var found=new ConcurrentBag<AutomaticSupplyChoice>();long processed=0;var gate=new object();
        progress?.Report(new("比较 CG 供货与循环收益",0,sources.Length,"所有合格货源均参与;数量受库存、需求、实际空舱和本金共同约束。"));
        var budget=Math.Max(0,Math.Min(request.Credits-request.ReserveCredits,request.InvestmentCredits??decimal.MaxValue));
        Parallel.ForEach(sources,new ParallelOptions{CancellationToken=token,MaxDegreeOfParallelism=TradeRoutePlanner.MaxParallelism},source=>
        {
            var delivery=TradeRoutePlanner.EstimateTravel(source.Position!,source.SystemName,destination,request);
            var empty=TradeRoutePlanner.EstimateTravel(destination.Position!,destination.SystemName,source,request);
            var reposition=request.CurrentMarketId==destination.MarketId||request.CurrentMarketId==source.MarketId?null
                :TradeRoutePlanner.EstimateTravel(request.CurrentPosition!,request.CurrentSystemName,source,request);
            foreach(var offer in fresh[source.MarketId])
            {
                token.ThrowIfCancellationRequested();
                if(offer.BuyPrice is <=0 or >NativeTradeBatchPolicy.MaximumUnitPrice||offer.Supply<=0||!NativeTradeBatchPolicy.AllowsFormal(offer.Commodity)
                    ||!buyers.TryGetValue(offer.Commodity,out var target)||target.SellPrice-offer.BuyPrice<Math.Max(1,request.MinimumProfitPerTon))continue;
                var quantity=(int)Math.Min(Math.Min((long)request.UsableCargoCapacity,offer.Supply),Math.Min(target.Demand,Math.Min(int.MaxValue,decimal.Floor(budget/offer.BuyPrice))));
                if(quantity<=0)continue;
                var order=new TradeOrder(offer.Commodity,offer.DisplayName,quantity,offer.BuyPrice,target.SellPrice);
                var cost=delivery.Cost+empty.Cost+(reposition?.Cost??0);var profit=order.Profit-cost;if(profit<=0)continue;
                var age=Math.Max(0,(request.AsOf-(offer.CollectedAt<target.CollectedAt?offer.CollectedAt:target.CollectedAt)).TotalMinutes);
                var risk=Math.Min(1,Math.Max(TradeRoutePlanner.SecurityRisk(source.Security),TradeRoutePlanner.SecurityRisk(destination.Security))
                    +.15*(double)quantity/Math.Max(1,target.Demand)+.12*age/request.MaxMarketAgeMinutes);
                if(risk>(request.Risk switch{RiskTolerance.Conservative=>.4,RiskTolerance.Balanced=>.75,_=>1}))continue;
                var leg=new TradeLeg{Number=1,From=source,To=destination,Orders=[order],DistanceLy=delivery.Distance,EstimatedJumps=delivery.Jumps,
                    EstimatedMinutes=delivery.Minutes,OperatingCost=delivery.Cost,CashAfter=request.Credits+order.Profit-delivery.Cost,RiskScore=risk};
                found.Add(new(source,leg,offer.Supply,target.Demand,request.UsableCargoCapacity,empty.Minutes,reposition?.Minutes??0,
                    profit,delivery.Minutes+empty.Minutes+(reposition?.Minutes??0)));
            }
            lock(gate){processed++;progress?.Report(new("比较 CG 供货与循环收益",processed,sources.Length,"已将实际可装载数量及空载返程计入 Cr/h。"));}
        });
        return found.OrderByDescending(c=>c.CreditsPerHour).ThenByDescending(c=>c.EstimatedNetProfit).ThenByDescending(c=>c.Order.Quantity)
            .ThenBy(c=>c.Source.MarketId).ThenBy(c=>c.Order.Commodity,StringComparer.Ordinal).Take(count).ToArray();
    }
}