这份演示运行真实的 Wasm CPython,Host 工具使用本地模拟行情和账户。你可以改价格上限、资金、数量或 Python 分支,直接观察哪些调用发生、哪些没有发生。
默认 ACME 价格为 100,初始现金 2000,计划买入 10 股,最高接受价格 110。金额使用演示用整数单位。每次命令都会创建一个新模拟账户,订单只保存在本轮内存中。
Bash 快捷入口
更新仓库后,可以直接运行:
./demos/01-price-skip.sh # 价格超上限:不调用 buy
./demos/02-cash-skip.sh # 现金不足:不调用 buy
./demos/03-host-denied.sh # 条件满足,Host 拒绝
./demos/04-approved.sh # Host 允许本地模拟下单
./demos/05-prefix.sh # 按片段发送源码,默认不允许下单
./demos/06-plm-source.sh # 显示实际 AST 改写
交易脚本默认给读取加 200ms 模拟延迟,保留完整时间戳日志。参数可以追加,后面的同名参数覆盖预设:
./demos/04-approved.sh -quantity 5 -price 95
./demos/05-prefix.sh -allow-order -memory copy
./demos/trade.sh -mode normal -read-delay 0
./demos/trade.sh -inputs '{"limit":110,"quantity":3}'
通用 Python 示例入口:
./demos/run.sh
./demos/run.sh examples/order.py -plm -show-transformed
./demos/run.sh examples/branch.py -inputs '{"item":"book","quantity":2,"delivery":false}'
脚本自动切到仓库根目录,因此也能用绝对路径从其他目录启动。source 的相对路径按仓库根目录解析;日志、stdout 和错误退出码原样保留。已有 dist/spine.wasm 即可,脚本不会下载依赖或重建 Guest。
1. 先运行,看到拒绝下单
从仓库根目录执行,前提是已有 dist/spine.wasm:
go run ./cmd/trade-demo
价格和现金条件都满足,但 Host 默认未允许下单。因此这次会读取 quote 和 cash,进入 buy 工具后收到拒绝,最终 Python 返回 blocked。结果摘要包含:
{"decision":"blocked","buy_attempts":1,"orders":[],"remaining_cash":2000}
这里摘出的是结果中的核心字段;命令的完整 JSON 还包含 simulation、mode、tool_calls 和 Python result。
调用日志写到 stderr,最终结果写到 stdout。result 来自 Python 程序;判断模拟下单是否发生,要看 Host 记录的 orders 和 order.committed。单独修改 Python 的结果字段不会改变这些账户记录。需要分别保存时:
go run ./cmd/trade-demo > result.json 2> trace.jsonl
2. 四种具体情境
下面四条命令只改变输入和 Host 许可。默认模式为 PLM。
价格太高:分支没有进入
go run ./cmd/trade-demo -limit 90 -allow-order
价格 100 高于上限 90。即使 Host 允许模拟下单,Python 也不会走到 buy。实际结果为 hold,buy_attempts 为 0,orders 为空。
现金不足:只完成读取和判断
go run ./cmd/trade-demo -cash 500 -allow-order
计划成本 1000,高于现金 500。两个读取完成后返回 hold;buy_attempts 仍为 0。
条件满足,但 Host 未允许
go run ./cmd/trade-demo -limit 110
会出现 tool.start/buy,然后是 order.denied 和 tool.error。Python 的 except 接住 RuntimeError,保留价格和现金快照,返回 blocked。账户不变。
Host 允许本地模拟下单
go run ./cmd/trade-demo -limit 110 -allow-order
实际产生一张本地模拟订单:10 股 × 100,成本 1000,剩余现金 1000。日志包含 order.authorized、order.committed,最终 decision 为 bought。
| 情境 | quote/cash | buy 调用次数 | 模拟订单数 | 剩余现金 |
|---|---|---|---|---|
| 价格超上限 | 各一次 | 0 | 0 | 2000 |
| 现金不足 | 各一次 | 0 | 0 | 500 |
| Host 未允许 | 各一次 | 1 | 0 | 2000 |
| Host 允许 | 各一次 | 1 | 1 | 1000 |
这张表对应本次实际运行的四组结果。
3. Python 程序怎样决定执行范围
examples/trading.py · L1–L14 · 打开完整源码
price = quote(symbol=inputs["symbol"])
cash_balance = cash()
if price <= inputs["limit"]:
cost = price * inputs["quantity"]
if cash_balance >= cost:
try:
order = buy(symbol=inputs["symbol"], quantity=inputs["quantity"])
result = {"decision": "bought", "price": price, "cash_snapshot": cash_balance, "order": order}
except RuntimeError:
result = {"decision": "blocked", "price": price, "cash_snapshot": cash_balance, "reason": "Host refused the simulated order"}
else:
result = {"decision": "hold", "price": price, "cash_snapshot": cash_balance, "reason": "insufficient cash"}
else:
result = {"decision": "hold", "price": price, "cash_snapshot": cash_balance, "reason": "price above limit"}
quote 和 cash 是开头两个独立读取。价格上限控制外层 if,现金检查控制内层 if,buy 位于两层条件之内。
当价格超上限或现金不足时,Python 直接构造 hold 结果。进入买入路径后,Host 仍有自己的检查;权限缺失、数量无效或当前账户资金不足都会让 buy 返回错误。这个例子用固定 reason 表示 blocked,具体原因查看 tool.error 日志。
4. 时间戳怎样读
每条日志是一行 JSON,包含:
time:实际 UTC 时间,RFC3339 格式,带小数秒。elapsed_ms:从本次演示初始化日志器开始的耗时,使用 Go 的单调时钟差值。seq:这条日志的序号;并发日志在写出时串行编号。event:发生的事件。tool:涉及的工具名。data:参数、返回值、拒绝原因或订单内容。
runner.create/ready 表示 runtime 创建过程。tool.start 是 Host 工具函数开始处理请求的记录,tool.end/error 是它返回的记录。order.committed 才表示本地模拟账户已发生写入。
日志没有把每次 prepare/resolve ABI 调用都加进运行时。要看改写位置,使用下一节的 show-transformed;要看工具实际开始和结束,使用时间戳。
用可控读取延迟观察重叠
go run ./cmd/trade-demo -mode normal -read-delay 200ms -allow-order
go run ./cmd/trade-demo -mode plm -read-delay 200ms -allow-order
read-delay 是演示工具主动等待的模拟延迟。观察工具开始和结束的相对顺序即可;整条命令还包含编译、Guest 初始化和日志开销,不用总耗时推算系统性能。
下面摘自一次真实 PLM 演示的日志。quote 与 cash 都开始后,才出现两个读取的结束事件;buy 随后进入。
{"time": "2026-09-13T09:07:54.6961Z", "elapsed_ms": 1923.571, "seq": 5, "event": "tool.start", "tool": "quote", "data": {"symbol": "ACME"}}
{"time": "2026-09-13T09:07:54.696163Z", "elapsed_ms": 1923.634, "seq": 6, "event": "tool.start", "tool": "cash", "data": {}}
{"time": "2026-09-13T09:07:54.898225Z", "elapsed_ms": 2125.697, "seq": 7, "event": "tool.end", "tool": "cash", "data": {"result": 2000}}
{"time": "2026-09-13T09:07:54.898403Z", "elapsed_ms": 2125.875, "seq": 8, "event": "tool.end", "tool": "quote", "data": {"result": 100}}
{"time": "2026-09-13T09:07:54.899427Z", "elapsed_ms": 2126.898, "seq": 9, "event": "tool.start", "tool": "buy", "data": {"symbol": "ACME", "quantity": 10}}
{"time": "2026-09-13T09:07:54.899509Z", "elapsed_ms": 2126.981, "seq": 10, "event": "order.authorized", "tool": "buy", "data": {"simulation": true}}
{"time": "2026-09-13T09:07:54.899539Z", "elapsed_ms": 2127.01, "seq": 11, "event": "order.committed", "tool": "buy", "data": {"order": {"id": 1, "symbol": "ACME", "quantity": 10, "price": 100, "cost": 1000}, "remaining_cash": 1000, "simulation": true}}
{"time": "2026-09-13T09:07:54.899591Z", "elapsed_ms": 2127.063, "seq": 12, "event": "tool.end", "tool": "buy", "data": {"result": {"id": 1, "symbol": "ACME", "quantity": 10, "price": 100, "cost": 1000}}}
不同机器和每次运行的时间戳会变化,两个读取的完成顺序也可以变化。
5. 同一个例子跑三种模式
go run ./cmd/trade-demo -mode normal
go run ./cmd/trade-demo -mode plm -show-transformed
go run ./cmd/trade-demo -mode prefix -read-delay 200ms
PLM 版本先为 quote 和 cash 创建准备请求,再在原赋值位置领取结果。分支保持在 Python 中。
改写后的买入分支里也可能出现 _spine_prepare:Host 提前读取名单只有 quote 和 cash,buy 的 prepare 返回 0,随后到原调用位置才普通执行。是否允许账户写入,继续由 buy 内部的 Host 许可检查决定。
Prefix 版本按行发送文件片段。source.send 记录准备发送的片段,不能据此断言 Guest 已处理完成;tool.start 才记录实际工具处理开始。source.closed 记录源码 channel 已关闭,最终执行随后按正常路径进行。若在接收中取消,还会出现 source.cancelled。
CLI 发送片段很快,日志顺序由实际调度决定。要验证“收到含买入分支的完整文本但尚未 EOF 时,只有安全读取启动”,运行握手测试:
go test ./cmd/trade-demo -run TestTradingPrefixReadsBeforeEOFWithoutBuying -count=1 -v
测试先保持源码流打开,等待 quote 和 cash 实际启动,检查 buy 尚未调用,然后关闭流并验证只下单一次。
已有 Linux COW 环境还可以使用 -memory cow;full-copy 用 -memory copy。默认 fresh,模式切换沿用现有 Runner 构造函数。
6. 可以怎样修改
# 改模拟行情、资金和策略输入
go run ./cmd/trade-demo -price 95 -cash 1500 -limit 100 -quantity 5 -allow-order
# 替换完整 Python 文件
go run ./cmd/trade-demo -source examples/trading.py -mode prefix
# 覆盖 Python 输入,不改变 Host 许可
go run ./cmd/trade-demo -inputs '{"limit":110,"quantity":10,"approved":true}'
可以编辑 examples/trading.py 的分支、数量计算和结果字段,重跑即可,无需重建 Guest。工具行为在 market.go,命令参数和源码传输在 main.go。
cash 返回本轮启动时冻结的现金快照,让它满足提前读取的稳定性约定。buy 每次都重新检查当前剩余现金,不能靠 Python 的早期判断批准透支。真实系统接入余额读取时,也需要明确它是固定快照还是实时变化的数据。
如果加入当前 AST pass 不支持的语法,程序会回退到正常执行。这个回退负责保持程序语义,危险操作的许可仍需要 Host 检查。仅给 Python inputs 添加 approved 字段,无法改变 Host 的 allow-order 设置。
7. 验证范围
测试覆盖普通、PLM、prefix 三种模式下的价格跳过、资金跳过、Host 拒绝、允许下单和非法数量。还检查了 buy 不能提前准备、Guest 无法自行授予许可、重复下单时 Host 重新核对资金,以及时间戳和事件顺序。
go test ./cmd/trade-demo -count=1 -v
go test -race ./cmd/trade-demo -count=1
这里所有交易都是本轮内存中的模拟写入,没有券商连接、真实行情或持久订单。取消会请求正在等待的工具退出;它不会撤销已经提交的模拟订单,demo 也没有自动重试订单。