前两节我们让市场跑起来了:价格会收敛、价差会收窄、深度会变厚。但一个「活」的市场同时也是一个「能被利用」的市场——只要两处价格不一样,就一定有人想把便宜的那边搬到贵的那边去。这就是套利。套利本身不是坏事,它是市场自我纠偏的机制;但它也有一条界线,越过这条线,套利就从「帮市场找平」变成了「从市场抽血」,而站在线那头的,通常是工作室。
先说边界。站内的经济平衡长文 用的放置类口径里没有玩家间价格,也就没有套利这一层——放置类的经济是「系统对玩家」,不存在「玩家对玩家」的价差。SLG 不同:一旦开了玩家市场,套利就是系统必须主动设计的一环。本节给两个可跑的模型——跨市场套利空间、工作室收益——再给一套阈值建议。涉及真实工作室/黑产数据的部分一律标 X,本节不编造任何真实数字。
哪里有价差,哪里就有套利
套利的机会来自同一个东西在不同地方有不同的价。SLG 里至少有三类价差:
- 跨服价差:老服金币产出多、物价低,新服金币稀缺、物价高,同一批金币在两个服值不同的钱;
- 跨资源价差:A 资源在市场上被高估、B 被低估,但两者在系统里有一个固定的兑换比(比如都用金币计价);
- 时间价差:活动前和活动后,同一资源的供需翻转,价格出现可预期的落差。
套利的收益,等于毛价差减去转移成本。转移成本包括手续费、运费(跨服/跨地块的转移损耗)、以及时间成本。只有当毛价差大于这些成本时,套利才真正有利可图。下面这份配置定义了三个市场、一套转移成本,以及一个工作室的成本结构。
{
"version": 1,
"note": "跨市场套利空间与工作室收益模型。价格、成本、费率均为整数;费率用千分比。所有真实工作室数据需业务环境,本文件为合成参数。",
"markets": [
{ "id": "home", "name": "本服主城", "goldPrice": 100 },
{ "id": "alt", "name": "跨服二区", "goldPrice": 112 },
{ "id": "rural","name": "新服", "goldPrice": 88 }
],
"transfer": { "feePermille": 50, "transportPermille": 20 },
"studio": {
"accounts": 50,
"goldPerAccountPerDay": 2000,
"goldSalePrice": 30,
"accountCostPerDay": 5,
"operatorCostPerDay": 300,
"banRatePermille": 150,
"horizonDays": 30
},
"riskThresholds": {
"healthyArbPermille": 100,
"warnArbPermille": 200,
"studioRoiWarnPct": 30,
"studioRoiBlockPct": 80
}
}
套利到什么程度是健康的
套利不是「有就要打」。恰恰相反,一点点套利是市场健康的标志——它说明价格在流动、有人愿意承担转移成本去抹平价差。真正要警惕的是套利空间大到工作室愿意为它专门开号。判断标准可以落成一个数:跨市场套利千分比——把毛价差扣掉转移成本后的净收益,除以低价侧价格,换算成千分比。
下面这段是 S 层仿真脚本,读 config/,对每一对市场算毛价差、转移成本、净套利空间与千分比,并按阈值给出「健康 / 预警 / 有害」的区间判定;同时用工作室成本结构算出一段窗口内的打金 ROI。
# file: sim/ch14_arbitrage_studio.py
# tier: S
# verify: python3 sim/ch14_arbitrage_studio.py --config config/ --out out/
"""套利空间与工作室收益模型。
1) 跨市场套利:对每一对市场算毛价差、转移成本(手续费 + 运费)、净套利空间与千分比,
并给出健康 / 预警 / 有害的区间判定。
2) 工作室打金收益:给定账号数、单账号日产金、金币售价、账号与人力成本、封禁率,
算一段窗口内的期望收入、成本、利润与 ROI。
产出 out/ch14_arbitrage.json + out/ch14_arbitrage_pairs.csv。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
def load_config(cfg_dir: str) -> dict:
with open(os.path.join(cfg_dir, "ch14_arbitrage.json"), encoding="utf-8") as f:
return json.load(f)
def arbitrage(cfg: dict) -> list[dict]:
mkt = cfg["markets"]
t = cfg["transfer"]
cost_permille = t["feePermille"] + t["transportPermille"]
thr = cfg["riskThresholds"]
rows = []
for a in range(len(mkt)):
for b in range(a + 1, len(mkt)):
lo, hi = mkt[a], mkt[b]
if lo["goldPrice"] > hi["goldPrice"]:
lo, hi = hi, lo
gross = hi["goldPrice"] - lo["goldPrice"]
cost = lo["goldPrice"] * cost_permille // 1000
net = gross - cost
arb_permille = net * 1000 // lo["goldPrice"] if lo["goldPrice"] else 0
band = "healthy" if arb_permille <= thr["healthyArbPermille"] else (
"warn" if arb_permille <= thr["warnArbPermille"] else "harmful")
rows.append({"from": lo["id"], "to": hi["id"], "lowPrice": lo["goldPrice"],
"highPrice": hi["goldPrice"], "gross": gross, "cost": cost,
"net": net, "arbPermille": arb_permille, "band": band})
return rows
def studio(cfg: dict) -> dict:
s = cfg["studio"]
h = s["horizonDays"]
gross_gold = s["accounts"] * s["goldPerAccountPerDay"] * h
sold_gold = gross_gold * (1000 - s["banRatePermille"]) // 1000
# goldSalePrice 为「每千金售价」,故除以 1000
revenue = sold_gold * s["goldSalePrice"] // 1000
cost = s["accounts"] * s["accountCostPerDay"] * h + s["operatorCostPerDay"] * h
profit = revenue - cost
roi_pct = profit * 100 // cost if cost else 0
return {"accounts": s["accounts"], "horizonDays": h,
"grossGold": gross_gold, "soldGold": sold_gold,
"revenue": revenue, "cost": cost, "profit": profit, "roiPct": roi_pct}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
cfg = load_config(args.config)
pairs = arbitrage(cfg)
st = studio(cfg)
report = {"version": 1, "transferCostPermille": cfg["transfer"]["feePermille"] + cfg["transfer"]["transportPermille"],
"thresholds": cfg["riskThresholds"], "pairs": pairs, "studio": st}
os.makedirs(args.out, exist_ok=True)
with open(os.path.join(args.out, "ch14_arbitrage.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
with open(os.path.join(args.out, "ch14_arbitrage_pairs.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["from", "to", "lowPrice", "highPrice", "gross", "cost", "net", "arbPermille", "band"])
for r in pairs:
w.writerow([r["from"], r["to"], r["lowPrice"], r["highPrice"], r["gross"],
r["cost"], r["net"], r["arbPermille"], r["band"]])
for r in pairs:
print(f"arb {r['from']:>5s}->{r['to']:<5s} net={r['net']:>4d} permille={r['arbPermille']:>4d} {r['band']}")
print(json.dumps(st, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机跑它,输出如下(同机相对量级,绝对数值不可移植):
arb home->alt net= 5 permille= 50 healthy
arb rural->home net= 6 permille= 68 healthy
arb rural->alt net= 18 permille= 204 harmful
{"accounts": 50, "horizonDays": 30, "grossGold": 3000000, "soldGold": 2550000,
"revenue": 76500, "cost": 16500, "profit": 60000, "roiPct": 363}
三对市场里,home→alt(50 千分点)和 rural→home(68 千分点)都落在健康区间,而 rural→alt(204 千分点)被判为有害。原因很直观:新服到跨服二区的价差最大(88 对 112),转移成本虽然吃掉了 6 个单位,但净空间仍有 18 个单位、折合千分比 204——这个空间大到足以支撑一条专门的跨服搬砖通道。这就是 14.1 里说的「形态决定经济形态」的具体后果:摆摊式、分散定价的市场,天然会产生这种跨市场的价差。
工作室收益模型:给定脚本成本算打金 ROI
套利空间是「机会」,工作室是「抓住机会的人」。把工作室抽象成一个成本结构,就能算出一个最关键的数——打金 ROI:
- 收入 = 账号数 × 单账号日产金 × 窗口天数 × 每千金售价 × (1 − 封禁率);
- 成本 = 账号数 × 账号日成本 × 窗口天数 + 操作者日成本 × 窗口天数;
- ROI = 利润 ÷ 成本 × 100%。
在上面的配置里,50 个账号、每账号每天产 2000 金、跑 30 天,理论产金 300 万;按 15% 封禁率折算,实际卖出 255 万金;按每千金 30 个单位的售价,收入 76500;成本 16500;利润 60000,ROI 高达 363%。
363% 是一个危险信号。它意味着:只要一台机器、50 个号、一个月,就能翻三倍多回本——这种收益水平足以让工作室把这里当成主业,甚至愿意持续补号去对抗封禁(因为补号的成本远低于收益)。风控要做的,不是「禁止一切套利」,而是把 ROI 压到「不值得专门开号」的水平。
B 基准:把套利空间与 ROI 与阈值对照
上面两个数(最大套利千分比、工作室 ROI)应该被当成基准指标持续监控。下面这段是 B 层基准脚本,把这两个数跟 config/ 里的风控阈值对照,算残差并给出软告警——软告警不硬失败,残差超阈值只打 WARN,退出码恒为 0。
# file: sim/ch14_arbitrage_bench.py
# tier: B
# verify: python3 sim/ch14_arbitrage_bench.py --config config/
"""B 基准:把套利空间与工作室 ROI 与风控阈值做对照,算残差并给出软告警。
软告警不硬失败——残差超阈值只打 WARN,退出码恒为 0。
基准点:
arb_max 最大跨市场套利千分比 vs healthyArbPermille(越大越不健康)
studio_roi 工作室 ROI 百分比 vs studioRoiWarnPct
"""
from __future__ import annotations
import argparse
import json
import os
from ch14_arbitrage_studio import arbitrage, load_config, studio
def main() -> int:
here = os.path.dirname(os.path.abspath(__file__))
ap = argparse.ArgumentParser()
ap.add_argument("--config", default=os.path.join(here, "..", "config"))
ap.add_argument("--out", default=os.path.join(here, "..", "out"))
args = ap.parse_args()
os.makedirs(args.out, exist_ok=True)
cfg = load_config(args.config)
thr = cfg["riskThresholds"]
pairs = arbitrage(cfg)
st = studio(cfg)
max_arb = max((p["arbPermille"] for p in pairs), default=0)
checks = [
{"point": "arb_max", "value": max_arb, "target": thr["healthyArbPermille"],
"residual": max_arb - thr["healthyArbPermille"],
"warn": max_arb > thr["healthyArbPermille"]},
{"point": "studio_roi", "value": st["roiPct"], "target": thr["studioRoiWarnPct"],
"residual": st["roiPct"] - thr["studioRoiWarnPct"],
"warn": st["roiPct"] > thr["studioRoiWarnPct"]},
]
res = {"version": 1, "checks": checks,
"anyWarn": any(c["warn"] for c in checks),
"maxAbsResidual": max(abs(c["residual"]) for c in checks)}
with open(os.path.join(args.out, "ch14_arbitrage_bench.json"), "w", encoding="utf-8") as f:
json.dump(res, f, ensure_ascii=False, indent=2)
for c in checks:
print(f"{'WARN' if c['warn'] else 'OK '} {c['point']:11s} value={c['value']:>4d} target={c['target']:>4d} residual={c['residual']:>5d}")
print(json.dumps({"anyWarn": res["anyWarn"], "maxAbsResidual": res["maxAbsResidual"]}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
跑它,输出是两条 WARN:
WARN arb_max value= 204 target= 100 residual= 104
WARN studio_roi value= 363 target= 30 residual= 333
{"anyWarn": true, "maxAbsResidual": 333}
两条都告警,说明当前配置是一个高风险市场:跨服套利空间(204)远超健康上限(100),工作室 ROI(363)远超预警线(30)。基准脚本的价值不在「跑一次」,而在每次改配置、每次做活动都跑一次——把「市场健不健康」从一个主观判断,变成两条会变红的指标。
风控阈值建议
把上面两个模型沉淀成一套可执行的阈值,大致是这样(区间与本节配置一致,实际数值需按你的游戏校准):
| 指标 | 阈值 | 含义 | 建议动作 |
|---|---|---|---|
| 跨市场套利千分比 | ≤ 100 | 健康 | 无需干预,套利在帮市场找平 |
| 跨市场套利千分比 | 100–200 | 预警 | 提高跨服转移手续费或降低转移额度 |
| 跨市场套利千分比 | > 200 | 有害 | 关闭或限额跨服转移通道,切断搬砖路径 |
| 工作室 ROI | ≤ 30% | 安全 | 常规监控 |
| 工作室 ROI | 30%–80% | 预警 | 加强封禁、提高账号成本、缩短结算周期 |
| 工作室 ROI | > 80% | 触发打击 | 专项治理:封号 + 转移限额 + 产出侧加税 |
核心思路是别跟套利本身较劲,去跟「套利的收益」较劲。价格差抹不平,就抬高转移成本(手续费/运费);封禁封不完,就让每个号的期望收益降到不值得开号。这两招都落在「成本」这一侧,而不是「价格」那一侧——因为价格是市场的,成本是你可控的。
X:真实工作室数据不可复现
必须说清楚:本节所有数字都来自 config/ 里的合成参数,不是任何真实游戏的真实后台数据。真实工作室的规模、成本、封禁对抗手段、变现渠道,都需要真实业务环境才能观测,命令行不可复现——这类内容在本卷一律标 X。本节能给你的,是一套可以被你自己的数据填进去的模型,而不是一组可以直接照抄的数字。
下一章我们离开「套利与工作室」这个偏经济的视角,转向经济风控与事故复盘:怎么用埋点做异常检测、怎么把一次经济事故写成可复用的案例库、出了事怎么修复与回滚。15.1 讲异常检测与打金识别。
阅读导航:上一节:定价机制与流动性 · 下一节:异常检测与打金识别 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。