无本套利 Agent
基于 Basic 蓝图实现 FFP 协议中的无本套利 Agent。该 Agent 能够在无需资金成本的情况下,基于 FFP 协议中的价差自动执行套利操作。
以下以 wAR/wUSDC 交易对为例,介绍无损套利 Agent 的实现过程。
目标场景
监听 FFP 协议中 wAR/wUSDC 交易对的兑换订单:
Agent 实时从 FFP 结算中心获取等待执行的 wAR/wUSDC 交易对的挂单。
把每个挂单价格和 FFP 中的 wAR/wUSDC AMM Agent 对比价差。
当两者存在价格差时执行套利:
假设发现某个用户挂单为 1 wAR 兑换 20 wUSDC。
wAR/wUSDC AMM Agent 此时价格为 1 wAR 兑换 21 wUSDC。
套利操作为:
请求 AMM Agent 创建一个 21 wUSDC 兑换 1wAR 的挂单到 FFP。
套利 Agent 把用户的挂单给 AMM Agent 创建的挂单提交给 FFP 结算中心。
结算中心的结算逻辑是 收到用户的 1 wAR 和 AMM Agent 发出的 21 wUSDC, 然后转出 20 wUSDC 给用户,1 wAR 给 AMM Agent,最终结余 1 wUSDC。
结算中心会把结余的 1 wUSDC 发送给套利 Agent,因为这笔订单的结算者为套利 Agent。
套利 Agent 代码解析
arbitrage.lua
local json = require('json')
local utils = require('.utils')
local bint = require('.bint')(512)
ProcessCfg = {
PageSize = '10',
PoolPid = 'vJY-ed1Aoa0pGgQ30BcpO9ehGBu1PfNHUlwV9W8_n5A', -- wUSDC/wAR
wUSDCPid = '7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ', -- Y
wARPid = 'xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10' -- X
}
--[[
example-notes:
[
{
"Price": 0.000023,
"IssueDate": 1732447831435,
"Status": "Open",
"ID": 2303,
"HolderAssetID": "7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ",
"AssetID": "xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10",
"Amount": "50000000000",
"Issuer": "ZsXb0JKGATswuk-qRs3iV49VbTbaiaMCEPYqk6GKdW0",
"HolderAmount": "1150000",
"NoteID": "ssfaQ3p3Ufj1rcKj1ZXVW3aYYW2zhcfAQ8vvIkSOsD4"
}
]
-- ]]
function getNotesFromSettle(assetID, holderAssetID, page)
print('getNotesFromSettle...'..assetID)
local res = Send({
Target = FFP.Settle,
Action = 'GetNotes',
AssetID = assetID,
HolderAssetID = holderAssetID,
Order = 'desc',
Status = 'Open',
Page = tostring(page),
PageSize = ProcessCfg.PageSize
}).receive()
local notes = json.decode(res.Data)
print('got notes: '.. #notes)
return notes
end
function getPoolInfo()
print('getPoolInfo: ' .. ProcessCfg.PoolPid)
local res = Send({
Target = ProcessCfg.PoolPid,
Action = 'Info',
}).receive()
if not res then
print('Error calling Send: ')
return {}
end
print('got poolInfo')
return {
["Executing"] = res.Tags.Executing,
["PX"] = res.Tags.PX,
["PY"] = res.Tags.PY,
["X"] = res.Tags.X,
["Y"] = res.Tags.Y,
["Fee"] = res.Tags.Fee
}
end
-- return amountOut
local function calcAmmInputPrice(amountIn, reserveIn, reserveOut, Fee)
local amountInWithFee = bint.__mul(amountIn, bint.__sub(10000, Fee))
local numerator = bint.__mul(amountInWithFee, reserveOut)
local denominator = bint.__add(bint.__mul(10000, reserveIn), amountInWithFee)
return bint.udiv(numerator, denominator)
end
-- makeOrder for amm pool
local function makeOrderForAmmPool(tokenIn, amountIn, tokenOut, amountOut)
print('makeOrderForAmmPool...')
local res = Send({
Target = ProcessCfg.PoolPid,
Action = 'MakeOrder',
TokenIn = tokenIn,
AmountIn = amountIn,
TokenOut = tokenOut,
AmountOut = amountOut
}).receive()
if res.Error then
return {
["Error"] = res.Error
}
end
local note = json.decode(res.Data)
return {
['Note'] = note
}
end
local function takeOrder(noteIDs)
-- start Settle
local res = Send({
Target = ao.id,
Action = 'FFP.TakeOrder',
Data = json.encode(noteIDs)
}).receive()
if res.Error then
return {
["Error"] = res.Error
}
end
return {}
end
-- arbitrageExecutor
function perNoteArbitrageExecutor(note)
local tokenIn = note.AssetID
local amountIn = note.Amount
local tokenOut = note.HolderAssetID
local expectedAmountOut = note.HolderAmount
local poolInfo = getPoolInfo()
if poolInfo.Executing == 'true' then
print('ammPool is executing...')
return {
["Error"] = 'err_pool_executing'
}
end
local reserveIn = poolInfo.PX
local reserveOut = poolInfo.PY
if tokenIn == poolInfo.Y then
reserveIn, reserveOut = reserveOut, reserveIn
end
local amountOut = calcAmmInputPrice(bint(amountIn), bint(reserveIn), bint(reserveOut), bint(poolInfo.Fee))
if utils.lt(amountOut, expectedAmountOut) then
print('amount out too small')
return {
['Error'] = 'amm_prices_low'
} -- can not settle, because pool price not ok
end
-- could process settle
-- makeOrder for amm pool
local ord = makeOrderForAmmPool(tokenIn, amountIn, tokenOut, tostring(amountOut))
if ord.Error then
print('makeOrderForAmmPool failed: ' .. ord.Error)
return {
['Error'] = ord.Error
}
end
print('amm makeorder note: '.. ord.Note.NoteID)
-- calc settle income and outcome
local si, so = utils.SettlerIO({ord.Note, note})
if next(so) then
print('some calc error, settlerout must be nil')
return {
["Error"] = 'err_settler_out'
}
end
-- take order use basic api
print('Taking order for NoteIDs: ' .. json.encode({ord.Note.NoteID, note.NoteID}))
local res = takeOrder({ord.Note.NoteID, note.NoteID})
if res.Error then
print('take order failed: ' .. res.Error)
return {
['Error'] = res.Error
}
end
return {
['SettlerIn'] = si
}
end
function arbitrageExec(assetID, holderAssetID)
local page = 1
while true do
-- get wUSDC swap wAR notes
local notes = getNotesFromSettle(assetID, holderAssetID, page)
if #notes == 0 then
print('not notes...')
return
end
for _, note in ipairs(notes) do
-- execute per note
local res = perNoteArbitrageExecutor(note)
if res.Error then
print('perNoteArbitrageExecutor failed: '..res.Error..' noteID: '.. note.NoteID)
else
print('success arbitrage note: '.. note.NoteID .. ' settlerIn: ' .. json.encode(res.SettlerIn))
end
end
if #notes < tonumber(ProcessCfg.PageSize) then
break
end
page = page + 1
end
end
function ArbitrageExecutor()
print('start wAR-wUSDC notes arbitrage...')
arbitrageExec(ProcessCfg.wARPid,ProcessCfg.wUSDCPid)
print('end wAR-wUSDC pairs arbitrage...')
print('start wUSDC-wAR notes arbitrage...')
arbitrageExec(ProcessCfg.wUSDCPid,ProcessCfg.wARPid)
print('end wUSDC-wAR notes arbitrage...')
end
return {
ArbitrageExecutor = ArbitrageExecutor
}
agent.lua
local json = require('json')
local bint = require('.bint')(512)
local utils = require('.utils')
local agent = require('.arbAgent')
FFP = FFP or {}
-- config
FFP.Settle = FFP.Settle or 'rKpOUxssKxgfXQOpaCq22npHno6oRw66L3kZeoo_Ndk'
FFP.Version = FFP.Version or '0.31'
FFP.MaxNotesToSettle = FFP.MaxNotesToSettle or 2
-- database
FFP.Notes = FFP.Notes or {}
FFP.Settled = FFP.Settled or {}
Handlers.add('ffp.withdraw', 'FFP.Withdraw',
function(msg)
assert(msg.From == Owner or msg.From == ao.id, 'Only owner can withdraw')
assert(type(msg.Token) == 'string', 'Token is required')
assert(type(msg.Amount) == 'string', 'Amount is required')
assert(bint.__lt(0, bint(msg.Amount)), 'Amount must be greater than 0')
Send({
Target = msg.Token,
Action = 'Transfer',
Quantity = msg.Amount,
Recipient = Owner
})
end
)
Handlers.add('ffp.takeOrder', 'FFP.TakeOrder',
function(msg)
assert(msg.From == Owner or msg.From == ao.id, 'Only owner can make order')
local noteIDs = utils.getNoteIDs(msg.Data)
if noteIDs == nil then
msg.reply({Error = 'err_invalid_note_ids', ['X-FFP-TakeOrderID'] = msg.Id})
return
end
if #noteIDs > FFP.MaxNotesToSettle then
msg.reply({Error = 'err_too_many_orders', ['X-FFP-TakeOrderID'] = msg.Id})
return
end
local notes = {}
for _, noteID in ipairs(noteIDs) do
local data = Send({Target = FFP.Settle, Action = 'GetNote', NoteID = noteID}).receive().Data
if data == '' then
msg.reply({Error = 'err_not_found', ['X-FFP-TakeOrderID'] = msg.Id})
return
end
local note = json.decode(data)
if note.Status ~= 'Open' then
msg.reply({Error = 'err_not_open', ['X-FFP-TakeOrderID'] = msg.Id, Data = noteID})
return
end
if note.Issuer == ao.id then
msg.reply({Error = 'err_cannot_take_self_order', ['X-FFP-TakeOrderID'] = msg.Id, Data = noteID})
return
end
if note.ExpireDate and note.ExpireDate < msg.Timestamp then
msg.reply({Error = 'err_expired', ['X-FFP-TakeOrderID'] = msg.Id, Data = noteID})
return
end
table.insert(notes, note)
end
local si, so = utils.SettlerIO(notes)
-- todo: make sure we have enough balance
-- start a settle session
local res = Send({Target = FFP.Settle, Action = 'StartSettle', Version = FFP.Version, Data = json.encode(noteIDs)}).receive()
if res.Error then
msg.reply({Error = res.Error, ['X-FFP-TakeOrderID'] = msg.Id})
return
end
local settleID = res.SettleID
FFP.Settled[settleID] = {SettleID = settleID, NoteIDs = noteIDs, Status = 'Pending'}
if next(so) == nil then
print('TakeOrder: Settler no need to transfer to settle process')
Send({Target = FFP.Settle, Action = 'Settle', Version = FFP.Version, SettleID = settleID})
msg.reply({SettleID = settleID})
return
end
for k, v in pairs(so) do
local amount = utils.subtract('0', v)
Send({Target = k, Action = 'Transfer', Quantity = amount, Recipient = FFP.Settle,
['X-FFP-SettleID'] = settleID,
['X-FFP-For'] = 'Settle'
})
end
msg.reply({SettleID = settleID})
end
)
Handlers.add('ffp.done',
function(msg)
return (msg.Action == 'Credit-Notice') and (msg['X-FFP-For'] == 'Settled' or msg['X-FFP-For'] == 'Refund')
end,
function(msg)
if msg.Sender ~= FFP.Settle then
print('Only settle can send settled notice')
return
end
local settleID = msg['X-FFP-SettleID']
if settleID and FFP.Settled[settleID] then
if msg['X-FFP-For'] == 'Settled' then
FFP.Settled[settleID].Status = 'Settled'
else
FFP.Settled[settleID].Status = 'Rejected'
end
FFP.Settled[settleID].SettledDate = msg['X-FFP-SettledDate']
for _, noteID in ipairs(FFP.Settled[settleID].NoteIDs) do
local data = Send({Target = FFP.Settle, Action = 'GetNote', NoteID = noteID}).receive().Data
FFP.Notes[noteID] = json.decode(data)
end
else
print('SettleID not found: ' .. settleID)
end
end
)
Handlers.add('ffp.getOrders', 'FFP.GetOrders',
function (msg)
msg.reply({Action = 'GetOrders-Notice', Data = json.encode(FFP.Notes)})
end
)
Handlers.add('ffp.getSettled', 'FFP.GetSettled',
function (msg)
msg.reply({Action = 'GetSettled-Notice', Data = json.encode(FFP.Settled)})
end
)
-- Trigger
ExecuteState = ExecuteState or 'ON'
Handlers.add(
"CronTick",
Handlers.utils.hasMatchingTag("Action", "Cron"), -- Send({Target=ao.id,Action='Cron'})
function()
print('Cron trigger arbitrage ...')
if ExecuteState == 'ON' then
-- find note: wUSDC swap wAR
ExecuteState = 'OFF'
agent.ArbitrageExecutor()
ExecuteState = 'ON'
print('finished..')
agent.Withdrawal()
print('finish withdrawal...')
else
print('ExecuteState is off...')
end
end
)
utils.lua
local json = require('json')
local bint = require('.bint')(512)
local mod = {
add = function(a, b)
return tostring(bint(a) + bint(b))
end,
subtract = function(a, b)
return tostring(bint(a) - bint(b))
end,
toBalanceValue = function(a)
return tostring(bint(a))
end,
toNumber = function(a)
return tonumber(a)
end,
lt = function (a, b)
return bint.__lt(bint(a), bint(b))
end
}
mod.getNoteIDs = function(data)
if string.find(data, "null") then
return nil
end
local noteIDs, err = json.decode(data)
if err then
return nil
end
if type(noteIDs) == "string" then
return {noteIDs}
end
if type(noteIDs) == "table" then
for i = 1, #noteIDs do
if noteIDs[i] == nil or type(noteIDs[i]) ~= "string" then
return nil
end
end
return noteIDs
end
return nil
end
mod.SettlerIO = function (notes)
local settlerIn = {}
local settlerOut = {}
local bc = {}
for _, note in ipairs(notes) do
if not bc[note.AssetID] then bc[note.AssetID] = bint(0) end
if not bc[note.HolderAssetID] then bc[note.HolderAssetID] = bint(0) end
bc[note.AssetID] = mod.add(bc[note.AssetID], note.Amount)
bc[note.HolderAssetID] = mod.subtract(bc[note.HolderAssetID], note.HolderAmount)
end
for k, v in pairs(bc) do
if v == '0' then bc[k] = nil end
end
for k, v in pairs(bc) do
if mod.lt(v, '0') then
settlerOut[k] = v
else
settlerIn[k] = v
end
end
return settlerIn, settlerOut
end
return mod
代码结构
arbitrage.lua 实现套利的所有执行逻辑。
agent.lua 在 basic.lua 代码基础上添加了套利相关的方法。
utils.lua 是 basic 蓝图复制过来的。
核心方法解读
1. ArbitrageExecutor
套利总调度器,调度整个套利过程:
首先处理 wAR -> wUSDC 的挂单套利。
然后处理 wUSDC -> wAR 的挂单套利。
这是为了覆盖两种可能得套利方向。
2. arbitrageExec
用户执行某个交易方向的所有挂单的套利操作:
通过传入的两个资产类型参数,确定需要执行套利方向。
从 FFP 中分页获取挂单(Notes)数据。
对每个 Note 执行套利操作。
3. getNotesFromSettle
专门用于从 FFP 中获取挂单函数:
通过资产类型和 Note 状态筛选需要获取的 Notes 从 Settle Process 中获取
4. perNoteArbitrageExecutor
处理单个 Note 的套利逻辑:
从 AMM Agent 获取流动性信息,确保池状态可用(未锁定)。
根据池流动性信息计算兑换价格:
如果 AMM 的价格小于 Note 的目标价格,则退出套利(价格不匹配)
请求 AMM 挂单:
如果 AMM 的价格大于 Note 的目标价格,会请求 AMM Agent 按照最好的价格发起一个 Note 对手单 Note,这两个 Note 的价差就是套利收益。
比如 Note 是 1 wAR 兑换 20 wUSDC,如果 AMM 此时的价格可以发起 Note 为 21 wUSDC 兑换 1 wAR ,这两个 Note 结算之后会有 1 wUSDC 的价差。
套利结算:
调用 takeOrder 发起结算,完成套利操作。
takeOrder 是 basic 蓝图实现的方法,套利 Agent 可以直接调用。
等待结算中心完成结算之后,Agent 就自动收取到两个 Note 的价差收益。
5. calcAmmInputPrice
该方法基于恒定乘积公式(UniswapV2 算法)和手续费计算 AMM 的兑换价格。在 perNoteArbitrageExecutor 方法中,首先请求 AMM 池信息,获取 reserveIn、reserveOut 和 Fee 参数。随后,从挂单(Note)中提取输入金额 AmountIn,并计算 AMM 当前可兑换的 AmountOut。接着,将 AmountOut 与挂单中的期望兑换金额进行对比。如果 AmountOut 大于期望值,则存在套利空间,继续执行套利操作。
Last updated
Was this helpful?