‖ Jev Trader

FAQ / Trading

Jev 如何驱动交易:从行情到订单

一个自动交易程序,需要不断重复三个动作:看行情、作判断、处理订单。

在 jev-trader 里,Jev 负责中间的判断。程序把行情整理好,问它更适合买还是卖,再根据回答和账户限制处理挂单。

本文沿着这条流程,介绍业务逻辑、Prompt 设计和接口参数。代码中的字段均附中文注释。

以下按本文核对的源码描述实现。Hyperliquid 使用 HYPE/USDC 真实现货行情和模拟账户;Kuru 保留链上订单代码,但当前页面入口只启动模拟会话。本文不把它描述成已经完成的合约交易系统。

一、业务流程:模型回答以后,才轮到执行

订单簿是还没有成交的买卖报价清单。程序读取订单簿和近期成交,将它们转换成模型能直接使用的数据。

读取行情报价、挂单量、近期成交
整理 state价差、涨跌幅、允许方向
Jev 选择买入或卖出只提供判断,不直接下单
执行层检查余额和仓位有允许执行的方向吗?
有 ↓
撤换挂单后续成交后更新账户
没有 ↓
不放置新订单记录本轮状态
展示判断、订单和盈亏新行情到来,再进入下一轮 ↻

图 1:主业务流程。Hyperliquid 路径中的订单与成交均为模拟。

这里有三个不同的时刻:模型作出选择、程序放置订单、订单真正成交。

例如,Jev 选择买入,程序就考虑挂一张买单。挂单只是报价,还要有人愿意卖给你,才会成交。当前策略偏向 post-only,也就是只挂在订单簿上,不立即吃掉对手方报价。

模型不直接决定订单数量,也不签名或操作钱包。执行层根据预设规则计算订单。如果模型选择的方向受余额或仓位限制,当前代码可能改用另一侧;两侧都不允许时,就不放置新订单。

二、接口结构:材料、问题、答案

Jev 的接口可以理解成做选择题。你提供材料和选项,它返回选择结果。

state现在发生了什么
questions判断什么,怎样选择
Jev根据材料回答问题
answers → Decision选项与概率 → 项目使用的结果
执行层再次检查约束,处理订单

图 2:模型返回的是判断,交易程序继续负责执行。

statequestions 是接口结构;midtradesallowed 等名字,是这个项目自己设计的业务字段。TypeSafe 接口文档

先看模型配置:

# 使用 Jev;项目默认 mock 是本地模拟模型,不请求 Jev。
MODEL=jev

# 服务端认证密钥,由 provider 读取,不放入行情或调用记录。
TYPESAFE_AI_API_KEY=你的_API_密钥

# 请求的模型名称,项目默认使用这个别名。
JEV_MODEL_ID=jev-latest

模型配置与交易模式是两件事。调用真实 Jev,也可以只做模拟交易。

项目通过 AI SDK 调用模型。下面保留调用结构,stateQUESTIONS 在后文解释:

import { experimental_evaluate } from "ai";
import { typeSafeAi } from "@ai-sdk/typesafe-ai";

// 实际项目读取 config.jevModelId,这里展示默认值。
const model = typeSafeAi.evaluationModel("jev-latest");

// 固定本次材料,避免异步等待期间原对象变化。
const input = structuredClone({
  state,                 // 本次行情和允许方向。
  questions: QUESTIONS,  // 本次问题集合。
});

const result = await experimental_evaluate({
  model,                       // provider 创建的模型实例,不是字符串。
  state: input.state,          // 交给模型判断的业务数据。
  questions: input.questions,  // 问题说明与候选答案。
  maxRetries: 0,               // 禁用 SDK 自动重试,避免在旧行情上反复请求。
});

maxRetries: 0 不是超时设置。这段调用没有设置取消信号,不能理解成超过一个区块时间就自动终止。

三、state:给模型看什么?

输入主要回答四件事:交易哪个市场、看多长时间、市场目前怎样、哪些方向允许执行。

下面完整列出当前 TradeState,包括嵌套字段。价格使用计价资产,数量使用基础资产;1 bps 是 0.01%。? 表示类型允许省略,null 表示没有可用值。

interface TradeState {
  market: string;       // 市场名称,例如 MON-USDC 或 HYPE-USDC。
  venue?: string;       // 交易场所;当前填写 Kuru 或 hyperliquid。
  baseAsset?: string;   // 基础资产,如 MON、HYPE,也是数量单位。
  quoteAsset?: string;  // 计价资产,如 USDC,也是价格的计价单位。

  block: number;          // Kuru 为链上区块号;Hyperliquid 为本地事件序号。
  horizonBlocks: number;  // 向前判断多少个观察单位,不是下单间隔。
  blockMs: number;        // 每个观察单位的名义时长,毫秒。
  mid: number;            // 中间价 =(最优买价 + 最优卖价)/ 2。
  spreadBps: number;      // 价差 =(最优卖价 - 最优买价)/ mid × 10000。
  bookImbalance: number;  // 中间价上下 1% 内,(买量 - 卖量)/(买量 + 卖量)。

  depth: {                // 各价格范围内的累计挂单数量。
    [band: string]: {     // 范围键名;当前统计 10、25、50 bps。
      bid: number;       // 该范围内累计买单量,单位为基础资产。
      ask: number;       // 该范围内累计卖单量,单位为基础资产。
    };
  };

  book: {                // 最优附近的订单簿,每侧最多 5 档。
    bids: string[];      // 买盘,最好报价在前,元素为“价格 x 数量”。
    asks: string[];      // 卖盘,最好报价在前,元素为“价格 x 数量”。
  };

  returnsBps: {          // 相对历史价格的变化,单位 bps。
    last1: number;       // 相对前 1 个已记录价格样本。
    last5: number;       // 相对前 5 个已记录价格样本。
    last20: number;      // 相对前 20 个已记录价格样本。
    last100: number;     // 相对前 100 个已记录价格样本。
  };

  recentMids: string;    // 从旧到新的中间价,用空格连接,采样规则见下表。

  trades: {             // 近期整个市场的主动成交统计,不是本策略的成交。
    count: number;      // 窗口内保留的成交记录条数。
    buyMon: number;     // 主动买入量,实际单位为 baseAsset。
    sellMon: number;    // 主动卖出量,实际单位为 baseAsset。
    cvdMon: number;     // buyMon - sellMon,可为负,仅统计当前窗口。
    vwap: number | null;       // 成交量加权均价;无有效成交量时为 null。
    lastPrice: number | null;  // 窗口内最后成交价;无记录时为 null。
    lastSide: "buy" | "sell" | null; // 最后成交的主动方方向;无记录时为 null。
  };

  recentTrades: string[]; // 最近最多 10 条成交,从旧到新;格式见下表。

  allowed: {             // 程序算出的方向许可,执行前仍需检查。
    buy: boolean;        // 当前是否允许放置一笔买单。
    sell: boolean;       // 当前是否允许放置一笔卖单。
  };
}

举例来说,买一价为 99.9、卖一价为 100.1,那么 mid = 100spreadBps = 20

如果近期主动买入 12 HYPE、主动卖出 8 HYPE,那么 cvdMon = 4。这里沿用了 Mon 的旧名称,实际表示 4 HYPE。

bookImbalance 表达买卖挂单量的偏向。它等于 0.5,不表示上涨概率为 50%。

两个市场复用了字段,但采样口径有区别:

参数KuruHyperliquid
horizonBlocks × blockMs默认 100 × 300ms,约 30 秒30 × 1000ms,约 30 秒
depth 键名"10bps""10",单位同样是 bps
recentMids最近 H 个样本中每 5 个取一个,包含最新值最近最多 30 个样本
trades 窗口最近 H 个区块内保留的成交最近 30 秒内保留的成交
recentTrades最近 10 条记录,开头是区块号最近 30 秒内最多 10 条,开头是毫秒时间戳
收益率历史不足对应 returnsBps 值填 0与最早保留的样本比较

H 指 horizonBlocks。成交摘要的其余格式是“buy或sell 数量 @ 价格”。样本可能因断线或处理延迟而不等间隔,因此“前 20 个样本”不能直接当成“20 秒前”。

四、Prompt 设计:把任务说具体

如果只问“现在应该买还是卖”,模型不知道你准备持有多久,也不知道订单怎样成交。

当前 Prompt 把这些信息拆开:判断目标、预测窗口、成交方式、参考材料和可选动作。

下面保留实际字段结构,将英文说明精简翻译成中文。它用于解释设计,不是源码原文。

const QUESTIONS = {
  direction: {             // 自定义问题名,答案通过 answers.direction 读取。
    type: "choice",        // 选择题,从 criteria 的选项中选择。

    instructions: {        // 问题背景;内部键名是项目自行定义的。
      question:            // 具体判断:指定观察窗口后,价格高于还是低于当前 mid?
        "经过 horizonBlocks 个观察单位后,基础资产价格会高于还是低于当前中间价?",

      goal:                // 业务用途:选择挂单方向,考虑成本、不利价格变化和库存限制。
        "为 venue 的 market 选择 post-only 挂单方向,预测窗口约为 horizonBlocks × blockMs 毫秒,并考虑价差、费用和仓位约束。",

      timing:              // 成交方式:挂单等待别人来成交,可能先被替换。
        "订单等待主动方成交或被替换。挂单不保证成交,也不是立即执行的市价单。",

      inputs:              // 重点材料与硬约束;两侧都不允许时,执行层不放新单。
        "参考 trades.cvdMon、trades.lastSide、recentTrades、depth、book、returnsBps 和 recentMids,并遵守 allowed。",
    },

    criteria: {            // 候选答案及其判断标准。
      buy:                 // 买入方向:偏向上涨,避免刚成交就下跌。
        "挂买单,判断未来价格偏强,并遵守 allowed.buy。",

      sell:                // 卖出方向:偏向下跌,避免卖出后立即上涨。
        "挂卖单,判断未来价格偏弱,并遵守 allowed.sell。",
    },
  },
} as const;

这种设计把“预测什么”和“怎样使用预测”放在同一个问题里。模型既看到价格方向任务,也知道自己是在为一张等待成交的限价单选择方向。

但有两点要准确理解。

第一,当前只有 buysell 两个候选答案,没有主动观望选项。程序中的 hold 不能直接解读成 Jev 选择了观望。

第二,Prompt 虽然要求考虑费用,当前 state 并没有提供明确费率,也没有完整仓位盈亏。写进说明,不等于已经给模型提供了对应数据。

五、返回值:怎样变成交易程序的决定?

下面是项目读取或保存的 SDK 业务字段,不包含响应头等传输信息。

interface UsedSdkResult {
  answers: {                    // 按请求的问题名组织答案。
    direction: {                // 对应 questions.direction。
      type: "choice";           // 答案类型,与请求一致。
      choice: "buy" | "sell";    // 模型选中的方向。
      probabilities?: {         // 当前安装的 SDK 类型允许概率缺失。
        buy: number;            // 买入选项概率,正常取值 0 到 1。
        sell: number;           // 卖出选项概率,正常情况下与 buy 相加为 1。
      };
    };
  };

  usage: {                      // token 用量,不是费用金额。
    inputTokens: number | undefined;   // 输入量,项目用它估算模型成本。
    outputTokens: number | undefined;  // 输出量,当前未单独用于费用估算。
    totalTokens: number | undefined;   // 总量,未单独复制进 Decision。
  };
}

SDK 将 HTTP 用量字段 input_tokensoutput_tokens 转成上述驼峰名称。官方 HTTP 文档还描述了 confidence,但当前安装的 SDK Choice 类型未暴露它,项目也没有读取它。返回值说明

接着,项目把答案整理成自己的结果类型,并保留调用记录:

interface Decision {
  action: "buy" | "sell" | "hold"; // Jev 初始为 buy 或 sell;执行层可能调整。
  probabilities: {              // 整理后的概率分布。
    buy: number;                // 买入概率;缺失的单个选项补 0。
    sell: number;               // 卖出概率;缺失的单个选项补 0。
    hold: number;               // JevModel 的正常结果固定填 0。
  };
  upIn10: number;                // 旧字段名,实际等于 buy 概率,不是另一次预测。
  latencyMs: number;             // 本地测得的模型调用耗时,含请求等待,不含下单。
  inputTokens: number;           // 输入 token 数,缺失时补 0,不代表实际免费。
  trace?: ModelTrace;            // 本次输入输出记录,用于审查。
}

interface ModelTrace {
  source: "jev" | "simulation";  // 判断来自 Jev 或 MockModel,不表示是否实盘。
  model: string;                // 配置的模型名称,如 jev-latest 或 mock。
  input: {                      // 调用前固定的输入快照。
    state: TradeState;          // 本次市场材料,字段见前文。
    questions?: unknown;        // 本次问题集合;MockModel 不填写。
  };
  output: unknown;              // Jev 保存 { answers, usage },字段见 UsedSdkResult;
                                // MockModel 保存模拟 Decision,不含 trace。
}

如果 SDK 完全没返回概率,当前代码把选中项补成 1,其他项补成 0。此时看到的 100% 是程序补值,不能当作模型的真实确定程度。

即使模型确实返回了 buy: 0.7,也只能先理解为买入选项的概率。它不自动等于交易胜率。 成交价格、退出时机和成本都会影响结果。

六、把一次判断串起来

假设 Jev 返回买入,买入概率为 0.7。

程序接着检查余额和仓位。如果买入允许,就按执行规则报价;如果买入受限而卖出允许,当前代码可能改挂卖单。订单中的 capped 字段会标记这种方向调整。

所以,审查时要区分三份记录:

  • trace.output:模型原本选择了什么。
  • 最终订单:程序实际准备执行什么。
  • 成交记录:市场最终成交了什么。

目前 Jev 接口只负责第一部分。订单执行和账户更新由代码负责,模型调用记录则让这条链路可以被核对。

文章依据当前工作区的 model.tstrader.tshyperliquid.ts及本地 SDK 类型定义整理。代码块是字段说明,不是一份可直接启动交易的完整脚本。

← 返回 FAQ,继续阅读