14.3 套利、工作室与风控边界

哪里有价差,哪里就有套利。本节先讲清套利的经济学:跨市场价差扣掉手续费与运费后还剩多少,什么程度是健康的、什么程度会招来工作室;再给一个可跑的工作室收益模型,用账号数、日产金、封禁率与成本算出打金 ROI,并给出跨市场套利千分比与 ROI 的风控阈值建议。

前两节我们让市场跑起来了:价格会收敛、价差会收窄、深度会变厚。但一个「活」的市场同时也是一个「能被利用」的市场——只要两处价格不一样,就一定有人想把便宜的那边搬到贵的那边去。这就是套利。套利本身不是坏事,它是市场自我纠偏的机制;但它也有一条界线,越过这条线,套利就从「帮市场找平」变成了「从市场抽血」,而站在线那头的,通常是工作室。

先说边界。站内的经济平衡长文 用的放置类口径里没有玩家间价格,也就没有套利这一层——放置类的经济是「系统对玩家」,不存在「玩家对玩家」的价差。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%安全常规监控
工作室 ROI30%–80%预警加强封禁、提高账号成本、缩短结算周期
工作室 ROI> 80%触发打击专项治理:封号 + 转移限额 + 产出侧加税

核心思路是别跟套利本身较劲,去跟「套利的收益」较劲。价格差抹不平,就抬高转移成本(手续费/运费);封禁封不完,就让每个号的期望收益降到不值得开号。这两招都落在「成本」这一侧,而不是「价格」那一侧——因为价格是市场的,成本是你可控的。

X:真实工作室数据不可复现

必须说清楚:本节所有数字都来自 config/ 里的合成参数,不是任何真实游戏的真实后台数据。真实工作室的规模、成本、封禁对抗手段、变现渠道,都需要真实业务环境才能观测,命令行不可复现——这类内容在本卷一律标 X。本节能给你的,是一套可以被你自己的数据填进去的模型,而不是一组可以直接照抄的数字。

下一章我们离开「套利与工作室」这个偏经济的视角,转向经济风控与事故复盘:怎么用埋点做异常检测、怎么把一次经济事故写成可复用的案例库、出了事怎么修复与回滚。15.1 讲异常检测与打金识别。

阅读导航:上一节:定价机制与流动性 · 下一节:异常检测与打金识别 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「slg」更多文章

  1. 12.3 留存与付费的联合分析
  2. 12.2 流失预警与召回
  3. 附录 C:全书公式、参数与数据集索引