Coflnet.Sky.Bazaar.Flipper.Client
0.9.0
dotnet add package Coflnet.Sky.Bazaar.Flipper.Client --version 0.9.0
NuGet\Install-Package Coflnet.Sky.Bazaar.Flipper.Client -Version 0.9.0
<PackageReference Include="Coflnet.Sky.Bazaar.Flipper.Client" Version="0.9.0" />
<PackageVersion Include="Coflnet.Sky.Bazaar.Flipper.Client" Version="0.9.0" />
<PackageReference Include="Coflnet.Sky.Bazaar.Flipper.Client" />
paket add Coflnet.Sky.Bazaar.Flipper.Client --version 0.9.0
#r "nuget: Coflnet.Sky.Bazaar.Flipper.Client, 0.9.0"
#:package Coflnet.Sky.Bazaar.Flipper.Client@0.9.0
#addin nuget:?package=Coflnet.Sky.Bazaar.Flipper.Client&version=0.9.0
#tool nuget:?package=Coflnet.Sky.Bazaar.Flipper.Client&version=0.9.0
BazaarFlipper template for new microservices
Demand Flip Methodology
The demand flip score uses a short rolling history of bazaar movement instead of ranking by spread alone.
- The service keeps a 3 minute lookback window of ordered
GraphResultupdates. - For each adjacent pair it computes positive-only changes in
BuyMovingWeekandSellMovingWeek. - Older intervals inside the active window are half-weighted so the score leans toward more recent momentum.
- Each side gets a sustained-demand weight from the share of positive intervals, raised to
2.5. - The directional signals are built as:
buySignal = avgBuyIncrease * buyDemandWeight * 1.0sellSignal = avgSellIncrease * sellDemandWeight
- The round-trip fill score is asymmetric and currently tuned to:
roundTripSignal = sellSignal^0.8 * buySignal^0.2
- After-fee spread is then applied to convert that fill score into profit and capacity estimates.
In code this becomes:
spreadAfterOrder = (Buy - 0.1) * (1 - 0.01125) - (Sell + 0.1) // sell-leg tax only; buy orders untaxed
currentProfitPerHour = spreadAfterOrder * roundTripSignal * 30
hourlyVolume = min(sellSignal, buySignal) * 30
exitHourlyVolume = buySignal * 30
The demand API exposes those results in two ways:
Volumeis the estimated daily turnover at the current bottleneck pace.SuggestedOrderVolumeis the lot size for the requested hold window.EstimatedSellTimeMinutesestimates how long that suggested lot should take to exit, ornullwhen there is not enough current demand to produce a reliable estimate.TotalProfitis the spread profit for that suggested lot at current prices.
Demand Benchmarking
The current constants were selected from a five item historical benchmark using:
BOX_OF_SEEDSSHARD_NESSIEBOOSTER_COOKIESHARD_BAMBULEAFREFINED_UMBER
Each benchmark sample evaluates a 3 minute window and compares the chosen item against a weighted future round-trip oracle using these horizons:
- 1 minute at weight
0.34 - 5 minutes at weight
0.26 - 10 minutes at weight
0.18 - 20 minutes at weight
0.12 - 30 minutes at weight
0.10
The tuned configuration changed from:
InstaBuyDemandWeight = 2.0SustainedDemandExponent = 2.0ProfitWeightedSellExponent = 0.4ProfitWeightedBuyExponent = 0.6
to:
InstaBuyDemandWeight = 1.0SustainedDemandExponent = 2.5ProfitWeightedSellExponent = 0.8ProfitWeightedBuyExponent = 0.2
That update improved weighted oracle capture from roughly 94% to roughly 95% in the sweep used to select the current constants. In exchange, the exact top-pick hit rate settles at roughly 89.9% on the current benchmark sample set.
Tax correction (2026-07-12): those figures were computed with a wrong ~2.4% round-trip tax (the buy leg was taxed too). The bazaar taxes only the sell leg — 1.125% round trip; the spread formula is now
(Buy - 0.1) * (1 - 0.01125) - (Sell + 0.1). Under the corrected tax this 5-item set no longer favors the profit-weighted score on oracle capture (the momentum exponents were tuned under the wrong tax and warrant a re-tune). The authoritative, tax-corrected evidence is the 80-item and historical-window pools below, whereRankMode.Throughputrobustly wins — and the production sort now uses it, so the0.8/0.2profit-weighting exponents no longer affect ordering.
Realistic-profit research (fill simulator + factor experiments)
The statistical benchmark above is nearly saturated, but its oracle is built from the same ingredients as the score (future moving-week deltas × top-of-book spread), so it cannot see order competition, being undercut, price drift during the hold, or capital lock time. To measure those, the scoring math was extracted into a single reusable core and graded against a mechanism-level ground truth.
Services/DemandFlipScoring.cs— the one scoring implementation used byDemandFlipService, the controller and the backtests.ScoringOptions.Defaultreproduces the historical formula bit-for-bit (guarded byRefactoredServiceMatchesLegacyFormulaOnAllHistories). Optional factor knobs are all off by default:SpreadMode(H2 spread durability),ChurnBeta(H3 undercut churn),CrowdingAlpha(H1 order competition),OverhangGamma(H4 queue overhang).Services/OrderFillSimulator.cs— simulates actually placing the buy order (Sell + 0.1), accruing fills from insta-sell flow while at the top of book, being outbid/undercut, re-listing, timeouts and final liquidation. Fill share is optimistic, count-growth, or (with order-book depth)DepthLevel1. It is the independent oracle.Services/DemandFlipBenchmark.Tests.cs— 5-item baseline + oracle-disagreement table.Services/DemandFlipWideBenchmark.Tests.cs— the reliable run: 12 diverse items × 14 days of 20s snapshots with order-book depth, pulled from the SkyBazaar history service intoMock/depth/(gitignored). Factor grids vs two sim oracles, walk-forward split, and leave-one-item-out stability.Assert.Ignores when the depth data is not present locally.
Pull the depth data (needs the SkyBazaar history service reachable, e.g.
kubectl port-forward -n sky svc/sky-bazaar 5011:8000) via
GET /api/bazaar/{item}/data?start&end&smallestResolution=true&includeArchivedOrderbook=true.
Key findings (14-day, 12-item, 1557 samples)
- The statistical oracle and the fill-simulator oracle disagree on the best item about half the time. The shipped model captures ~84% of statistical-oracle profit but only ~60% of realistic (depth-aware fill-sim) profit — the realistic gap is real and larger than the statistical benchmark implies.
- Modeling competition at your own price level (depth-aware queue share) lowers realized profit materially vs the optimistic bound, confirming undercutting is a first-order effect.
- Among the interpretable factors, the near-exit supply wall (H6, depth) and a mild undercut-churn penalty (H3, β≈0.5) improve realistic-profit ranking on the held-out fold without hurting the statistical benchmark, and are stable across leave-one-item-out. The drift-adjusted spread (H2) captures the most realized profit but degrades the statistical metrics — a higher-variance bet. Order-count crowding (H1) and level-1 order count alone did not help.
None of the factors is a large enough, clean enough winner to flip on blindly, so defaults are
unchanged. They can be enabled for a live A/B without a redeploy via the DEMAND config section
(DEMAND:SPREADMODE, DEMAND:CHURNBETA, DEMAND:CROWDINGALPHA, DEMAND:OVERHANGGAMMA).
Top-N basket capture — the product-relevant metric (biggest finding)
The demand API ranks a top-N list, so "did we pick the single absolute best item" understates
the product. Services/PortfolioCapture.Tests.cs measures how much realized profit our top-N set
captures vs the best-achievable top-N, over an 80-item liquid pool (Mock/wide/, gitignored).
- On a realistic 80-item pool our current top-10 captures only ~47% of the best-possible top-10 (and ~33% of all market profit vs a 66% ceiling for a perfect top-10) — real headroom that the 12-item toy pool hid (there top-10-of-12 is trivially ~98%).
- The micro-structure factors (H1-H4, H6) do not move the top-10 basket capture — noise.
- The lever is the ranking formulation: the momentum score is a geometric-mean signal, but the oracle grades realized coins/hour, so ranking by a throughput/volume estimate helps.
Multi-window confirmation (2026-07-12) — sqrt-volume was overfit, Throughput is the winner
The +8pp for spread × √volume came from a single 14-day window. Confirming it against four
disjoint, all-older-than-14-day windows of 5-min archived history (top-40 liquid items,
Mock/histwin/, gitignored; Services/HistoricalWindows.Tests.cs) reversed it, and two
isolation tests on the existing 80-item/20s pool explain why:
spread × √volume(SpreadSqrtVolume) fails to generalize: 0/4 historical windows, −11.9pp avg. It is a pool-breadth artifact —FormulationDependsOnPoolBreadthWidePoolshows it only helps when the pool is padded with low-volume items to deprioritize (broad 80-item: 55.5% vs momentum 46.9%); on the all-liquid top-40 subset momentum jumps to 82.1% and √volume stays 57.1%.- The apparent edge is not a lookback effect:
MomentumLookbackVsFormulationOnWidePoolshows widening the momentum lookback (3→60 min) does not help (46.6%→44.3%). Throughput(spread × momentum-derived hourly fill rate) beats momentum in every regime: broad 80-item +3.8pp, all-liquid top-40 +5.5pp, and 4/4 historical windows (+1.2pp avg). It keeps momentum's timing while scaling by realistic fill volume, so it avoids thin-spread mega-volume traps. It is the recommended ranking if the default is changed.
This is exposed as ScoringOptions.RankMode (Momentum, Throughput, SpreadSqrtVolume,
SpreadVolume). It changes only the sort key, not the displayed profit/hour. The production
default (resolved in Startup.BuildDemandScoringOptions) is now Throughput, since it beat the
momentum sort 6/6 across pool breadth and time. ScoringOptions.Default itself is left as the
momentum formula (the equivalence-guard and benchmark baseline); the live default is the DI-provided
value. Override via DEMAND:RANKMODE (e.g. Momentum to revert, for an A/B control). The historical
margin is small (~1pp) and every oracle shares the moving-week fill assumption, so a live A/B —
or wiring in real per-user fills as an independent oracle — is still the way to settle it precisely.
Run the experiments: dotnet test --filter "Category=Benchmark" (wide runs need Mock/depth/,
Mock/wide/; the multi-window run needs Mock/histwin/ — pull via scratchpad/pull_histwin.py).
Fleet allocation — coordinating contending traders (2026-08-17)
The ranking above is single-trader. A pool of K traders (nominal 20, range 10–30) each holding
≤ M=10 distinct items contend for the same finite market flow: the fill simulator only fills an
order while it is at top of book, so N traders piled on one item split that one ΔmovingWeek flow.
Per-item group profit is therefore concave in traders-per-item (linear while capital-limited,
flat once the flow is saturated), and the profit-maximising allocation is greedy water-filling —
Services/FleetAllocator.cs (Allocate(items, opts)): hand each next trader-slot to the item with
the highest marginal gain, cap n_i ≤ K, stop when idle capital beats a starved order, then pack the
counts into K baskets of ≤ M distinct items (Gale–Ryser).
Services/FleetCapture.Tests.cs grades this walk-forward on the four Mock/histwin/ windows against
the same OrderFillSimulator, comparing four allocations (all scored by the identical simulator):
pileTopM (today's uncoordinated fleet — every trader takes the top-M by RankScore, n_i = K),
spreadTopKM (naive 1-per-item diversification), capacityAware (the allocator), and oracle
(foresight-greedy on the realized simulator objective — a reference ceiling).
- capacityAware beats the best naive baseline by +70% at K=20 / 10M-coins-per-order (538M vs 317M aggregate realized coins), robust in every window (+62→79%) and every fleet size (K=10 +36%, K=20 +70%, K=30 +105% — more contenders, more coordination gain). It reaches ~51% of the perfect-foresight ceiling, so a smarter allocator still has room.
- Uplift is capital-regime-dependent (per-order capital sweep): +32% at 2M, +70% at 10M, +12% at 50M. With large orders thin books overload — the profitable ceiling drops below K·M slots and naive piling collapses (pile falls 238M→84M as K grows at 50M).
Caveats held in view (not tuned away): graded by the moving-week fill simulator, not yet by
independent real fills; pileTopM is flattered (intra-fleet undercutting unmodelled, so the real tax
is likely larger); the oracle is foresight-greedy, not a proven optimum.
Heterogeneous / tiered allocation (Services/TieredFleetAllocator.cs). Real traders aren't a
homogeneous fleet: different tiers (premium+ > premium > …), budgets, and fast/patient modes, with
real users preferred over bots and only ACTIVE users counted. The allocator does priority-ordered,
budget-bounded, return-on-capital greedy filling that drains shared per-item capacity and emits the
leftover as a residual feed (for untracked website users). It carries a per-item maxCoinExposurePerItem
cap — a required risk control: without it the greedy allocator over-commits the fleet to a single
thin, mis-estimated item and lost to production by −44% on real data.
Services/TieredAllocation.Tests.cs grades it against a FAITHFUL model of today's production
(SkyModCommands.BazaarFlipService: items deduped across same-tier users via assignedTags, one
order/user/cycle, fixed 64/4/1 order sizes from BazaarOrderAmountHelper — NOT budget-scaled).
Honest result: the edge is capital-dependent. The dominant lever is budget-aware order sizing —
production deploys fixed tiny stacks that leave large purses idle. Swept over budget scale × steady-state
slots/user S: at 1x budgets tiered wins +26→+151%; at 0.5x budget with production fully ramped (S=10)
it loses −19% (reported, not tuned away). Takeaway: size orders off purse + capacity (floored at
production's fixed size so low-purse users never do worse), and the biggest, lowest-risk production win
is that sizing change, not the routing. Priority fidelity holds (premium+ > premium > starter realized
profit/trader). Patient-mode scoring is a later step (all benchmark traders are Mode=Fast for now).
Real-fill persistence (the independent oracle, accumulating): SkyUserState emits every completed
flip to the sky-bazaar-flip Kafka topic (TOPICS:BAZAAR_FLIP). Services/ObservedBazaarFillBackgroundService.cs
now consumes it (fresh group, AutoOffsetReset.Earliest to backfill) and persists to
ObservedBazaarFill (EF/MySQL) via Services/ObservedFillService.cs (dedupes on player+item+soldAt).
Schema is additive and guarded behind MIGRATE_ON_STARTUP (default off; a normal startup does zero DDL).
This is what lets the fleet study eventually be re-graded against measured realized profit rather than
the moving-week sim.
Deploying
This project should be deployed within a container.
Configuration
See appsettings.json
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- JsonSubTypes (>= 2.0.1)
- Newtonsoft.Json (>= 13.0.3)
- Polly (>= 8.1.0)
- RestSharp (>= 112.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.9.0 | 622 | 8/21/2026 |
| 0.8.0 | 127 | 8/18/2026 |
| 0.7.0 | 1,431 | 3/21/2026 |
| 0.6.0 | 304 | 2/10/2026 |
| 0.5.0 | 947 | 11/5/2025 |
| 0.4.2 | 473 | 9/10/2025 |
| 0.4.1 | 217 | 9/9/2025 |
| 0.4.0 | 219 | 9/9/2025 |
| 0.3.2 | 358 | 8/23/2025 |
| 0.3.1 | 810 | 7/21/2025 |
| 0.3.0 | 589 | 7/21/2025 |
| 0.2.0 | 453 | 6/29/2025 |
| 0.1.2 | 2,706 | 10/31/2024 |
| 0.1.1 | 3,293 | 4/24/2024 |
| 0.1.0 | 481 | 3/29/2024 |
| 0.0.2 | 907 | 12/23/2023 |
| 0.0.1 | 286 | 12/23/2023 |
Minor update