done
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Zero-token backfill:
|
||||
- Mark old posts with prefilter_reason (rt_only / url_only / empty) where applicable
|
||||
- Fill ai_reasoning for posts that have a signal but empty reasoning
|
||||
- Stamp analysis_version='legacy' on anything still missing it
|
||||
|
||||
Run once after the schema migration. Safe to re-run.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models import Post
|
||||
|
||||
|
||||
def classify(text: str) -> Optional[str]:
|
||||
stripped = (text or "").strip()
|
||||
if not stripped:
|
||||
return "empty"
|
||||
if stripped.startswith("RT: https://") and len(stripped) < 60:
|
||||
return "rt_only"
|
||||
if stripped.startswith("https://") and " " not in stripped:
|
||||
return "url_only"
|
||||
return None
|
||||
|
||||
|
||||
PREFILTER_MSG = {
|
||||
"rt_only": "Pre-filtered: retweet with no added commentary.",
|
||||
"url_only": "Pre-filtered: bare URL with no text content.",
|
||||
"empty": "Pre-filtered: empty post body.",
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
async with AsyncSessionLocal() as db:
|
||||
posts = (await db.execute(select(Post))).scalars().all()
|
||||
|
||||
pre = ai_fix = ver = 0
|
||||
for p in posts:
|
||||
reason = classify(p.text)
|
||||
if reason and not p.prefilter_reason:
|
||||
p.prefilter_reason = reason
|
||||
if not p.ai_reasoning:
|
||||
p.ai_reasoning = PREFILTER_MSG[reason]
|
||||
ai_fix += 1
|
||||
pre += 1
|
||||
|
||||
# Any remaining signal'd post with empty reasoning: fill a minimal note
|
||||
if p.signal and not p.ai_reasoning:
|
||||
p.ai_reasoning = "(legacy) reasoning not recorded for this post."
|
||||
ai_fix += 1
|
||||
|
||||
if not p.analysis_version:
|
||||
p.analysis_version = "legacy"
|
||||
ver += 1
|
||||
|
||||
await db.commit()
|
||||
print(f"✅ prefilter_reason set on {pre} posts")
|
||||
print(f"✅ ai_reasoning filled on {ai_fix} posts")
|
||||
print(f"✅ analysis_version stamped 'legacy' on {ver} posts")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
把 AI 信号写回数据库。
|
||||
跳过纯RT/URL帖子和已有signal的帖子。
|
||||
并发执行提速,自动限速避免API超限。
|
||||
用法: python scripts/backfill_signals.py [--limit 500] [--overwrite]
|
||||
"""
|
||||
import asyncio
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.services.analysis import analyze_post
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models import Post
|
||||
from sqlalchemy import select
|
||||
|
||||
CONCURRENCY = 5 # 并发数,避免触发API限速
|
||||
|
||||
|
||||
async def process_one(post: Post, semaphore: asyncio.Semaphore, overwrite: bool) -> str:
|
||||
"""分析单条帖子并写回DB,返回状态字符串"""
|
||||
if not overwrite and post.signal is not None:
|
||||
return "skip"
|
||||
|
||||
async with semaphore:
|
||||
analysis = await analyze_post(post.text)
|
||||
await asyncio.sleep(0.1) # 轻度限速
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await db.execute(select(Post).where(Post.id == post.id))
|
||||
p = result.scalar_one_or_none()
|
||||
if p:
|
||||
p.sentiment = analysis["sentiment"]
|
||||
p.signal = analysis["signal"]
|
||||
p.ai_confidence = analysis["confidence"]
|
||||
p.ai_reasoning = analysis["reasoning"]
|
||||
p.relevant = analysis["relevant"]
|
||||
p.prefilter_reason = analysis.get("prefilter_reason")
|
||||
p.analysis_version = analysis.get("analysis_version")
|
||||
if analysis["relevant"] and analysis["asset"] and not p.price_impact_asset:
|
||||
p.price_impact_asset = analysis["asset"]
|
||||
await db.commit()
|
||||
|
||||
sig = analysis["signal"]
|
||||
return f"{sig}:{analysis['confidence']}%"
|
||||
|
||||
|
||||
async def main(limit: int, overwrite: bool):
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await db.execute(
|
||||
select(Post).order_by(Post.published_at.desc()).limit(limit)
|
||||
)
|
||||
posts = result.scalars().all()
|
||||
|
||||
total = len(posts)
|
||||
print(f"共 {total} 条帖子,并发={CONCURRENCY},overwrite={overwrite}\n")
|
||||
|
||||
semaphore = asyncio.Semaphore(CONCURRENCY)
|
||||
tasks = [process_one(p, semaphore, overwrite) for p in posts]
|
||||
|
||||
done = 0
|
||||
buy = short = sell = hold = skipped = 0
|
||||
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
status = await coro
|
||||
done += 1
|
||||
if status == "skip":
|
||||
skipped += 1
|
||||
elif status.startswith("buy"): buy += 1
|
||||
elif status.startswith("short"): short += 1
|
||||
elif status.startswith("sell"): sell += 1
|
||||
else: hold += 1
|
||||
|
||||
if done % 20 == 0 or done == total:
|
||||
print(f" 进度 {done}/{total} | BUY={buy} SHORT={short} SELL={sell} HOLD={hold} SKIP={skipped}")
|
||||
|
||||
print(f"\n✅ 完成: BUY={buy} SHORT={short} SELL={sell} HOLD={hold} SKIP={skipped}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--limit", type=int, default=400)
|
||||
ap.add_argument("--overwrite", action="store_true", help="重新分析已有signal的帖子")
|
||||
args = ap.parse_args()
|
||||
asyncio.run(main(args.limit, args.overwrite))
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
批量回跑语义分析 — 验证新prompt效果
|
||||
用法: python scripts/reanalyze_sample.py
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.services.analysis import analyze_post
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models import Post
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
async def main():
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await db.execute(
|
||||
select(Post)
|
||||
.order_by(Post.published_at.desc())
|
||||
.limit(60)
|
||||
)
|
||||
posts = result.scalars().all()
|
||||
|
||||
print(f"分析 {len(posts)} 条帖子 (model: claude-sonnet-4-6)\n")
|
||||
|
||||
signals = {"buy": 0, "sell": 0, "short": 0, "hold": 0}
|
||||
relevant_count = 0
|
||||
|
||||
correct_m5, checked_m5 = 0, 0
|
||||
correct_m15, checked_m15 = 0, 0
|
||||
correct_m1h, checked_m1h = 0, 0
|
||||
|
||||
for i, post in enumerate(posts):
|
||||
analysis = await analyze_post(post.text)
|
||||
signals[analysis["signal"]] += 1
|
||||
if analysis["relevant"]:
|
||||
relevant_count += 1
|
||||
|
||||
if analysis["relevant"]:
|
||||
sig = analysis["signal"].upper()
|
||||
conf = analysis["confidence"]
|
||||
|
||||
actual_m5 = post.price_impact_m5
|
||||
actual_m15 = post.price_impact_m15
|
||||
actual_m1h = post.price_impact_m1h
|
||||
|
||||
def dir_ok(actual, signal):
|
||||
if actual is None: return None
|
||||
if signal == "buy": return actual > 0
|
||||
if signal == "short": return actual < 0
|
||||
return None
|
||||
|
||||
m5_ok = dir_ok(actual_m5, analysis["signal"])
|
||||
m15_ok = dir_ok(actual_m15, analysis["signal"])
|
||||
m1h_ok = dir_ok(actual_m1h, analysis["signal"])
|
||||
|
||||
if m5_ok is not None: checked_m5 += 1; correct_m5 += int(m5_ok)
|
||||
if m15_ok is not None: checked_m15 += 1; correct_m15 += int(m15_ok)
|
||||
if m1h_ok is not None: checked_m1h += 1; correct_m1h += int(m1h_ok)
|
||||
|
||||
# accuracy marks
|
||||
marks = ""
|
||||
if m5_ok is not None: marks += f" 5m:{'✅' if m5_ok else '❌'}"
|
||||
if m15_ok is not None: marks += f" 15m:{'✅' if m15_ok else '❌'}"
|
||||
if m1h_ok is not None: marks += f" 1h:{'✅' if m1h_ok else '❌'}"
|
||||
|
||||
print(f"[{i+1:03d}] {post.published_at.strftime('%m-%d %H:%M')} | {sig:5s} {conf:3d}% |{marks}")
|
||||
print(f" {post.text[:110]}")
|
||||
print(f" → {analysis['reasoning'][:200]}")
|
||||
print()
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
print("=" * 70)
|
||||
print(f"总计: {len(posts)} 帖 | Relevant: {relevant_count} ({relevant_count/len(posts)*100:.0f}%)")
|
||||
print(f"信号: BUY={signals['buy']} SHORT={signals['short']} SELL={signals['sell']} HOLD={signals['hold']}")
|
||||
print()
|
||||
if checked_m5: print(f"准确率 5m: {correct_m5}/{checked_m5} = {correct_m5/checked_m5*100:.0f}%")
|
||||
if checked_m15: print(f"准确率 15m: {correct_m15}/{checked_m15} = {correct_m15/checked_m15*100:.0f}%")
|
||||
if checked_m1h: print(f"准确率 1h: {correct_m1h}/{checked_m1h} = {correct_m1h/checked_m1h*100:.0f}%")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Hyperliquid 接入测试脚本
|
||||
|
||||
用法:
|
||||
source venv/bin/activate
|
||||
HL_API_PRIVATE_KEY="0x你的API钱包私钥" \
|
||||
HL_ACCOUNT_ADDRESS="0x你的MetaMask地址" \
|
||||
python scripts/test_hyperliquid.py
|
||||
|
||||
会做以下验证(只读,不下单):
|
||||
1. 连通性检查
|
||||
2. 账户 USDC 余额
|
||||
3. BTC-PERP 当前价格
|
||||
4. 当前持仓
|
||||
5. 模拟开仓参数(不实际下单)
|
||||
|
||||
加 --trade 参数才会真正下一个 0.001 BTC 的小仓位并立即平掉(mainnet 慎用)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.services.hyperliquid import HyperliquidTrader
|
||||
|
||||
|
||||
async def main(do_trade: bool):
|
||||
api_key = os.getenv("HL_API_PRIVATE_KEY") or os.getenv("HL_API_KEY", "")
|
||||
account = os.getenv("HL_ACCOUNT_ADDRESS", "")
|
||||
|
||||
if not api_key:
|
||||
print("❌ 缺少 HL_API_PRIVATE_KEY 环境变量")
|
||||
print(" export HL_API_PRIVATE_KEY='0x你的API钱包私钥'")
|
||||
sys.exit(1)
|
||||
if not account:
|
||||
print("❌ 缺少 HL_ACCOUNT_ADDRESS 环境变量")
|
||||
print(" export HL_ACCOUNT_ADDRESS='0x你的MetaMask地址'")
|
||||
sys.exit(1)
|
||||
|
||||
mainnet = os.getenv("HL_MAINNET", "true").lower() != "false"
|
||||
net_label = "MAINNET" if mainnet else "TESTNET"
|
||||
print(f"\n=== Hyperliquid 接入测试 ({net_label}) ===\n")
|
||||
|
||||
trader = HyperliquidTrader(
|
||||
api_private_key=api_key,
|
||||
account_address=account,
|
||||
leverage=3,
|
||||
mainnet=mainnet,
|
||||
)
|
||||
|
||||
# 1. USDC 余额
|
||||
print("① 查询 USDC 余额...")
|
||||
balance = await trader.get_balance()
|
||||
print(f" 可提取余额: ${balance:.2f} USDC")
|
||||
if balance < 10:
|
||||
print(" ⚠️ 余额不足 $10,交易可能失败")
|
||||
else:
|
||||
print(" ✅ 余额充足")
|
||||
|
||||
# 2. BTC mid price
|
||||
print("\n② 查询 BTC-PERP 价格...")
|
||||
try:
|
||||
from hyperliquid.info import Info
|
||||
base_url = "https://api.hyperliquid.xyz" if mainnet else "https://api.hyperliquid-testnet.xyz"
|
||||
info = Info(base_url, skip_ws=True)
|
||||
mids = info.all_mids()
|
||||
btc_price = float(mids.get("BTC", 0))
|
||||
print(f" BTC mid price: ${btc_price:,.2f}")
|
||||
print(" ✅ 价格获取正常")
|
||||
except Exception as e:
|
||||
print(f" ❌ 价格获取失败: {e}")
|
||||
|
||||
# 3. 当前持仓
|
||||
print("\n③ 查询当前持仓...")
|
||||
positions = await trader.get_open_positions()
|
||||
if positions:
|
||||
for p in positions:
|
||||
side = "多" if p["szi"] > 0 else "空"
|
||||
print(f" {p['coin']} {side}仓: {abs(p['szi'])} 张 @ 均价 ${p['entry_px']:,.2f} 未实现盈亏: ${p['unrealized_pnl']:.2f}")
|
||||
print(f" 共 {len(positions)} 个持仓")
|
||||
else:
|
||||
print(" 无持仓 ✅")
|
||||
|
||||
# 4. 模拟开仓参数
|
||||
print("\n④ 模拟开仓参数(不实际下单)...")
|
||||
sz_dec = await trader._get_sz_decimals("BTC")
|
||||
mid = await trader._mid_price("BTC")
|
||||
size_usd = 100
|
||||
size_coins = round(size_usd / mid, sz_dec)
|
||||
limit_buy = round(mid * 1.01, 1)
|
||||
print(f" 开多 $100 notional:")
|
||||
print(f" → 数量: {size_coins} BTC")
|
||||
print(f" → 限价(1%滑点): ${limit_buy:,.1f}")
|
||||
print(f" → 杠杆: 3x 隔离保证金")
|
||||
print(f" → 所需保证金: ${size_usd / 3:.2f} USDC")
|
||||
|
||||
# 5. 真实测试下单(需要 --trade 参数)
|
||||
if do_trade:
|
||||
print(f"\n⑤ 真实下单测试 (0.001 BTC long → 立即平仓)...")
|
||||
if not mainnet:
|
||||
print(" 使用 testnet,无需担心资金损失")
|
||||
else:
|
||||
print(" ⚠️ MAINNET 真实资金!继续中...")
|
||||
|
||||
try:
|
||||
result = await trader.open_position("BTC", "long", size_usd=50)
|
||||
print(f" ✅ 开仓成功: fill_price=${result['fill_price']:,.2f}, size={result['size_coins']} BTC")
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
close = await trader.close_position("BTC")
|
||||
print(f" ✅ 平仓成功: fill_price=${close['fill_price']:,.2f}")
|
||||
except Exception as e:
|
||||
print(f" ❌ 下单失败: {e}")
|
||||
else:
|
||||
print("\n提示: 加 --trade 参数进行真实下单+平仓测试")
|
||||
|
||||
print("\n=== 测试完成 ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--trade", action="store_true", help="执行真实下单+平仓测试")
|
||||
args = parser.parse_args()
|
||||
asyncio.run(main(args.trade))
|
||||
Reference in New Issue
Block a user