提交
This commit is contained in:
+727
@@ -0,0 +1,727 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
XAU-USDT-SWAP 自动盯盘脚本
|
||||
- 每 3 分钟自动刷新行情, 计算价格/成交量/均线(SMA/EMA)/RSI/MACD
|
||||
- 判断趋势(上涨/下跌/震荡), 给出 3~15 分钟操作建议(多/空/观望)+ 置信度 + 支撑阻力
|
||||
- 自动运行(可后台循环), 日志写入 xau_monitor.log, 行情突变(价/量异动)本地提醒
|
||||
- 循环间隔内置刷新倒计时显示(格式 分:秒), 归零自动触发刷新并重新计时
|
||||
- 读取账户当前 XAU 持仓/挂单(只读), 结合实时行情给出持仓操作建议(含保本最低收益率/保本价)
|
||||
- 实时抓取金十 7x24 新闻, 按标的自动关联, 给出≤10字核心说明与利多/利空倾向; 盈亏率着色(盈红亏绿)
|
||||
- 支持 --inst 指定盯盘标的(如 BTC-USDT-SWAP / ETH-USDT-SWAP), 新闻与建议随标的自动适配
|
||||
用法:
|
||||
python -X utf8 xau_monitor.py --dns-ip 198.18.0.0 # 前台循环, 每3分钟一次
|
||||
python -X utf8 xau_monitor.py --dns-ip 198.18.0.0 --once # 仅跑一次
|
||||
python -X utf8 xau_monitor.py --dns-ip 198.18.0.0 --alert-popup # 突变时弹窗(需 Windows)
|
||||
"""
|
||||
import json, hmac, hashlib, base64, datetime, sys, os, argparse, subprocess, time, math, re
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
KEYS_FILE = os.path.join(HERE, "okx_keys.json")
|
||||
LOG_FILE = os.path.join(HERE, "xau_monitor.log")
|
||||
INST = "XAU-USDT-SWAP"
|
||||
INTERVAL = 180 # 3 分钟
|
||||
|
||||
|
||||
# ---------- OKX 请求(走 fake-ip 绕过 DNS 污染 + curl 绕过 WAF) ----------
|
||||
def load_keys():
|
||||
with open(KEYS_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def sign(secret, ts, method, path, body=""):
|
||||
pre = ts + method.upper() + path + body
|
||||
return base64.b64encode(hmac.new(secret.encode(), pre.encode(), hashlib.sha256).digest()).decode()
|
||||
|
||||
|
||||
def okx_get(path, keys, dns_ip):
|
||||
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
||||
sig = sign(keys["secret"], ts, "GET", path, "")
|
||||
domain = "www.okx.com"
|
||||
cmd = ["curl", "-s", "--max-time", "15", "--resolve", f"{domain}:443:{dns_ip}"]
|
||||
cmd += ["-H", f"OK-ACCESS-KEY:{keys['api_key']}", "-H", f"OK-ACCESS-SIGN:{sig}",
|
||||
"-H", f"OK-ACCESS-TIMESTAMP:{ts}", "-H", f"OK-ACCESS-PASSPHRASE:{keys['passphrase']}",
|
||||
"-H", "Content-Type:application/json", "-H", "User-Agent:curl/8.5.0",
|
||||
f"https://{domain}{path}"]
|
||||
try:
|
||||
return json.loads(subprocess.run(cmd, capture_output=True, text=True, timeout=20).stdout)
|
||||
except Exception as e:
|
||||
return {"code": "-1", "msg": str(e)}
|
||||
|
||||
|
||||
# ---------- 终端着色(盈亏率: 盈利红 / 亏损绿) ----------
|
||||
_ANSI_RED = "\033[31m"
|
||||
_ANSI_GREEN = "\033[32m"
|
||||
_ANSI_RESET = "\033[0m"
|
||||
|
||||
|
||||
def enable_color():
|
||||
"""Windows 10+ 启用虚拟终端以显示 ANSI 颜色; 非 Windows/不支持则静默忽略."""
|
||||
if os.name == "nt":
|
||||
try:
|
||||
os.system("") # 触发启用 VT100 处理
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def red(s):
|
||||
return f"{_ANSI_RED}{s}{_ANSI_RESET}"
|
||||
|
||||
|
||||
def green(s):
|
||||
return f"{_ANSI_GREEN}{s}{_ANSI_RESET}"
|
||||
|
||||
|
||||
def strip_ansi(s):
|
||||
"""去除 ANSI 转义, 用于写日志文件避免乱码."""
|
||||
return re.sub(r"\033\[[0-9;]*m", "", s)
|
||||
|
||||
|
||||
# ---------- 实时新闻(金十 7x24, 走 fake-ip) ----------
|
||||
def news_profile(inst):
|
||||
"""根据标的代码返回新闻关键词配置.
|
||||
返回 dict: keywords(相关性过滤词), bias_bull(利好), bias_bear(利空), asset(中文名).
|
||||
宏观类关键词(美联储/美元/降息/加息/避险/地缘/非农/通胀/央行/美债)
|
||||
对所有品种通用, 叠加品种专属词."""
|
||||
macro = ["美联储", "美元", "降息", "加息", "避险", "地缘", "非农", "通胀",
|
||||
"央行", "美债", "杰克逊霍尔", "就业", "GDP", "PMI"]
|
||||
i = (inst or "").upper()
|
||||
if "XAU" in i or "黄金" in inst or "GOLD" in i:
|
||||
return dict(keywords=macro + ["黄金", "XAU", "白银", "贵金属", "购金", "增持黄金", "金矿"],
|
||||
bias_bull=["降息", "避险", "地缘", "宽松", "走弱", "下跌", "购金", "增持黄金"],
|
||||
bias_bear=["加息", "鹰派", "走强", "上涨", "紧缩", "强美元", "抛售", "减持黄金"],
|
||||
asset="黄金")
|
||||
if "BTC" in i or "比特币" in inst:
|
||||
return dict(keywords=macro + ["比特币", "BTC", "加密", "币圈", "ETF", "矿机", "灰度"],
|
||||
bias_bull=["降息", "宽松", "走弱", "下跌", "ETF", "流入", "减半", "采用"],
|
||||
bias_bear=["加息", "鹰派", "走强", "上涨", "紧缩", "强美元", "流出", "监管", "禁令"],
|
||||
asset="比特币")
|
||||
if "ETH" in i or "以太" in inst:
|
||||
return dict(keywords=macro + ["以太坊", "ETH", "加密", "币圈", "ETF", "灰度", "Layer2"],
|
||||
bias_bull=["降息", "宽松", "走弱", "下跌", "ETF", "流入", "升级", "采用"],
|
||||
bias_bear=["加息", "鹰派", "走强", "上涨", "紧缩", "强美元", "流出", "监管", "禁令"],
|
||||
asset="以太坊")
|
||||
# 默认(股票/其他): 仅宏观 + 标的自身代码作为关键词
|
||||
return dict(keywords=macro + [inst],
|
||||
bias_bull=["降息", "宽松", "走弱", "下跌", "利好", "超预期", "回购", "增持"],
|
||||
bias_bear=["加息", "鹰派", "走强", "上涨", "紧缩", "利空", "不及预期", "减持"],
|
||||
asset=inst)
|
||||
|
||||
|
||||
def get_news(dns_ip, inst, limit=5):
|
||||
"""抓取金十首页内嵌快讯, 过滤与标的(inst)相关的新闻, 返回 list[dict(title,brief,bias)].
|
||||
brief 为核心说明(≤10字); bias: 'bull'利好 / 'bear'利空 / 'neutral'中性(均相对该标的)."""
|
||||
domain = "www.jin10.com"
|
||||
cmd = ["curl", "-s", "--max-time", "15", "--resolve", f"{domain}:443:{dns_ip}",
|
||||
"-H", "User-Agent:curl/8.5.0", "-H", "Referer:https://www.jin10.com/",
|
||||
f"https://{domain}/"]
|
||||
try:
|
||||
html = subprocess.run(cmd, capture_output=True, text=True, timeout=20).stdout
|
||||
except Exception:
|
||||
return []
|
||||
if not html:
|
||||
return []
|
||||
prof = news_profile(inst)
|
||||
NEWS_KEYWORDS = prof["keywords"]
|
||||
BIAS_BULL = prof["bias_bull"]
|
||||
BIAS_BEAR = prof["bias_bear"]
|
||||
# 提取快讯标题: 优先 flash-top-list__item-content 文本, 兜底 title="..." 属性
|
||||
titles = re.findall(r'flash-top-list__item-content">(.*?)</div>', html, re.S)
|
||||
if not titles:
|
||||
kw_alt = "|".join(NEWS_KEYWORDS[:12])
|
||||
titles = re.findall(r'title="([^"]*(?:' + kw_alt + r')[^\"]*)"', html)
|
||||
# 清洗标签
|
||||
cleaned = []
|
||||
for t in titles:
|
||||
t = re.sub(r"<[^>]+>", "", t).strip()
|
||||
if t and len(t) > 4:
|
||||
cleaned.append(t)
|
||||
# 去重 + 过滤相关词
|
||||
seen, rel = set(), []
|
||||
for t in cleaned:
|
||||
if t in seen:
|
||||
continue
|
||||
seen.add(t)
|
||||
if any(k in t for k in NEWS_KEYWORDS):
|
||||
rel.append(t)
|
||||
if len(rel) >= limit * 3:
|
||||
break
|
||||
# 生成说明与倾向
|
||||
out = []
|
||||
for t in rel[:limit]:
|
||||
brief = t[:10] # 单条不超过10个字
|
||||
low = t
|
||||
if any(b in low for b in BIAS_BULL) and not any(b in low for b in BIAS_BEAR):
|
||||
bias = "bull"
|
||||
elif any(b in low for b in BIAS_BEAR):
|
||||
bias = "bear"
|
||||
else:
|
||||
bias = "neutral"
|
||||
out.append({"title": t, "brief": brief, "bias": bias})
|
||||
return out
|
||||
|
||||
|
||||
# ---------- 技术指标 ----------
|
||||
def sma(vals, n):
|
||||
if len(vals) < n:
|
||||
return None
|
||||
return sum(vals[-n:]) / n
|
||||
|
||||
|
||||
def ema(vals, n):
|
||||
if len(vals) < n:
|
||||
return None
|
||||
k = 2 / (n + 1)
|
||||
e = vals[0]
|
||||
for v in vals[1:]:
|
||||
e = v * k + e * (1 - k)
|
||||
return e
|
||||
|
||||
|
||||
def rsi(closes, n=14):
|
||||
if len(closes) < n + 1:
|
||||
return None
|
||||
gains, losses = [], []
|
||||
for i in range(1, len(closes)):
|
||||
d = closes[i] - closes[i - 1]
|
||||
gains.append(max(d, 0)); losses.append(max(-d, 0))
|
||||
# Wilder smoothing
|
||||
ag = sum(gains[:n]) / n; al = sum(losses[:n]) / n
|
||||
for i in range(n, len(gains)):
|
||||
ag = (ag * (n - 1) + gains[i]) / n
|
||||
al = (al * (n - 1) + losses[i]) / n
|
||||
if al == 0:
|
||||
return 100.0
|
||||
rs = ag / al
|
||||
return 100 - 100 / (1 + rs)
|
||||
|
||||
|
||||
def macd(closes, fast=12, slow=26, sig=9):
|
||||
if len(closes) < slow + sig:
|
||||
return None, None, None
|
||||
ema_f = ema(closes, fast)
|
||||
# 需逐根计算保证连续
|
||||
kf, ks, kg = 2 / (fast + 1), 2 / (slow + 1), 2 / (sig + 1)
|
||||
ef, es = closes[0], closes[0]
|
||||
ef_arr, es_arr = [], []
|
||||
for v in closes:
|
||||
ef = v * kf + ef * (1 - kf); es = v * ks + es * (1 - ks)
|
||||
ef_arr.append(ef); es_arr.append(es)
|
||||
dif = [ef_arr[i] - es_arr[i] for i in range(len(closes))]
|
||||
dea = dif[0]
|
||||
dea_arr = [dea]
|
||||
for v in dif[1:]:
|
||||
dea = v * kg + dea * (1 - kg); dea_arr.append(dea)
|
||||
dif_v, dea_v = dif[-1], dea_arr[-1]
|
||||
return dif_v, dea_v, (dif_v - dea_v)
|
||||
|
||||
|
||||
def sar(highs, lows, af_start=0.02, af_max=0.20):
|
||||
"""抛物线转向 SAR (Wilder). 返回最近一根的 SAR 值, 及当前多/空方向.
|
||||
highs/lows 为升序(旧->新)序列."""
|
||||
if len(highs) < 2:
|
||||
return None, None
|
||||
# 初始方向: 比较前两根收盘用高低判断
|
||||
uptrend = highs[1] >= highs[0]
|
||||
if uptrend:
|
||||
sar = lows[0]
|
||||
ep = highs[1]
|
||||
else:
|
||||
sar = highs[0]
|
||||
ep = lows[1]
|
||||
af = af_start
|
||||
for i in range(2, len(highs)):
|
||||
prev_sar = sar
|
||||
if uptrend:
|
||||
sar = prev_sar + af * (ep - prev_sar)
|
||||
# 不能高于前两根低
|
||||
sar = min(sar, lows[i - 1], lows[i - 2] if i >= 2 else lows[i - 1])
|
||||
if highs[i] > ep:
|
||||
ep = highs[i]; af = min(af + af_start, af_max)
|
||||
if lows[i] < sar: # 反转
|
||||
uptrend = False; sar = ep; ep = lows[i]; af = af_start
|
||||
else:
|
||||
sar = prev_sar + af * (ep - prev_sar)
|
||||
# 不能低于前两根高
|
||||
sar = max(sar, highs[i - 1], highs[i - 2] if i >= 2 else highs[i - 1])
|
||||
if lows[i] < ep:
|
||||
ep = lows[i]; af = min(af + af_start, af_max)
|
||||
if highs[i] > sar: # 反转
|
||||
uptrend = True; sar = ep; ep = highs[i]; af = af_start
|
||||
return sar, ("多" if uptrend else "空")
|
||||
|
||||
|
||||
# ---------- 持仓/挂单查询(只读) + 操作建议 ----------
|
||||
def get_positions_and_orders(keys, dns_ip, inst):
|
||||
"""查询指定标的(inst)的当前持仓与未成交挂单(只读, 不下单).
|
||||
返回 (positions, orders), 任一查询失败则该部分为空列表."""
|
||||
positions, orders = [], []
|
||||
try:
|
||||
p = okx_get(f"/api/v5/account/positions?instType=SWAP&instId={inst}", keys, dns_ip)
|
||||
if p.get("code") == "0":
|
||||
for it in p.get("data", []):
|
||||
if float(it.get("pos", 0) or 0) != 0: # 仅保留有实际仓位的
|
||||
positions.append(it)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
o = okx_get(f"/api/v5/trade/orders-pending?instType=SWAP&instId={inst}", keys, dns_ip)
|
||||
if o.get("code") == "0":
|
||||
for it in o.get("data", []):
|
||||
orders.append(it)
|
||||
except Exception:
|
||||
pass
|
||||
return positions, orders
|
||||
|
||||
|
||||
def position_advice(positions, orders, d, fee_rate=0.0005):
|
||||
"""结合实时行情 d, 给出每个持仓/挂单的操作建议. 返回字符串列表.
|
||||
fee_rate: 单边 taker 手续费率(默认 0.05%), 保本按 开仓+平仓 两次计."""
|
||||
lines = []
|
||||
last = d["last"]
|
||||
sar_v, sar_dir = d["sar"], d["sar_dir"]
|
||||
support, resistance = d["support"], d["resistance"]
|
||||
trend = d["trend"]
|
||||
# 保本所需总费率(开+平)
|
||||
be_rate = fee_rate * 2
|
||||
|
||||
if not positions and not orders:
|
||||
lines.append("当前无持仓、无挂单, 账户空闲, 可按信号择机建仓.")
|
||||
return lines
|
||||
|
||||
# 持仓建议
|
||||
for p in positions:
|
||||
side = p.get("posSide", "")
|
||||
sz = float(p.get("pos", 0) or 0)
|
||||
avg = float(p.get("avgPx", 0) or 0)
|
||||
mark = float(p.get("markPx", 0) or 0)
|
||||
liq = float(p.get("liqPx", 0) or 0)
|
||||
upl = float(p.get("upl", 0) or 0)
|
||||
lev = p.get("lever", "—")
|
||||
# OKX net 模式(单合约净持仓)下 posSide=net 即代表多头仓位
|
||||
eff_side = "long" if side in ("long", "net") else "short"
|
||||
side_cn = {"long": "多", "short": "空"}.get(eff_side, eff_side)
|
||||
# 浮盈/亏百分比(相对开仓价)
|
||||
pnl_pct = (mark - avg) / avg * 100 if avg else 0
|
||||
if eff_side == "short":
|
||||
pnl_pct = -pnl_pct
|
||||
# 距强平价的安全垫(多头: 现价-强平; 空头: 强平-现价)
|
||||
if liq:
|
||||
if eff_side == "long":
|
||||
liq_buf = (last - liq) / last * 100
|
||||
else:
|
||||
liq_buf = (liq - last) / last * 100
|
||||
else:
|
||||
liq_buf = None
|
||||
|
||||
# 保本价与保本最低收益率(覆盖开仓+平仓手续费, 不含资金占用时间成本)
|
||||
# 公式: 盈亏按名义额放大杠杆, 手续费同样按名义额计 -> 保本涨跌幅≈开平费率之和, 与杠杆无关
|
||||
if eff_side == "long":
|
||||
be_px = avg * (1 + be_rate)
|
||||
be_ret = (be_px - last) / last * 100 # 现价还需再涨多少才回本
|
||||
else:
|
||||
be_px = avg * (1 - be_rate)
|
||||
be_ret = (last - be_px) / last * 100 # 现价还需再跌多少才回本
|
||||
|
||||
adv = []
|
||||
# 1) 技术面与持仓方向冲突
|
||||
if eff_side == "long" and sar_dir == "空":
|
||||
adv.append("SAR 转空/价格已跌破 SAR, 多单与趋势背离, 建议减仓或设严止损")
|
||||
elif eff_side == "short" and sar_dir == "多":
|
||||
adv.append("SAR 转多/价格已上破 SAR, 空单与趋势背离, 建议减仓或设严止损")
|
||||
# 2) 逼近阻力/支撑(多单看阻力, 空单看支撑)
|
||||
if eff_side == "long" and (resistance - last) / last * 100 < 0.4:
|
||||
adv.append(f"价格贴近阻力 {resistance:.1f}, 多单可逢高部分止盈")
|
||||
if eff_side == "short" and (last - support) / last * 100 < 0.4:
|
||||
adv.append(f"价格贴近支撑 {support:.1f}, 空单可逢低部分止盈")
|
||||
# 3) 浮盈保护
|
||||
if pnl_pct > 1.5:
|
||||
adv.append(f"浮盈 {pnl_pct:+.2f}%, 可上移止损至成本/保本, 锁定利润")
|
||||
elif pnl_pct < -1.5:
|
||||
adv.append(f"浮亏 {pnl_pct:+.2f}%, 接近止损阈值, 严格按原计划止损勿加仓摊平")
|
||||
# 4) 强平风险
|
||||
if liq_buf is not None and liq_buf < 3:
|
||||
adv.append(f"⚠距强平价仅 {liq_buf:.1f}%, 杠杆 {lev}, 风险极高, 建议降杠杆或减仓")
|
||||
# 5) 保本最低收益率
|
||||
if be_ret > 0:
|
||||
adv.append(f"保本价 {be_px:.1f}, 现价需再{'涨' if eff_side=='long' else '跌'} "
|
||||
f"{be_ret:.2f}% 方可覆盖手续费成本(费率 {be_rate*100:.2f}%)")
|
||||
else:
|
||||
adv.append(f"保本价 {be_px:.1f} 已低于/高于现价, 当前价位平仓已覆盖手续费成本")
|
||||
if not adv:
|
||||
adv.append("持仓方向与指标暂无明显冲突, 可持有并跟踪止损")
|
||||
# 盈亏率着色: 盈利红 / 亏损绿
|
||||
if upl >= 0:
|
||||
pnl_disp = red(f"{upl:+,.2f}({pnl_pct:+.2f}%)")
|
||||
else:
|
||||
pnl_disp = green(f"{upl:+,.2f}({pnl_pct:+.2f}%)")
|
||||
lines.append(f"持仓[{side_cn}] 数量 {sz:.0f} 开仓 {avg:.1f} 标记 {mark:.1f} "
|
||||
f"浮盈亏 {pnl_disp} 保本价 {be_px:.1f} "
|
||||
f"强平 {liq:.1f} 杠杆 {lev}x")
|
||||
for a in adv:
|
||||
lines.append(f" ↳ {a}")
|
||||
|
||||
# 挂单建议
|
||||
for o in orders:
|
||||
o_side = o.get("side", "") # buy/sell
|
||||
o_pos = o.get("posSide", "") # long/short/net (开平方向)
|
||||
px = float(o.get("px", 0) or 0)
|
||||
sz = float(o.get("sz", 0) or 0)
|
||||
ord_type = o.get("ordType", "")
|
||||
side_cn = {"buy": "买", "sell": "卖"}.get(o_side, o_side)
|
||||
# 推断挂单意图: buy+net=开多挂单, sell+net=平多挂单, long/short 为双向持仓模式的开平仓
|
||||
if o_pos == "net":
|
||||
intent = "开多" if o_side == "buy" else "平多"
|
||||
elif o_pos == "long":
|
||||
intent = "开多" if o_side == "buy" else "平多"
|
||||
elif o_pos == "short":
|
||||
intent = "开空" if o_side == "sell" else "平空"
|
||||
else:
|
||||
intent = o_pos
|
||||
# 挂单相对现价的位置
|
||||
gap_pct = (px - last) / last * 100
|
||||
adv = []
|
||||
if intent in ("开多",):
|
||||
# 开多挂单: 通常在现价下方买入, 高于现价属突破追多
|
||||
if gap_pct < -1.0:
|
||||
adv.append(f"挂开多单远低于现价 {gap_pct:+.2f}%, 若趋势转空可能接到下跌飞刀, 建议评估撤改")
|
||||
elif gap_pct > 0:
|
||||
adv.append(f"挂开多单高于现价 {gap_pct:+.2f}%, 属突破追多单, 确认趋势向上后再保留")
|
||||
else:
|
||||
adv.append("挂开多单贴近现价, 成交概率高, 注意仓位与止损")
|
||||
elif intent in ("开空",):
|
||||
if gap_pct > 1.0:
|
||||
adv.append(f"挂开空单远高于现价 {gap_pct:+.2f}%, 若趋势转多可能接到上涨飞刀, 建议评估撤改")
|
||||
elif gap_pct < 0:
|
||||
adv.append(f"挂开空单低于现价 {gap_pct:+.2f}%, 属破位追空单, 确认趋势向下后再保留")
|
||||
else:
|
||||
adv.append("挂开空单贴近现价, 成交概率高, 注意仓位与止损")
|
||||
elif intent in ("平多",):
|
||||
# 平多挂单: 通常在现价上方卖出平仓锁定利润
|
||||
if gap_pct > 0.3:
|
||||
adv.append(f"挂平多单高于现价 {gap_pct:+.2f}%, 为高位止盈/平仓单, 趋势不破可保留")
|
||||
elif gap_pct < 0:
|
||||
adv.append(f"挂平多单低于现价 {gap_pct:+.2f}%, 属止损平仓单, 触发即离场")
|
||||
else:
|
||||
adv.append("挂平多单贴近现价, 成交概率高")
|
||||
elif intent in ("平空",):
|
||||
if gap_pct < -0.3:
|
||||
adv.append(f"挂平空单低于现价 {gap_pct:+.2f}%, 为低位止盈/平仓单, 趋势不破可保留")
|
||||
elif gap_pct > 0:
|
||||
adv.append(f"挂平空单高于现价 {gap_pct:+.2f}%, 属止损平仓单, 触发即离场")
|
||||
else:
|
||||
adv.append("挂平空单贴近现价, 成交概率高")
|
||||
# 与当前趋势一致性(仅对开仓类挂单提示)
|
||||
if intent == "开多" and trend == "下跌":
|
||||
adv.append(f"当前趋势为{trend}, 与开多挂单相悖, 谨慎")
|
||||
if intent == "开空" and trend == "上涨":
|
||||
adv.append(f"当前趋势为{trend}, 与开空挂单相悖, 谨慎")
|
||||
lines.append(f"挂单[{side_cn}/{intent}] 价格 {px:.1f} 数量 {sz:.0f} 类型 {ord_type} "
|
||||
f"(距现价 {gap_pct:+.2f}%)")
|
||||
for a in adv:
|
||||
lines.append(f" ↳ {a}")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
# ---------- 主分析 ----------
|
||||
def analyze(keys, dns_ip, fee_rate=0.0005, inst="XAU-USDT-SWAP"):
|
||||
c = okx_get(f"/api/v5/market/candles?instId={inst}&bar=1m&limit=120", keys, dns_ip)
|
||||
t = okx_get(f"/api/v5/market/ticker?instId={inst}", keys, dns_ip)
|
||||
if c.get("code") != "0" or t.get("code") != "0":
|
||||
raise RuntimeError(f"行情获取失败 c={c.get('msg')} t={t.get('msg')}")
|
||||
rows = c["data"][::-1] # 转升序: 最旧->最新
|
||||
closes = [float(x[4]) for x in rows]
|
||||
highs = [float(x[2]) for x in rows]
|
||||
lows = [float(x[3]) for x in rows]
|
||||
vols = [float(x[7]) for x in rows] # USDT 计价成交量
|
||||
last = closes[-1]
|
||||
prev = closes[-2]
|
||||
|
||||
# 均线
|
||||
sma7 = sma(closes, 7); sma25 = sma(closes, 25); sma60 = sma(closes, 60)
|
||||
ema12 = ema(closes, 12); ema26 = ema(closes, 26)
|
||||
r = rsi(closes, 14)
|
||||
dif, dea, hist = macd(closes)
|
||||
sar_v, sar_dir = sar(highs, lows)
|
||||
|
||||
# 趋势判定
|
||||
# 斜率: 近15根线性回归斜率
|
||||
n = 15
|
||||
ys = closes[-n:]
|
||||
xs = list(range(n))
|
||||
mx = sum(xs) / n; my = sum(ys) / n
|
||||
num = sum((xs[i] - mx) * (ys[i] - my) for i in range(n))
|
||||
den = sum((xs[i] - mx) ** 2 for i in range(n))
|
||||
slope = num / den if den else 0
|
||||
slope_pct = slope / last * 100 # 每根(分钟)百分比斜率
|
||||
|
||||
ema_gap = (ema12 - ema26) / last * 100
|
||||
above_ma = (last > sma25) and (last > sma60)
|
||||
# SAR 方向加权
|
||||
sar_up = (sar_dir == "多")
|
||||
|
||||
if slope_pct > 0.02 and ema_gap > 0 and above_ma and sar_up:
|
||||
trend = "上涨"
|
||||
elif slope_pct < -0.02 and ema_gap < 0 and not above_ma and not sar_up:
|
||||
trend = "下跌"
|
||||
else:
|
||||
trend = "震荡"
|
||||
|
||||
# 支撑/阻力: 近期高低 + 均线 + 枢轴
|
||||
recent_h = max(highs[-60:]); recent_l = min(lows[-60:])
|
||||
pivot = (recent_h + recent_l + last) / 3
|
||||
# 成交量异动
|
||||
avg_vol = sum(vols[-30:-1]) / 29
|
||||
cur_vol = vols[-1]
|
||||
vol_spike = cur_vol > avg_vol * 2.2
|
||||
|
||||
# 关键位
|
||||
support = min(recent_l, sma60 if sma60 else recent_l)
|
||||
resistance = max(recent_h, sma25 if sma25 else recent_h)
|
||||
|
||||
# ---------- 操作建议逻辑 ----------
|
||||
reasons = []
|
||||
score = 0 # -3..+3
|
||||
# 趋势权重
|
||||
if trend == "上涨":
|
||||
score += 1; reasons.append("趋势向上(EMA12>EMA26,价格站上均线)")
|
||||
elif trend == "下跌":
|
||||
score -= 1; reasons.append("趋势向下")
|
||||
else:
|
||||
reasons.append("区间震荡, 等待突破")
|
||||
|
||||
# RSI
|
||||
if r is not None:
|
||||
if r > 70:
|
||||
score -= 1; reasons.append(f"RSI {r:.0f} 超买, 追多风险大")
|
||||
elif r < 30:
|
||||
score += 1; reasons.append(f"RSI {r:.0f} 超卖, 反弹概率高")
|
||||
elif r > 55:
|
||||
reasons.append(f"RSI {r:.0f} 偏强")
|
||||
elif r < 45:
|
||||
reasons.append(f"RSI {r:.0f} 偏弱")
|
||||
|
||||
# MACD
|
||||
if dif is not None:
|
||||
if hist > 0 and dif > dea:
|
||||
score += 0.5; reasons.append("MACD 金叉/红柱放大")
|
||||
elif hist < 0 and dif < dea:
|
||||
score -= 0.5; reasons.append("MACD 死叉/绿柱放大")
|
||||
if abs(hist) < 0.3:
|
||||
reasons.append("MACD 动能温和")
|
||||
|
||||
# SAR
|
||||
if sar_v is not None:
|
||||
if sar_dir == "多":
|
||||
score += 0.5; reasons.append(f"SAR 多方(点 {sar_v:.1f}, 价格在其上)")
|
||||
else:
|
||||
score -= 0.5; reasons.append(f"SAR 空方(点 {sar_v:.1f}, 价格在其下)")
|
||||
|
||||
# 位置相对支撑阻力
|
||||
dist_to_s = (last - support) / last * 100
|
||||
dist_to_r = (resistance - last) / last * 100
|
||||
if dist_to_s < 0.3:
|
||||
score += 0.5; reasons.append(f"贴近支撑 {support:.1f}, 接多性价比高")
|
||||
elif dist_to_r < 0.3:
|
||||
score -= 0.5; reasons.append(f"贴近阻力 {resistance:.1f}, 做多空间受限")
|
||||
|
||||
# 成交量异动
|
||||
if vol_spike:
|
||||
if trend == "上涨":
|
||||
score += 0.5; reasons.append("放量上涨, 动量确认")
|
||||
elif trend == "下跌":
|
||||
score -= 0.5; reasons.append("放量下跌, 动量确认")
|
||||
else:
|
||||
reasons.append("成交量突增但方向未明, 警惕变盘")
|
||||
|
||||
# 决策
|
||||
if score >= 2:
|
||||
action = "做多"; conf = min(95, 55 + score * 12)
|
||||
elif score <= -2:
|
||||
action = "做空"; conf = min(95, 55 + abs(score) * 12)
|
||||
else:
|
||||
action = "观望"; conf = 50 + abs(score) * 5
|
||||
|
||||
# 入场/止损/止盈(基于建议方向, 仅供参考)
|
||||
if action == "做多":
|
||||
entry = f"{last:.1f} 附近"
|
||||
sl = f"{max(support, last*0.997):.1f}"
|
||||
tp = f"{min(resistance, last*1.006):.1f}"
|
||||
elif action == "做空":
|
||||
entry = f"{last:.1f} 附近"
|
||||
sl = f"{min(resistance, last*1.003):.1f}"
|
||||
tp = f"{max(support, last*0.994):.1f}"
|
||||
else:
|
||||
entry = sl = tp = "—"
|
||||
|
||||
# 持仓/挂单操作建议(只读查询, 失败不影响主行情)
|
||||
try:
|
||||
positions, orders = get_positions_and_orders(keys, dns_ip, inst)
|
||||
pos_advice = position_advice(positions, orders, dict(
|
||||
last=last, sar=sar_v, sar_dir=sar_dir, support=support,
|
||||
resistance=resistance, trend=trend), fee_rate=fee_rate)
|
||||
except Exception as e:
|
||||
pos_advice = [f"持仓查询失败(不影响行情): {e}"]
|
||||
|
||||
# 实时新闻(失败不影响主行情), 按标的自动关联
|
||||
try:
|
||||
news = get_news(dns_ip, inst)
|
||||
except Exception:
|
||||
news = []
|
||||
# 新闻面倾向统计(融入操作建议)
|
||||
nbull = sum(1 for n in news if n['bias'] == 'bull')
|
||||
nbear = sum(1 for n in news if n['bias'] == 'bear')
|
||||
if nbull > nbear:
|
||||
news_bias = "bull"
|
||||
reasons.append(f"新闻面偏多({nbull}利好/{nbear}利空{news[0]['brief'] if news else ''}), 倾向做多")
|
||||
if action == "做空":
|
||||
conf = max(20, conf - 8)
|
||||
elif nbear > nbull:
|
||||
news_bias = "bear"
|
||||
reasons.append(f"新闻面偏空({nbull}利好/{nbear}利空{news[0]['brief'] if news else ''}), 倾向做空")
|
||||
if action == "做多":
|
||||
conf = max(20, conf - 8)
|
||||
else:
|
||||
news_bias = "neutral"
|
||||
if news:
|
||||
reasons.append(f"新闻面中性({nbull}利好/{nbear}利空), 以技术面为主")
|
||||
|
||||
return dict(last=last, prev=prev, sma7=sma7, sma25=sma25, sma60=sma60,
|
||||
ema12=ema12, ema26=ema26, rsi=r, dif=dif, dea=dea, hist=hist,
|
||||
sar=sar_v, sar_dir=sar_dir,
|
||||
trend=trend, slope_pct=slope_pct, recent_h=recent_h, recent_l=recent_l,
|
||||
pivot=pivot, avg_vol=avg_vol, cur_vol=cur_vol, vol_spike=vol_spike,
|
||||
support=support, resistance=resistance, action=action, conf=conf,
|
||||
entry=entry, sl=sl, tp=tp, reasons=reasons, score=score,
|
||||
pos_advice=pos_advice, news=news, news_bias=news_bias)
|
||||
|
||||
|
||||
def fmt(x, d=2):
|
||||
try:
|
||||
return f"{float(x):,.{d}f}"
|
||||
except Exception:
|
||||
return "—"
|
||||
|
||||
|
||||
def report(d):
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
bar = "=" * 64
|
||||
inst = d.get('inst', 'XAU-USDT-SWAP')
|
||||
asset = news_profile(inst)['asset']
|
||||
lines = [bar, f"{inst} 盯盘 {now}", bar]
|
||||
lines.append(f"现价 {fmt(d['last'])} 前一根 {fmt(d['prev'])} 涨跌 {(d['last']-d['prev'])/d['prev']*100:+.2f}%")
|
||||
lines.append(f"趋势: {d['trend']} 斜率 {d['slope_pct']:+.3f}%/min")
|
||||
lines.append(f"均线 SMA7 {fmt(d['sma7'])} SMA25 {fmt(d['sma25'])} SMA60 {fmt(d['sma60'])}")
|
||||
lines.append(f"EMA EMA12 {fmt(d['ema12'])} EMA26 {fmt(d['ema26'])} 差 {fmt((d['ema12']-d['ema26'])/d['last']*100)}%")
|
||||
lines.append(f"RSI(14) {fmt(d['rsi'],1)} MACD DIF {fmt(d['dif'])} DEA {fmt(d['dea'])} HIST {fmt(d['hist'])}")
|
||||
sar_rel = "上方(多头守护)" if (d['sar_dir'] == "多") else "下方(空方压制)"
|
||||
lines.append(f"SAR {fmt(d['sar'])} 方向 {d['sar_dir']} 现价{d['sar'] and ('在SAR'+sar_rel)}")
|
||||
lines.append(f"支撑 {fmt(d['support'])} 阻力 {fmt(d['resistance'])} 枢轴 {fmt(d['pivot'])}")
|
||||
lines.append(f"成交量 当前 {fmt(d['cur_vol'])} USDT / 均值 {fmt(d['avg_vol'])} {'⚡异动' if d['vol_spike'] else ''}")
|
||||
lines.append("-" * 64)
|
||||
lines.append(f"【操作建议】 {d['action']} 置信度 {d['conf']:.0f}%")
|
||||
if d['action'] != "观望":
|
||||
lines.append(f" 入场 {d['entry']} 止损 {d['sl']} 止盈 {d['tp']} (3~15分钟级别)")
|
||||
lines.append(" 依据:")
|
||||
for rsn in d['reasons']:
|
||||
lines.append(f" · {rsn}")
|
||||
lines.append("-" * 64)
|
||||
lines.append("【当前持仓操作建议】(基于实时行情, 仅供参考, 脚本不下单)")
|
||||
for pa in d.get('pos_advice', []):
|
||||
lines.append(f" {pa}")
|
||||
lines.append("-" * 64)
|
||||
lines.append(f"【相关新闻 · {asset}】(金十 7x24, 实时抓取, 按标的自动关联)")
|
||||
news = d.get('news', [])
|
||||
if not news:
|
||||
lines.append(f" 暂未抓取到与{asset}相关快讯")
|
||||
else:
|
||||
bias_cn = {"bull": red(f"利好{asset}"), "bear": green(f"利空{asset}"), "neutral": "中性影响"}
|
||||
for n in news:
|
||||
lines.append(f" · {n['brief']} [{bias_cn.get(n['bias'], '中性影响')}]")
|
||||
# 新闻对持仓操作的综合倾向提示
|
||||
bull = sum(1 for n in news if n['bias'] == 'bull')
|
||||
bear = sum(1 for n in news if n['bias'] == 'bear')
|
||||
bias_word = {"bull": red("偏多"), "bear": green("偏空"), "neutral": "中性"}[d.get('news_bias', 'neutral')]
|
||||
lines.append(f" ↳ 新闻面{bias_word}({bull}利好/{bear}利空), 已融入上方操作建议")
|
||||
lines.append(bar)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def log_write(text):
|
||||
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||
f.write(strip_ansi(text) + "\n")
|
||||
|
||||
|
||||
def popup(title, msg):
|
||||
try:
|
||||
import ctypes
|
||||
ctypes.windll.user32.MessageBoxW(0, msg, title, 0x40 | 0x1000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--dns-ip", default="198.18.0.0")
|
||||
ap.add_argument("--once", action="store_true")
|
||||
ap.add_argument("--alert-popup", action="store_true", help="行情突变弹窗提醒")
|
||||
ap.add_argument("--fee-rate", type=float, default=0.0005,
|
||||
help="单边 taker 手续费率(默认 0.0005=0.05%, 保本按开+平两次计)")
|
||||
ap.add_argument("--inst", default="XAU-USDT-SWAP",
|
||||
help="盯盘标的代码, 如 XAU-USDT-SWAP / BTC-USDT-SWAP / ETH-USDT-SWAP")
|
||||
args = ap.parse_args()
|
||||
enable_color() # 启用终端 ANSI 颜色(盈亏率着色)
|
||||
keys = load_keys()
|
||||
inst = args.inst.upper()
|
||||
|
||||
last_alert = {} # 突变去重
|
||||
while True:
|
||||
try:
|
||||
d = analyze(keys, args.dns_ip, fee_rate=args.fee_rate, inst=inst)
|
||||
d['inst'] = inst
|
||||
out = report(d)
|
||||
print(out)
|
||||
log_write(out)
|
||||
# 行情突变提醒
|
||||
price_chg = abs(d['last'] - d['prev']) / d['prev'] * 100
|
||||
alert_msgs = []
|
||||
if d['vol_spike']:
|
||||
alert_msgs.append(f"⚡成交量突增: 当前 {fmt(d['cur_vol'])} vs 均值 {fmt(d['avg_vol'])}")
|
||||
if price_chg > 0.3:
|
||||
alert_msgs.append(f"⚠价格急动: {d['last']-d['prev']:+.1f} ({price_chg:+.2f}%)/分钟")
|
||||
if d['rsi'] is not None and (d['rsi'] > 75 or d['rsi'] < 25):
|
||||
alert_msgs.append(f"⚠RSI 极端 {d['rsi']:.0f} (超{'买' if d['rsi']>75 else '卖'})")
|
||||
if alert_msgs and (args.alert_popup or True):
|
||||
tag = "|".join(alert_msgs)
|
||||
if last_alert.get(tag, 0) < time.time() - 300: # 5分钟内同内容不重复
|
||||
log_write("【提醒】 " + " ; ".join(alert_msgs))
|
||||
if args.alert_popup:
|
||||
popup("XAU 行情突变", "\n".join(alert_msgs) + f"\n现价 {fmt(d['last'])}")
|
||||
last_alert[tag] = time.time()
|
||||
except Exception as e:
|
||||
err = f"[{datetime.datetime.now()}] 运行错误: {e}"
|
||||
print(err); log_write(err)
|
||||
|
||||
if args.once:
|
||||
break
|
||||
|
||||
# ---------- 刷新倒计时(分:秒), 归零自动触发下一轮刷新 ----------
|
||||
# 用目标绝对时间点计算剩余, 避免 sleep 累积漂移导致计时不同步
|
||||
next_at = time.time() + INTERVAL
|
||||
while True:
|
||||
remain = int(round(next_at - time.time()))
|
||||
if remain <= 0:
|
||||
break
|
||||
mm = remain // 60
|
||||
ss = remain % 60
|
||||
# \r 回到行首覆盖, 不换行; flush 确保即时显示
|
||||
print(f"\r距下次刷新: {mm:02d}:{ss:02d} ", end="", flush=True)
|
||||
# 整秒粒度休眠, 剩余 <1s 时立即退出循环进入刷新
|
||||
time.sleep(min(1, remain))
|
||||
# 收尾换行, 下一轮刷新会先打印分隔报告
|
||||
print("\r距下次刷新: 00:00 刷新中...\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user