无本套利 Agent

基于 Basic 蓝图实现 FFP 协议中的无本套利 Agent。该 Agent 能够在无需资金成本的情况下,基于 FFP 协议中的价差自动执行套利操作。

以下以 wAR/wUSDC 交易对为例,介绍无损套利 Agent 的实现过程。

目标场景

  1. 监听 FFP 协议中 wAR/wUSDC 交易对的兑换订单:

    1. Agent 实时从 FFP 结算中心获取等待执行的 wAR/wUSDC 交易对的挂单。

    2. 把每个挂单价格和 FFP 中的 wAR/wUSDC AMM Agent 对比价差。

  2. 当两者存在价格差时执行套利:

    1. 假设发现某个用户挂单为 1 wAR 兑换 20 wUSDC。

    2. wAR/wUSDC AMM Agent 此时价格为 1 wAR 兑换 21 wUSDC。

    3. 套利操作为:

      1. 请求 AMM Agent 创建一个 21 wUSDC 兑换 1wAR 的挂单到 FFP。

      2. 套利 Agent 把用户的挂单给 AMM Agent 创建的挂单提交给 FFP 结算中心。

      3. 结算中心的结算逻辑是 收到用户的 1 wAR 和 AMM Agent 发出的 21 wUSDC, 然后转出 20 wUSDC 给用户,1 wAR 给 AMM Agent,最终结余 1 wUSDC。

      4. 结算中心会把结余的 1 wUSDC 发送给套利 Agent,因为这笔订单的结算者为套利 Agent。

套利 Agent 代码解析

chevron-rightarbitrage.luahashtag
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
}
chevron-rightagent.luahashtag
chevron-rightutils.luahashtag

代码结构

  • 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