fix(flip-candidates): floor GE tax to whole coins, pin full sell-cycle, qualify F2P path

Governor-review bounce (PR #7) fixes on issue-1:
1. GE 2% convenience fee rounds DOWN per item: net = sell - floor(0.02*sell) - buy.
   Anchor net 16.12 -> 17 (244-4-223); top-8 members table recomputed with
   floored tax -> 598,000/4h = 149,500 gp/h (was 138,000); gp_per_hour_low/high
   updated. Validator now rejects fractional-tax arithmetic for GE-margin entries.
2. State machine split wait_fill into wait_buy_fill / wait_sell_fill so the full
   buy -> buy-fill -> sell -> sell-fill -> collect path is explicit; sell cycle
   can no longer loop back into sell without collecting.
3. Economy qualified as the members path (8 slots); F2P path added (3 slots,
   F2P-tradeable rows only): Iron bar + Cosmic rune + Silver ore = 227,000/4h
   = 56,750 gp/h. Members/F2P flags cross-checked against wiki item pages.
This commit is contained in:
Claude 2026-08-27 05:27:09 -05:00
parent 04a4af0299
commit 864f2c9e1c
2 changed files with 76 additions and 25 deletions

View File

@ -3,21 +3,25 @@ title: GE Flipping (margin trading)
domain: economy
tags: [flipping, merchanting, ge, money-making, hybrid]
status: verified
version: 1
version: 2
updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
summary: >
Buy-and-sell Grand Exchange margin flipping: place resting buy offers at
the low edge of an item's price band and resting sell offers at the high
edge, harvesting the spread on high-volume items. There are no skill or
quest requirements — capital breadth and slot discipline are the whole game.
quest requirements — capital breadth and slot discipline are the whole
game. Members accounts run 8 GE slots over the full candidate set;
F2P accounts get 3 slots and F2P-tradeable rows only (separate F2P path
in profit_drivers).
accounts:
membership: hybrid # flipping works on F2P and members accounts alike
membership: hybrid # members: 8 slots/full set; F2P: 3 slots/F2P rows (see economy)
min_level: 3 # post-Tutorial Island baseline; no skill requirement
requires_quest: []
requires_other:
- "Post-Tutorial Island account with Grand Exchange access (no skill or quest requirement)"
- "Members account: 8 GE slots, full candidate set (the modeled path below). F2P account: 3 GE slots and F2P-tradeable rows only — run the F2P path in profit_drivers."
best_practice:
equipment:
@ -48,22 +52,35 @@ best_practice:
some items share a connected limit with related items — treat those as one bucket
when filling slots."
source: "OSRS wiki — Grand Exchange (Buy limits; Grand Exchange/Buying limits)"
- name: run the path that matches the account's slot set
why: "Members get eight GE slots and may trade any row; F2P accounts get three slots
and may only purchase free-to-play rows. The modeled economics below assume the
members set — a hybrid account must swap to the 3-slot F2P path (Iron bar /
Cosmic rune / Silver ore) instead of chasing members-only rows it cannot buy."
source: "OSRS wiki — Grand Exchange (Members get eight Grand Exchange slots ... free players are limited to three. Additionally, free players may only purchase free-to-play items, and not members' items)"
economy:
# Model (documented, reproducible from the snapshot):
# per slot: buy `limit` units at the low edge, sell at the high edge.
# net_pu = 0.98 * sell - buy (2% convenience fee on the sale; the 5M/item cap and
# the sub-50gp rounding exemption are not hit at these prices)
# 4h/slot = net_pu * limit (buy-limit window caps per-4h volume)
# Snapshot 2026-08-27: prices.runescape.wiki mapping + latest; limits cross-checked
# against Module:Exchange/<Item> (all agree). Top 8 by 4h/slot below in profit_drivers.
# Members path (8 GE slots — the modeled economics below):
# per slot: buy `limit` units at the low edge, sell at the high edge.
# tax_pu = floor(0.02 * sell) # 2% convenience fee, rounded DOWN to
# # whole coins per item; the 5M/item cap
# # and the sub-50gp rounding exemption
# # are not hit at these prices
# net_pu = sell - tax_pu - buy # integer per-item net (floored tax)
# 4h/slot = net_pu * limit # buy-limit window caps per-4h volume
# F2P path (3 GE slots, F2P-tradeable rows only): same arithmetic, top-3
# F2P rows — see profit_drivers for the table and rate.
# Snapshots 2026-08-27: prices.runescape.wiki mapping + latest; limits and
# members flags cross-checked against the wiki item pages (all agree). Top 8
# by 4h/slot below in profit_drivers.
item: Rune arrowtips # anchor: top candidate by modeled 4h/slot (members)
item_id: 44
cost_per_cycle: 223 # buy @ low edge per unit (snapshot 2026-08-27)
income_per_cycle: 244 # sell @ high edge per unit, gross (snapshot 2026-08-27)
net_per_cycle: 16.12 # 0.98 * 244 - 223 (2% fee applied to the sale)
gp_per_hour_low: 69000 # 50% of the top-8 modeled upper bound — partial fills/drift
gp_per_hour_high: 138000 # top-8 modeled upper bound: sum(4h/slot) / 4 (8 slots, full fills)
net_per_cycle: 17 # 244 - floor(244*0.02) - 223 = 244 - 4 - 223 (floored tax)
gp_per_hour_low: 74750 # 50% of the top-8 modeled upper bound — partial fills/drift
gp_per_hour_high: 149500 # top-8 modeled upper bound: sum(4h/slot) / 4 (8 slots, full fills)
xp_per_hour: 0 # flipping awards no XP
supplies:
- item: Rune arrowtips # canonical wiki names per action-grammar conventions
@ -80,9 +97,10 @@ economy:
ge_limit: 18000
profit_drivers:
- "Spread per unit x trade volume; volume is the safety factor (high-volume items fill faster and drift less)"
- "Slot breadth: cover as many net-positive candidates as capital allows (standard 8 slots)"
- "Snapshot 2026-08-27 top 8 by modeled 4h/slot: Rune arrowtips 161,200 (buy 223 / sell 244 / limit 10000); Iron bar 88,600 (145/157/10000); Cosmic rune 61,920 (122/128/18000); Magic logs 61,680 (772/793/12000); Silver ore 57,460 (73/79/13000); Mud rune 53,640 (96/101/18000); Raw swordfish 41,100 (206/213/15000); Raw lobster 27,000 (106/110/15000)"
- "Post-2%-fee reality: 11 of 21 classic staples are net-positive at band edges; sub-2%-spread items (law/chaos/blood runes, dragon bones, adamantite/mithril bars) need sub-low buys or above-high sells"
- "Slot breadth: cover as many net-positive candidates as capital allows (members 8 slots / F2P 3 slots)"
- "Members path — snapshot 2026-08-27 top 8 by modeled 4h/slot (floored 2% tax): Rune arrowtips 170,000 (M, buy 223 / sell 244 / limit 10000); Iron bar 90,000 (F2P, 145/157/10000); Cosmic rune 72,000 (F2P, 122/128/18000); Magic logs 72,000 (M, 772/793/12000); Silver ore 65,000 (F2P, 73/79/13000); Mud rune 54,000 (M, 96/101/18000); Raw swordfish 45,000 (F2P, 206/213/15000); Raw lobster 30,000 (F2P, 106/110/15000) — total 598,000 per 4h = 149,500 gp/h on 8 slots"
- "F2P path — 3 slots, F2P-tradeable rows only: Iron bar 90,000 + Cosmic rune 72,000 + Silver ore 65,000 = 227,000 per 4h = 56,750 gp/h (alternates: Raw swordfish 45,000, Raw lobster 30,000)"
- "Post-2%-fee reality: 11 of 21 classic staples are net-positive at band edges for members; sub-2%-spread items (law/chaos/blood runes, dragon bones, adamantite/mithril bars) need sub-low buys or above-high sells"
state_machine:
initial: idle
@ -96,13 +114,16 @@ state_machine:
next: [buy, idle]
buy:
goal: place one resting buy offer per slot at or below the low edge
next: [wait_fill, collect, idle]
wait_fill:
goal: hold offers; re-quote any approaching the 4h window lapse
next: [wait_buy_fill, collect, idle]
wait_buy_fill:
goal: hold BUY offers; re-quote any approaching the 4h window lapse
next: [sell, buy, collect]
sell:
goal: place one sell offer per filled slot at or above the high edge
next: [wait_fill, collect]
next: [wait_sell_fill, collect]
wait_sell_fill:
goal: hold SELL offers; re-quote any approaching the 4h window lapse
next: [collect, sell, idle]
collect:
goal: collect proceeds, deposit coins, log the cycle
next: [scan, complete, idle]
@ -119,16 +140,22 @@ state_machine:
to: idle
- from: buy
when: "all slots quoted"
to: wait_fill
- from: wait_fill
to: wait_buy_fill
- from: wait_buy_fill
when: "all pending buys filled"
to: sell
- from: wait_fill
when: "4h window lapse with unfilled offers"
- from: wait_buy_fill
when: "4h window lapse with unfilled BUY offers"
to: collect
- from: sell
when: "sells placed"
to: wait_fill
to: wait_sell_fill
- from: wait_sell_fill
when: "all pending sells filled"
to: collect
- from: wait_sell_fill
when: "4h window lapse with unfilled SELL offers"
to: sell
- from: collect
when: "session GP target reached"
to: complete
@ -216,7 +243,7 @@ actions:
delay: [600, 1400]
run_control:
iteration: "one full buy -> fill -> sell -> collect rotation across all slots"
iteration: "one full buy -> buy-fill -> sell -> sell-fill -> collect rotation across all slots"
max_iterations: null
break:
every: [6, 10] # rotations

View File

@ -11,6 +11,10 @@ Runs:
=> WARNING (entry should be re-verified, not dropped)
- `best_practice` lines whose source starts with "researched"
=> WARNING (citation TODO — file a follow-up issue)
- GE-margin entries (tags include flipping/merchanting): the 2% GE
convenience fee is rounded DOWN to whole coins per item, so
net_per_cycle MUST equal income - floor(0.02*income) - cost.
Fractional-tax arithmetic (0.98*sell - buy) is an error.
3. Global id uniqueness across the whole codex tree.
Exit codes: 0 = clean (warnings ok), 1 = validation errors.
@ -19,6 +23,7 @@ Usage: python3 tools/validate.py [--stale-days 30]
"""
import datetime
import json
import math
import re
import sys
from pathlib import Path
@ -107,6 +112,25 @@ def main():
if len(a_steps) != len(set(a_steps)):
errors.append(f"{rel}: duplicate action step names")
# GE-margin floor-tax arithmetic. The wiki: "The 2% tax rounds down
# to the nearest whole number, so items sold for below 50 coins have
# a tax of less than one coin" — per-item net MUST use the floored
# tax, never 0.98*sell - buy. Scoped to GE-margin tags so alchemy
# (income is alch value, not a GE sale) is unaffected.
tags = {str(t).lower() for t in data.get("tags", [])}
eco = data.get("economy") or {}
if tags & {"flipping", "merchanting", "ge"} and all(
k in eco for k in ("income_per_cycle", "cost_per_cycle", "net_per_cycle")
):
sell, buy = eco["income_per_cycle"], eco["cost_per_cycle"]
tax = math.floor(sell * 0.02)
expected = sell - tax - buy
if eco["net_per_cycle"] != expected:
errors.append(
f"{rel}: net_per_cycle {eco['net_per_cycle']} != floored-tax model "
f"{expected} (sell {sell} - floor({sell}*0.02)={tax} - buy {buy}); "
f"GE tax rounds down per item — fractional 0.98*sell - buy is invalid")
# 3. staleness / research TODOs
if data.get("status") == "verified":
try: