量化项目研究学习(AI Trading Agent (Nocturne) 架构分析)
AI Trading Agent (Nocturne) 架构分析
项目: Gajesh2007/ai-trading-agent(454 stars)
语言: Python 3.12+ | 包管理: Poetry | 部署: Docker / EigenCloud
分析目的: 提炼 LLM 驱动交易系统的设计模式,与当前配对交易项目做对比
1. 项目概览
Nocturne 是一个以 LLM 为核心决策引擎的 Hyperliquid 永续合约交易系统。与传统量化策略不同,它不使用硬编码规则,而是将技术指标作为上下文喂给 LLM,由 LLM 做出买/卖/持有决策。
核心特点:
- LLM 即策略引擎 — 不写交易规则,让 LLM 基于技术指标做决策
- Tool Calling — LLM 可主动调用 TAAPI 获取额外指标,非预设管道
- 多资产并行 — 单次 LLM 调用决策多个资产
- 实盘部署 — 3 个实盘 Agent(GPT-5 Pro / DeepSeek R1 / Grok 4)
技术栈:
| 组件 | 技术 | 说明 |
|---|---|---|
| LLM 网关 | OpenRouter API | 支持多模型切换(Grok 4、GPT-5 Pro、DeepSeek R1) |
| 交易所 | Hyperliquid Python SDK | 永续合约交易 |
| 技术指标 | TAAPI.io REST API | 200+ 技术指标(EMA、MACD、RSI、ATR 等) |
| HTTP API | aiohttp | 轻量观测端点(/diary、/logs) |
| 配置 | python-dotenv + 自定义 loader | 类型化环境变量加载 |
| 输出格式化 | rich | 终端美化(声明但未深度使用) |
2. 目录结构
ai-trading-agent/
├── .env.example # 环境变量模板
├── .gitignore
├── Dockerfile # 容器化部署
├── README.md
├── pyproject.toml # Poetry 依赖管理
├── poetry.lock
├── docs/
│ └── ARCHITECTURE.md # 架构设计文档
└── src/
├── __init__.py
├── main.py # 主入口 + 交易循环 + HTTP API(~600行)
├── config_loader.py # 类型化环境变量加载器
├── agent/
│ ├── __init__.py
│ └── decision_maker.py # LLM 决策引擎(Tool Calling + 结构化输出)
├── indicators/
│ ├── __init__.py
│ └── taapi_client.py # TAAPI 技术指标客户端
├── trading/
│ ├── __init__.py
│ └── hyperliquid_api.py # Hyperliquid 交易执行(SDK 封装)
└── utils/
├── __init__.py
├── formatting.py # 数值格式化工具
└── prompt_utils.py # JSON 序列化辅助
3. 核心架构分析
3.1 LLM 决策引擎(Decision Maker)
这是 Nocturne 最核心的设计 — LLM 不是辅助工具,而是决策主体。
# src/agent/decision_maker.py
class TradingAgent:
def __init__(self):
self.model = CONFIG["llm_model"] # 主决策模型(如 x-ai/grok-4)
self.sanitize_model = "openai/gpt-5" # 廉价修复模型
self.taapi = TAAPIClient() # 指标查询客户端
def decide_trade(self, assets, context):
"""单次 LLM 调用,为多个资产生成交易决策"""
return self._decide(context, assets=assets)
决策流程:
技术指标 + 账户状态 + 持仓信息 + 历史日记
│
▼
┌─────────────────────────────────────────┐
│ System Prompt(量化交易员人设) │
│ - 低频交易核心策略 │
│ - 滞后效应(Hysteresis)防频繁翻转 │
│ - 冷却期机制(Cooldown) │
│ - 杠杆策略(3x-10x) │
│ - TP/SL 合理性校验规则 │
│ - Tool Calling 指引 │
│ - JSON Schema 输出契约 │
└──────────────┬──────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ LLM 推理(最多 6 轮 Tool Loop) │
│ ┌───────────────────────────────┐ │
│ │ 轮次 1: LLM 分析上下文 │ │
│ │ → 可能调用 fetch_taapi_ │ │
│ │ indicator 获取额外指标 │ │
│ │ 轮次 2: 工具结果回传 │ │
│ │ → LLM 继续推理或输出决策 │ │
│ │ ... │ │
│ │ 轮次 N: 输出结构化 JSON 决策 │ │
│ └───────────────────────────────┘ │
└──────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 输出解析 + 容错 │
│ 1. 优先使用 message.parsed │
│ 2. 降级到 JSON.parse(content) │
│ 3. 再降级到 Sanitizer LLM 修复 │
│ 4. 最终降级到全 hold + parse error │
└──────────────────────────────────────┘
vs 当前项目(纯规则驱动):
| 维度 | Nocturne (LLM 决策) | 当前项目 (规则驱动) |
|---|---|---|
| 决策引擎 | LLM 自然语言推理 | 协整检验 + Z-score 阈值 |
| 信号来源 | TAAPI 200+ 指标 + LLM 选择 | 价格序列协整分析 |
| 可解释性 | LLM 生成 rationale 文本 | 数学公式清晰 |
| 可复现性 | 低 — 相同输入可能不同输出 | 高 — 相同输入必定相同输出 |
| 回测可行性 | 极难 — LLM 调用不可重放 | 可行 — 规则确定性执行 |
| 策略迭代 | 改 prompt = 改策略 | 改代码/参数 = 改策略 |
| 成本 | 每次决策 $0.01-$1(视模型) | 几乎零成本 |
借鉴价值: 中-高。不建议替换当前项目的协整信号系统,但可以引入 LLM 辅助信号确认层 — 在规则系统生成信号后,用 LLM 对信号做"第二意见"过滤。
3.2 Tool Calling 模式
Nocturne 最独特的设计之一 — LLM 在推理过程中可主动查询额外指标:
# src/agent/decision_maker.py — Tool 定义
tools = [{
"type": "function",
"function": {
"name": "fetch_taapi_indicator",
"description": "Fetch any TAAPI indicator. Available: ema, sma, rsi, macd, "
"bbands, stochastic, adx, atr, cci, dmi, ichimoku, supertrend, "
"vwap, obv, mfi, willr, roc, mom, sar, fibonacci, ...",
"parameters": {
"type": "object",
"properties": {
"indicator": {"type": "string"},
"symbol": {"type": "string"}, # 如 "BTC/USDT"
"interval": {"type": "string"}, # 如 "5m", "4h"
"period": {"type": "integer"},
"backtrack": {"type": "integer"},
"other_params": {"type": "object"},
},
"required": ["indicator", "symbol", "interval"],
},
},
}]
Tool 执行流程(最多 6 轮):
for _ in range(6): # 最多 6 轮 tool loop
data = {"model": self.model, "messages": messages, "tools": tools}
resp_json = _post(data)
message = resp_json["choices"][0]["message"]
messages.append(message)
tool_calls = message.get("tool_calls") or []
if tool_calls:
for tc in tool_calls:
# 执行 TAAPI 查询
args = json.loads(tc["function"]["arguments"])
ind_resp = requests.get(
f"{self.taapi.base_url}{args['indicator']}",
params={...}, timeout=30
).json()
# 结果回传 LLM
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": json.dumps(ind_resp),
})
continue # 继续下一轮让 LLM 消化工具结果
# 无 tool call → 解析最终输出
parsed = json.loads(message["content"])
return parsed
设计亮点:
- LLM 不是被动接收预设指标,而是 主动选择 需要什么额外数据
- 比如 LLM 可能看到 RSI 接近超买,主动调用 Bollinger Bands 确认
- 这种模式让 LLM 的决策更像人类交易员的思考过程
vs 当前项目:当前项目的信号管道是预设的(协整分析 → Z-score → 信号触发),无"按需查询"能力。
借鉴价值: 高。如果引入 LLM 辅助层,Tool Calling 是最自然的方式 — 让 LLM 在确认信号时可以主动查询额外指标。
3.3 结构化输出(JSON Schema 强约束)
Nocturne 使用 OpenAI 兼容的 response_format 强制 LLM 输出类型安全的 JSON:
# src/agent/decision_maker.py — JSON Schema 定义
schema = {
"type": "object",
"properties": {
"reasoning": {"type": "string"},
"trade_decisions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"asset": {"type": "string", "enum": assets},
"action": {"type": "string", "enum": ["buy", "sell", "hold"]},
"allocation_usd": {"type": "number", "minimum": 0},
"tp_price": {"type": ["number", "null"]},
"sl_price": {"type": ["number", "null"]},
"exit_plan": {"type": "string"},
"rationale": {"type": "string"},
},
"required": [...],
"additionalProperties": False,
},
}
},
"required": ["reasoning", "trade_decisions"],
"additionalProperties": False,
}
# 应用到请求
data["response_format"] = {
"type": "json_schema",
"json_schema": {
"name": "trade_decisions",
"strict": True, # 严格模式,不允许额外字段
"schema": schema,
},
}
关键设计点:
"strict": True强制 LLM 严格遵循 schema,不允许额外字段"enum": assets限制 asset 只能是预定义的资产列表"enum": ["buy", "sell", "hold"]限制 action 为三选一tp_price/sl_price允许null(用["number", "null"]联合类型)
借鉴价值: 高。如果在当前项目中引入 LLM,结构化输出是必须的 — 确保 LLM 的回复可以直接被代码消费。
3.4 双 LLM 容错(Sanitizer 模式)
当主 LLM 返回格式不正确时,使用廉价快速模型修复:
# src/agent/decision_maker.py
self.sanitize_model = CONFIG.get("sanitize_model") or "openai/gpt-5"
def _sanitize_output(raw_content: str, assets_list):
"""用廉价 LLM 修复格式错误的输出"""
payload = {
"model": self.sanitize_model,
"messages": [
{"role": "system", "content":
"You are a strict JSON normalizer. Return ONLY a JSON array "
"matching the provided JSON Schema. If input is wrapped or "
"has prose/markdown, fix it. Do not add fields."
},
{"role": "user", "content": raw_content},
],
"response_format": {
"type": "json_schema",
"json_schema": {"name": "trade_decisions", "strict": True, "schema": schema},
},
"temperature": 0, # 确定性输出
}
resp = _post(payload)
# ...
容错层次(4 级降级):
Level 1: message.parsed(结构化输出直接解析)
│ 失败
▼
Level 2: json.loads(message.content)(手动 JSON 解析)
│ 失败
▼
Level 3: _sanitize_output()(用 GPT-5 修复格式)
│ 失败
▼
Level 4: 生成全 hold 决策 + "parse error" rationale
借鉴价值: 中-高。双 LLM 模式是处理 LLM 输出不确定性的实用方案。主模型做推理(贵但聪明),修复模型做格式化(便宜但可靠)。
3.5 Provider 自适应降级
Nocturne 在运行时检测 LLM 提供商的能力限制,自动降级重试:
# src/agent/decision_maker.py — 自适应降级逻辑
allow_tools = True
allow_structured = True
for _ in range(6):
data = {"model": self.model, "messages": messages}
if allow_structured:
data["response_format"] = {...}
if allow_tools:
data["tools"] = tools
try:
resp_json = _post(data)
except requests.HTTPError as e:
err = e.response.json()
raw = err.get("error", {}).get("metadata", {}).get("raw", "")
provider = err.get("error", {}).get("metadata", {}).get("provider_name", "")
# xAI 不支持 tool schema → 禁用 tools 重试
if e.response.status_code == 422 and provider.startswith("xai"):
allow_tools = False
continue
# Provider 不支持 structured outputs → 禁用 response_format 重试
if "response_format" in json.dumps(err) or "structured" in json.dumps(err):
allow_structured = False
continue
设计亮点:
- 不同 LLM 提供商(xAI、OpenAI、DeepSeek)对 Tool Calling 和 Structured Output 的支持不同
- 通过检测错误响应中的
provider_name和错误类型,自动降级 - 先尝试最完整的功能集,逐步降级到最基础的纯文本输出
借鉴价值: 中。当需要支持多 LLM 提供商时,这种自适应降级模式很实用。
4. 交易执行层
4.1 Hyperliquid API 封装
# src/trading/hyperliquid_api.py
class HyperliquidAPI:
def __init__(self):
# 支持私钥或助记词
if CONFIG["hyperliquid_private_key"]:
self.wallet = Account.from_key(CONFIG["hyperliquid_private_key"])
elif CONFIG["mnemonic"]:
Account.enable_unaudited_hdwallet_features()
self.wallet = Account.from_mnemonic(CONFIG["mnemonic"])
# 网络选择(mainnet / testnet)
network = CONFIG.get("hyperliquid_network", "mainnet")
self.info = Info(base_url)
self.exchange = Exchange(self.wallet, base_url)
4.2 异步重试机制
async def _retry(self, fn, *args, max_attempts=3, backoff_base=0.5,
reset_on_fail=True, to_thread=True, **kwargs):
"""指数退避重试 + 可选客户端重建"""
for attempt in range(max_attempts):
try:
if to_thread:
return await asyncio.to_thread(fn, *args, **kwargs)
return await fn(*args, **kwargs)
except (WebSocketConnectionClosedException, aiohttp.ClientError,
ConnectionError, TimeoutError) as e:
if reset_on_fail:
self._reset_clients() # 重建 SDK 客户端
await asyncio.sleep(backoff_base * (2 ** attempt))
设计亮点:
asyncio.to_thread()将同步 SDK 调用桥接到异步事件循环- 连接失败时自动重建
Exchange和Info客户端实例 - 区分可恢复错误(网络问题,3 次重试)和不可恢复错误(逻辑错误,1 次重试后放弃)
vs 当前项目:当前项目使用 CircuitBreaker + RateLimiter 模式,更系统化。Nocturne 的重试更轻量但缺乏熔断保护。
4.3 订单精度处理
def round_size(self, asset, amount):
"""根据交易所元数据精确控制下单精度"""
meta = self._meta_cache[0]
universe = meta.get("universe", [])
asset_info = next((u for u in universe if u["name"] == asset), None)
if asset_info:
decimals = asset_info.get("szDecimals", 8)
return round(amount, decimals)
return round(amount, 8)
4.4 TP/SL 触发单
async def place_take_profit(self, asset, is_buy, amount, tp_price):
amount = self.round_size(asset, amount)
order_type = {
"trigger": {
"triggerPx": tp_price,
"isMarket": True, # 触发后市价执行
"tpsl": "tp"
}
}
return await self._retry(lambda: self.exchange.order(
asset, not is_buy, amount, tp_price, order_type, True # reduce_only=True
))
借鉴价值: 中。当前项目的止损/止盈逻辑在应用层实现,可以考虑使用交易所原生触发单来减少监控压力。
5. 交易所状态对账机制(Reconciliation)
Nocturne 的核心风控设计之一 — 交易所状态永远是权威来源:
# src/main.py — 每轮循环中的状态对账
try:
# 从交易所获取当前持仓资产
assets_with_positions = set()
for pos in state['positions']:
if abs(float(pos.get('szi') or 0)) > 0:
assets_with_positions.add(pos.get('coin'))
# 获取有挂单的资产
assets_with_orders = {o.get('coin') for o in open_orders if o.get('coin')}
# 清理过时的本地 active_trades
for tr in active_trades[:]:
asset = tr.get('asset')
if asset not in assets_with_positions and asset not in assets_with_orders:
# 交易所没有持仓也没有挂单 → 本地记录过时
active_trades.remove(tr)
# 记录到 diary
diary_entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"asset": asset,
"action": "reconcile_close",
"reason": "no_position_no_orders",
}
f.write(json.dumps(diary_entry) + "\n")
except Exception:
pass
设计原则:
- 本地
active_trades可能因 TP/SL 被交易所自动执行而过时 - 每轮循环检查:如果交易所既没有持仓也没有挂单,就清理本地记录
- 对账结果写入
diary.jsonl,可追溯
vs 当前项目:当前项目通过 TimescaleDB 持久化状态 + 崩溃恢复实现更强的一致性保证。但 Nocturne 的"交易所即权威"原则值得在当前项目中也显式实现 — 定期与交易所持仓对账,确保本地状态不偏离。
借鉴价值: 高。即使有数据库持久化,也应该定期与交易所做 reconciliation。
6. Exit Plan as State — LLM 决策连续性
Nocturne 最创新的设计之一 — LLM 写的退出计划作为下一轮的上下文回传:
# LLM 输出的 exit_plan 示例:
# "close if 4h close above EMA50; cooldown_bars:3 until 2025-10-19T15:55Z"
# 存储到 active_trades
active_trades.append({
"asset": asset,
"is_long": is_buy,
"entry_price": current_price,
"exit_plan": output["exit_plan"], # LLM 自定义的退出条件
"opened_at": datetime.now().isoformat()
})
# 下一轮循环中,active_trades 作为 dashboard 的一部分回传给 LLM
dashboard = {
"active_trades": [
{
"asset": tr.get('asset'),
"exit_plan": tr.get('exit_plan'), # ← 上一轮 LLM 自己写的
"opened_at": tr.get('opened_at')
}
for tr in active_trades
],
}
设计精妙之处:
- 传统系统:退出条件是硬编码的规则(如"Z-score 回归到 0 时平仓")
- Nocturne:LLM 自己定义退出条件,下一轮 LLM 自己检查这些条件
- 实现了 LLM 跨轮次的"记忆"和"承诺"
- System prompt 中的
Core policy (1)要求 LLM 尊重先前的 exit_plan
局限性:
- LLM 可能"遗忘"或无视自己之前写的 exit_plan
- 没有代码级强制执行(虽然有
check_exit_condition函数,但仅支持有限模式匹配) - 完全依赖 prompt 中的软约束
借鉴价值: 中。这个设计本身很创新,但在严肃的交易系统中,退出条件应该有代码级硬约束兜底。可以考虑"LLM 建议 + 规则执行"的混合模式。
7. 配置管理
7.1 类型化辅助函数(config_loader.py)
Nocturne 的配置加载器提供了一组类型安全的辅助函数:
# src/config_loader.py
def _get_env(name: str, default: str | None = None, required: bool = False) -> str | None:
"""带必填校验的环境变量获取"""
value = os.getenv(name, default)
if required and (value is None or value == ""):
raise RuntimeError(f"Missing required environment variable: {name}")
return value
def _get_bool(name: str, default: bool = False) -> bool:
"""布尔型环境变量(支持 1/true/yes/on)"""
raw = os.getenv(name)
return raw.strip().lower() in {"1", "true", "yes", "on"} if raw else default
def _get_int(name: str, default: int | None = None) -> int | None:
"""整型环境变量,带错误提示"""
def _get_json(name: str, default: dict | None = None) -> dict | None:
"""JSON 对象型环境变量"""
def _get_list(name: str, default: list[str] | None = None) -> list[str] | None:
"""列表型环境变量(支持 JSON 数组或逗号分隔)"""
# 模块级字典,启动时即加载
CONFIG = {
"taapi_api_key": _get_env("TAAPI_API_KEY", required=True),
"llm_model": _get_env("LLM_MODEL", "x-ai/grok-4"),
"reasoning_enabled": _get_bool("REASONING_ENABLED", False),
"api_port": _get_env("APP_PORT") or _get_env("API_PORT") or "3000",
"provider_config": _get_json("PROVIDER_CONFIG"),
"provider_quantizations": _get_list("PROVIDER_QUANTIZATIONS"),
# ...
}
设计亮点:
- 比裸
os.getenv()更安全 — 类型转换失败时有明确错误信息 _get_list支持 JSON 数组和逗号分隔两种格式_get_json支持直接在环境变量中传递 JSON 对象required=True在启动时就校验,不会运行时才崩溃
vs 当前项目:当前项目使用 dataclass + os.getenv(),Nocturne 的类型化辅助函数虽然没有 Pydantic 那么强大,但零依赖且实用。
借鉴价值: 中。如果不想引入 Pydantic,这组辅助函数是轻量替代方案。
8. 可观测性
8.1 HTTP API 端点
# src/main.py — aiohttp 轻量 API
async def start_api(app):
app.router.add_get('/diary', handle_diary) # 交易日记
app.router.add_get('/logs', handle_logs) # 日志查看
async def main_async():
app = web.Application()
await start_api(app)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, CFG.get("api_host"), int(CFG.get("api_port")))
await site.start()
await run_loop() # 交易循环
可用端点:
| 端点 | 参数 | 功能 |
|---|---|---|
GET /diary |
limit=200 |
JSON 格式的最近交易日记 |
GET /diary?raw=1 |
— | 原始 JSONL 文本 |
GET /diary?download=1 |
— | 下载 diary.jsonl 文件 |
GET /logs |
path=llm_requests.log |
查看 LLM 请求日志 |
GET /logs?limit=all |
— | 获取完整日志 |
GET /logs?download=1 |
— | 下载日志文件 |
8.2 交易日记(diary.jsonl)
每笔交易和决策都记录到 diary.jsonl:
{
"timestamp": "2025-10-19T12:34:56+00:00",
"asset": "BTC",
"action": "buy",
"allocation_usd": 500.0,
"amount": 0.0052,
"entry_price": 96153.85,
"tp_price": 98000.0,
"sl_price": 95000.0,
"exit_plan": "close if 4h RSI > 70 and MACD bearish cross",
"rationale": "Structure bullish on 4h with EMA20 > EMA50...",
"filled": true
}
8.3 LLM 请求日志(llm_requests.log)
完整记录每次 LLM 调用的请求和响应:
with open("llm_requests.log", "a") as f:
f.write(f"\n\n=== {datetime.now()} ===\n")
f.write(f"Model: {payload.get('model')}\n")
f.write(f"Payload:\n{json.dumps(payload, indent=2)}\n")
借鉴价值: 高。轻量级 HTTP API 暴露内部状态非常实用,比 SSH 到服务器查日志方便很多。当前项目可以用类似方式暴露策略状态、持仓信息、信号历史。
9. 独特设计模式详解
模式 1: LLM 辅助信号解读
核心思想: 不替换量化规则,而是让 LLM 对规则产出的信号做"第二意见"。
在当前项目中的应用方式:
协整分析 → Z-score 信号 → LLM 确认/过滤 → 执行
LLM 可以回答:"当前 Z-score 2.1 触发了做多信号,但 BTC 大盘正在暴跌,基本面不支持,建议跳过"。
模式 2: Tool Calling 按需指标查询
核心思想: LLM 在决策过程中主动选择需要的额外数据。
在当前项目中的应用方式: 定义工具函数如 get_funding_rate、get_order_book_depth、get_correlation_with_btc,让 LLM 在确认信号时按需调用。
模式 3: 结构化输出强约束
核心思想: 使用 JSON Schema 的 strict: true 模式,确保 LLM 输出可直接被代码消费。
模式 4: 双 LLM 容错
核心思想: 主模型做推理(贵但聪明),修复模型做格式化(便宜但可靠)。
模式 5: Exit Plan as State
核心思想: LLM 写退出条件 → 存储 → 下一轮作为上下文回传 → LLM 自我检查。
模式 6: HTTP API 可观测性
核心思想: 用 aiohttp 暴露 2-3 个只读端点,零前端依赖,远程可查。
10. 与当前项目的对比矩阵
10.1 架构维度对比
| 维度 | Nocturne (LLM 驱动) | 当前项目 (规则驱动) | 评价 |
|---|---|---|---|
| 决策引擎 | LLM 自然语言推理 | 协整检验 + Z-score | 各有适用场景 |
| 并发模型 | asyncio + to_thread | threading (~37 线程) | Nocturne 更轻量 |
| 信号来源 | TAAPI 200+ 指标 + LLM 选择 | 价格序列协整分析 | 当前项目更专精 |
| 交易类型 | 单腿方向性交易 | 双腿配对交易 + 对冲 | 当前项目更安全 |
| 输出格式 | JSON Schema 强约束 | 不涉及(规则直接执行) | Nocturne 创新 |
| 可复现性 | 低(LLM 非确定性) | 高(数学规则确定) | 当前项目胜出 |
| 数据持久化 | 无(JSONL 仅日记) | TimescaleDB | 当前项目胜出 |
| 崩溃恢复 | 无(重启丢 active_trades) | 数据库恢复 | 当前项目胜出 |
| 安全防护 | 杠杆仅 prompt 软约束 | KillSwitch + RateLimiter + CircuitBreaker | 当前项目胜出 |
| 状态对账 | 每轮与交易所对账 | 依赖数据库状态 | Nocturne 思路值得借鉴 |
| 可观测性 | HTTP API + diary.jsonl | 日志 + 飞书通知 | Nocturne 更可查询 |
| 远程监控 | HTTP 端点随时查 | SSH + 飞书推送 | Nocturne 更方便 |
| 回测能力 | 极难(LLM 不可重放) | 可行(规则确定性) | 当前项目胜出 |
| API 成本 | 高(每次决策 LLM 调用) | 几乎零 | 当前项目胜出 |
| 测试覆盖 | 无 | 无 | 都需要改进 |
| 代码组织 | main.py ~600 行膨胀 | 模板方法 + 职责分离 | 当前项目胜出 |
10.2 当前项目独有优势(不需要改动)
- TimescaleDB 持久化 + 崩溃恢复 — Nocturne 重启后丢失所有 active_trades,仅有 diary.jsonl 不足以恢复运行
- 三层安全防护 — KillSwitch(文件触发紧急停止)+ RateLimiter + CircuitBreaker,Nocturne 无任何代码级安全防护
- 双腿配对交易 + 对冲回滚 — 统计套利模型自带对冲属性,Nocturne 是单腿方向性交易
- 多周期协整分析 — 5m/1h/4h 三周期 Z-score + 双重确认,数学上可回测
- 线程安全状态管理 — Lock/RLock 保护共享状态,Nocturne 使用全局变量无保护
- 日损限额 + 最大回撤监控 — 企业级风控,Nocturne 杠杆约束仅靠 prompt 软约束
- 批量写入优化 — TimescaleDB COPY 方法 >40K records/sec
11. 可借鉴改进建议
P0 - 高优先级(高价值低成本)
11.1 LLM 辅助信号过滤层
现状: 纯规则驱动信号,可能在极端市况下产生假信号
改进: 在规则信号触发后,增加 LLM 确认层
# 概念方案
class LLMSignalFilter:
def __init__(self, model="openai/gpt-4o-mini"):
self.model = model
def confirm_signal(self, signal: dict, market_context: dict) -> dict:
"""LLM 对规则信号做第二意见过滤"""
context = {
"signal": signal, # Z-score, direction, pair
"market": market_context, # 大盘状态, funding, OI
}
# 结构化输出: {confirm: bool, confidence: float, reason: str}
return self._call_llm(context)
工作量: ~1-2 天 | 风险: 低(LLM 仅做过滤,不直接控制交易) | 收益: 减少假信号
11.2 交易所状态对账(Reconciliation)
现状: 依赖数据库状态,可能与交易所实际状态偏离
改进: 定期与交易所持仓/挂单状态对账
async def reconcile_positions(self):
"""与交易所对账,确保本地状态准确"""
exchange_positions = await self.api.get_positions()
local_positions = self.state_manager.get_active_positions()
for local in local_positions:
if local.symbol not in exchange_positions:
self.state_manager.mark_closed(local, reason="reconcile")
self.notify(f"Reconciled stale position: {local.symbol}")
工作量: ~3-4 小时 | 风险: 低 | 收益: 防止状态偏离导致风控失效
11.3 HTTP API 可观测端点
现状: 只能通过 SSH + 日志或飞书推送了解系统状态
改进: 增加轻量 HTTP API 暴露内部状态
# 概念方案(可用 aiohttp 或 FastAPI)
# GET /status → 系统状态、运行时长、活跃持仓
# GET /signals → 最近信号历史
# GET /positions → 当前持仓详情
# GET /metrics → Z-score、协整强度、PnL 等关键指标
工作量: ~4-6 小时 | 风险: 低 | 收益: 远程监控能力
P1 - 中优先级
11.4 结构化 LLM 输出(如果引入 LLM)
前提: 如果实施 11.1 的 LLM 信号过滤
方案: 使用 OpenAI 兼容的 JSON Schema 强约束
response_format = {
"type": "json_schema",
"json_schema": {
"name": "signal_confirmation",
"strict": True,
"schema": {
"type": "object",
"properties": {
"confirm": {"type": "boolean"},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"reason": {"type": "string"},
"suggested_adjustment": {
"type": ["object", "null"],
"properties": {
"position_scale": {"type": "number"},
"tighter_stop": {"type": "boolean"},
}
}
},
"required": ["confirm", "confidence", "reason"],
}
}
}
11.5 交易日记持久化
现状: 飞书通知是即发即忘
改进: 借鉴 Nocturne 的 diary.jsonl 模式,每笔决策都结构化记录
diary_entry = {
"timestamp": datetime.utcnow().isoformat(),
"pair": "HYPE/PURR",
"action": "open_long",
"z_score": 2.15,
"signal_source": "cointegration",
"llm_confirmed": True, # 如果有 LLM 层
"entry_prices": {"HYPE": 25.3, "PURR": 0.45},
"position_size_usd": 500,
"rationale": "Z-score > 2.0, 三周期确认, funding 有利"
}
P2 - 低优先级(长期改进)
11.6 LLM Tool Calling 按需指标查询
前提: 更深度的 LLM 集成
方案: 定义一组 Tool Function,让 LLM 在信号确认时按需查询
tools = [
{"name": "get_funding_rate", "params": {"asset": "string"}},
{"name": "get_order_book_imbalance", "params": {"asset": "string", "depth": "int"}},
{"name": "get_correlation", "params": {"asset1": "string", "asset2": "string"}},
{"name": "get_market_sentiment", "params": {"asset": "string"}},
]
11.7 Provider 自适应降级
前提: 使用 LLM 且需要支持多提供商
方案: 借鉴 Nocturne 的运行时能力检测 + 自动降级
12. 架构缺陷警示(应避免)
以下是 Nocturne 的设计缺陷,当前项目应避免这些问题:
-
main.py 膨胀(~600 行) — 混合了 CLI 参数解析、HTTP 路由、交易循环、状态管理、Sharpe 计算。当前项目的模板方法模式 + 职责分离明显更优
-
同步 HTTP 阻塞异步事件循环 —
decision_maker.py使用requests.post()发送 LLM 请求,这是同步调用,会阻塞 asyncio 事件循环。应该使用aiohttp或httpx# 问题代码 resp = requests.post(self.base_url, headers=headers, json=payload, timeout=60) # 这个调用在异步上下文中会阻塞整个事件循环 60 秒 -
无持久化恢复 —
active_trades是内存列表,重启后完全丢失。diary.jsonl 记录了历史但没有恢复逻辑 -
无测试代码 — 零测试文件,项目没有 tests/ 目录
-
LLM 杠杆约束仅靠 prompt 软约束 — System prompt 说 "KEEP IT WITHIN 10X" 但代码中没有任何硬限制。LLM 完全可能输出 50x 杠杆的 allocation_usd
# 无任何代码级杠杆校验 alloc_usd = float(output.get("allocation_usd", 0.0)) amount = alloc_usd / current_price order = await hyperliquid.place_buy_order(asset, amount) # 直接执行,无上限检查 -
Sharpe 比率计算中 pnl 字段从未填充 —
trade_log中的 entry 没有pnl字段,导致calculate_sharpe永远返回 0# trade_log.append 时没有 pnl 字段 trade_log.append({"type": action, "price": current_price, "amount": amount, ...}) # 但 calculate_sharpe 读取 pnl vals = [r.get('pnl', 0) for r in returns] # 永远是 0 -
全局变量无保护 —
active_trades、invocation_count等全局变量在异步上下文中无锁保护 -
TAAPI 调用未做频率限制 — 每轮循环为每个资产串行调用多次 TAAPI API(EMA、MACD、RSI 等),无 rate limiting
13. 总结
Nocturne 的核心价值
| 设计模式 | 借鉴价值 | 实施难度 | 说明 |
|---|---|---|---|
| LLM 辅助信号过滤 | 高 | 中 | 不替换规则,仅做信号确认 |
| Tool Calling 按需查询 | 高 | 中 | LLM 主动选择需要的数据 |
| 结构化输出 JSON Schema | 高 | 低 | 确保 LLM 输出可编程消费 |
| 双 LLM 容错(Sanitizer) | 中-高 | 低 | 廉价模型修复格式错误 |
| Exit Plan as State | 中 | 低 | LLM 跨轮次决策连续性 |
| HTTP API 可观测性 | 高 | 低 | 轻量远程监控 |
| 交易所状态对账 | 高 | 低 | 防止本地状态偏离 |
| Provider 自适应降级 | 中 | 中 | 多 LLM 提供商兼容 |
行动清单
- [ ] P0 实现交易所状态对账(Reconciliation)
- [ ] P0 增加 HTTP API 可观测端点
- [ ] P0 评估 LLM 信号过滤层可行性
- [ ] P1 实现结构化交易日记(diary 模式)
- [ ] P1 如引入 LLM,使用 JSON Schema 强约束输出
- [ ] P2 LLM Tool Calling 按需指标查询
- [ ] P2 多 LLM Provider 自适应降级