Spaces:
Paused
Paused
zt p commited on
Commit ·
b04609c
1
Parent(s): cd60f2f
Tune market move alert cadence
Browse files- README.md +3 -1
- app/monitors/market_move.py +63 -27
- app/monitors/strategy_treasury.py +5 -1
README.md
CHANGED
|
@@ -115,12 +115,14 @@ DISABLE_WORKER=true .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 7860
|
|
| 115 |
- `STRATEGY_SEC_8K_SCAN_LIMIT`:启动或最新 8-K 变化时最多扫描多少份 8-K,默认 `20`。
|
| 116 |
- `SEC_USER_AGENT`:SEC API User-Agent,建议配置成包含联系方式的字符串。
|
| 117 |
- `ENABLE_MARKET_MOVE`:是否启用全市场日/月涨跌监控,默认 `true`。
|
|
|
|
| 118 |
- `MARKET_MOVE_INTERVAL_SEC`:全市场涨跌监控轮询间隔,默认 `300`。
|
| 119 |
-
- `MARKET_MOVE_THRESHOLD_PCT`:
|
| 120 |
- `MARKET_MOVE_LOOKBACK_DAYS`:月线对比天数,默认 `30`。
|
| 121 |
- `MARKET_MOVE_EXCHANGES`:启用的交易所列表,逗号分隔;默认启用 Binance、OKX、Bitget、Hyperliquid、Asterdex、Bybit、Gate、KuCoin、MEXC、Kraken、Upbit。
|
| 122 |
- `MARKET_MOVE_COINS`:覆盖默认币种列表,格式如 `BTC,ETH,HYPE:Hyperliquid`。
|
| 123 |
- `MARKET_MOVE_EXTRA_COINS`:在默认币种列表后追加币种,格式同上。
|
|
|
|
| 124 |
- `BINANCE_API_KEY` / `BINANCE_API_SECRET`:币安私有接口凭证,可选。
|
| 125 |
- `BINANCE_SPOT_BASE` / `BINANCE_FAPI_BASE` / `BINANCE_FSTREAM_BASE`:币安接口基址,可选。
|
| 126 |
- `BINANCE_ALPHA_BASE`:Binance Alpha 公共行情基址,默认 `https://www.binance.com`。
|
|
|
|
| 115 |
- `STRATEGY_SEC_8K_SCAN_LIMIT`:启动或最新 8-K 变化时最多扫描多少份 8-K,默认 `20`。
|
| 116 |
- `SEC_USER_AGENT`:SEC API User-Agent,建议配置成包含联系方式的字符串。
|
| 117 |
- `ENABLE_MARKET_MOVE`:是否启用全市场日/月涨跌监控,默认 `true`。
|
| 118 |
+
- `MARKET_MOVE_WEWORK_WEBHOOK`:全市场日/月涨跌监控专用企业微信机器人;未配置时使用代码内默认的“寻找牛市标的”机器人。
|
| 119 |
- `MARKET_MOVE_INTERVAL_SEC`:全市场涨跌监控轮询间隔,默认 `300`。
|
| 120 |
+
- `MARKET_MOVE_THRESHOLD_PCT`:兼容旧配置;当前实际提醒规则为 30% 每 24 小时、50% 每 12 小时、60% 每 6 小时、70% 每 3 小时、80% 每 1 小时,首次触发立即通知。
|
| 121 |
- `MARKET_MOVE_LOOKBACK_DAYS`:月线对比天数,默认 `30`。
|
| 122 |
- `MARKET_MOVE_EXCHANGES`:启用的交易所列表,逗号分隔;默认启用 Binance、OKX、Bitget、Hyperliquid、Asterdex、Bybit、Gate、KuCoin、MEXC、Kraken、Upbit。
|
| 123 |
- `MARKET_MOVE_COINS`:覆盖默认币种列表,格式如 `BTC,ETH,HYPE:Hyperliquid`。
|
| 124 |
- `MARKET_MOVE_EXTRA_COINS`:在默认币种列表后追加币种,格式同上。
|
| 125 |
+
- `STRATEGY_TREASURY_WEWORK_WEBHOOK`:Strategy 财务监控专用企业微信机器人;未配置时使用代码内默认的 Strategy 财务机器人。
|
| 126 |
- `BINANCE_API_KEY` / `BINANCE_API_SECRET`:币安私有接口凭证,可选。
|
| 127 |
- `BINANCE_SPOT_BASE` / `BINANCE_FAPI_BASE` / `BINANCE_FSTREAM_BASE`:币安接口基址,可选。
|
| 128 |
- `BINANCE_ALPHA_BASE`:Binance Alpha 公共行情基址,默认 `https://www.binance.com`。
|
app/monitors/market_move.py
CHANGED
|
@@ -17,6 +17,18 @@ SERVICE_ID = "market_move:service"
|
|
| 17 |
CONFIG_KEY = "market_move:config"
|
| 18 |
ALERT_STATE_KEY = "market_move:alert_state"
|
| 19 |
HEALTH_ALERT_COOLDOWN_SEC = 3600
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
SUPPORTED_EXCHANGES = [
|
| 22 |
"binance",
|
|
@@ -219,6 +231,14 @@ def format_pct(value):
|
|
| 219 |
return f"{value:+.2f}%"
|
| 220 |
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
def market_type_label(value):
|
| 223 |
return "现货" if value == "spot" else "合约"
|
| 224 |
|
|
@@ -1179,7 +1199,7 @@ class MarketMoveMonitor(BaseMonitor):
|
|
| 1179 |
self._failure_counts = {}
|
| 1180 |
self._health_alert_at = {}
|
| 1181 |
self.client = PublicMarketDataClient(self.log, self.config)
|
| 1182 |
-
self.pusher = legacy.WeWorkBotPusher(
|
| 1183 |
self._register_monitors()
|
| 1184 |
|
| 1185 |
def start(self):
|
|
@@ -1390,8 +1410,6 @@ class MarketMoveMonitor(BaseMonitor):
|
|
| 1390 |
return candles[0].close
|
| 1391 |
|
| 1392 |
def _handle_snapshot_alerts(self, snapshot, cfg):
|
| 1393 |
-
threshold = float(cfg.get("threshold_pct", 30.0))
|
| 1394 |
-
reset_threshold = threshold * float(cfg.get("reset_ratio", 0.8))
|
| 1395 |
state = self.store.get_kv(ALERT_STATE_KEY, {})
|
| 1396 |
sent = 0
|
| 1397 |
changed = False
|
|
@@ -1400,39 +1418,57 @@ class MarketMoveMonitor(BaseMonitor):
|
|
| 1400 |
for window, change_info in snapshot.get("changes", {}).items():
|
| 1401 |
change_pct = change_info.get("change_pct")
|
| 1402 |
key = f"{snapshot['symbol']}:{window}"
|
| 1403 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1404 |
continue
|
| 1405 |
-
|
| 1406 |
-
|
| 1407 |
-
|
| 1408 |
-
|
| 1409 |
-
|
| 1410 |
-
|
| 1411 |
-
|
| 1412 |
-
|
| 1413 |
-
|
| 1414 |
-
|
| 1415 |
-
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
|
| 1422 |
-
|
| 1423 |
-
|
| 1424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1425 |
if changed:
|
| 1426 |
self.store.set_kv(ALERT_STATE_KEY, state)
|
| 1427 |
return sent
|
| 1428 |
|
| 1429 |
-
def _send_move_alert(self, snapshot, window, change_info, direction,
|
| 1430 |
-
title = f"{snapshot['symbol']} {change_info.get('label') or window}涨跌超过 {threshold:g}%"
|
| 1431 |
text = (
|
| 1432 |
-
"
|
| 1433 |
f"币种:{snapshot.get('name')} ({snapshot.get('symbol')})\n"
|
| 1434 |
f"方向:{direction}\n"
|
| 1435 |
f"周期:{change_info.get('label') or window}\n"
|
|
|
|
| 1436 |
f"涨跌幅:{format_pct(change_info.get('change_pct'))}\n"
|
| 1437 |
f"当前价格:{fmt_price(snapshot.get('price'))}\n"
|
| 1438 |
f"参考价格:{fmt_price(change_info.get('reference_price'))}\n"
|
|
|
|
| 17 |
CONFIG_KEY = "market_move:config"
|
| 18 |
ALERT_STATE_KEY = "market_move:alert_state"
|
| 19 |
HEALTH_ALERT_COOLDOWN_SEC = 3600
|
| 20 |
+
MARKET_MOVE_WEWORK_WEBHOOK = os.getenv(
|
| 21 |
+
"MARKET_MOVE_WEWORK_WEBHOOK",
|
| 22 |
+
"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1b44a22e-5d3d-4336-86a0-4662a3b9460f",
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
MOVEMENT_ALERT_RULES = [
|
| 26 |
+
{"threshold": 80.0, "cooldown": 3600, "label": "80%+", "frequency": "1小时"},
|
| 27 |
+
{"threshold": 70.0, "cooldown": 3 * 3600, "label": "70%+", "frequency": "3小时"},
|
| 28 |
+
{"threshold": 60.0, "cooldown": 6 * 3600, "label": "60%+", "frequency": "6小时"},
|
| 29 |
+
{"threshold": 50.0, "cooldown": 12 * 3600, "label": "50%+", "frequency": "12小时"},
|
| 30 |
+
{"threshold": 30.0, "cooldown": 24 * 3600, "label": "30%+", "frequency": "24小时"},
|
| 31 |
+
]
|
| 32 |
|
| 33 |
SUPPORTED_EXCHANGES = [
|
| 34 |
"binance",
|
|
|
|
| 231 |
return f"{value:+.2f}%"
|
| 232 |
|
| 233 |
|
| 234 |
+
def movement_alert_rule(change_pct):
|
| 235 |
+
magnitude = abs(num(change_pct, 0) or 0)
|
| 236 |
+
for rule in MOVEMENT_ALERT_RULES:
|
| 237 |
+
if magnitude >= rule["threshold"]:
|
| 238 |
+
return rule
|
| 239 |
+
return None
|
| 240 |
+
|
| 241 |
+
|
| 242 |
def market_type_label(value):
|
| 243 |
return "现货" if value == "spot" else "合约"
|
| 244 |
|
|
|
|
| 1199 |
self._failure_counts = {}
|
| 1200 |
self._health_alert_at = {}
|
| 1201 |
self.client = PublicMarketDataClient(self.log, self.config)
|
| 1202 |
+
self.pusher = legacy.WeWorkBotPusher(MARKET_MOVE_WEWORK_WEBHOOK)
|
| 1203 |
self._register_monitors()
|
| 1204 |
|
| 1205 |
def start(self):
|
|
|
|
| 1410 |
return candles[0].close
|
| 1411 |
|
| 1412 |
def _handle_snapshot_alerts(self, snapshot, cfg):
|
|
|
|
|
|
|
| 1413 |
state = self.store.get_kv(ALERT_STATE_KEY, {})
|
| 1414 |
sent = 0
|
| 1415 |
changed = False
|
|
|
|
| 1418 |
for window, change_info in snapshot.get("changes", {}).items():
|
| 1419 |
change_pct = change_info.get("change_pct")
|
| 1420 |
key = f"{snapshot['symbol']}:{window}"
|
| 1421 |
+
rule = movement_alert_rule(change_pct)
|
| 1422 |
+
if change_pct is None or rule is None:
|
| 1423 |
+
if key in state:
|
| 1424 |
+
state.pop(key, None)
|
| 1425 |
+
changed = True
|
| 1426 |
continue
|
| 1427 |
+
|
| 1428 |
+
direction = "涨" if change_pct > 0 else "跌"
|
| 1429 |
+
previous = state.get(key) or {}
|
| 1430 |
+
previous_threshold = float(
|
| 1431 |
+
previous.get("band_threshold")
|
| 1432 |
+
or (30.0 if previous.get("active") else 0.0)
|
| 1433 |
+
)
|
| 1434 |
+
last_alert_at = float(previous.get("last_alert_at") or 0)
|
| 1435 |
+
is_first_alert = last_alert_at <= 0
|
| 1436 |
+
direction_changed = previous.get("direction") and previous.get("direction") != direction
|
| 1437 |
+
escalated = rule["threshold"] > previous_threshold
|
| 1438 |
+
cooldown_elapsed = now - last_alert_at >= rule["cooldown"]
|
| 1439 |
+
should_send = is_first_alert or direction_changed or escalated or cooldown_elapsed
|
| 1440 |
+
|
| 1441 |
+
if should_send:
|
| 1442 |
+
delivered = self._send_move_alert(snapshot, window, change_info, direction, rule)
|
| 1443 |
+
sent += 1
|
| 1444 |
+
self.stats["alerts"] += 1
|
| 1445 |
+
if not delivered:
|
| 1446 |
+
self.stats["notify_fails"] += 1
|
| 1447 |
+
last_alert_at = now
|
| 1448 |
+
|
| 1449 |
+
state[key] = {
|
| 1450 |
+
"active": True,
|
| 1451 |
+
"direction": direction,
|
| 1452 |
+
"band_threshold": rule["threshold"],
|
| 1453 |
+
"band_label": rule["label"],
|
| 1454 |
+
"cooldown_seconds": rule["cooldown"],
|
| 1455 |
+
"change_pct": change_pct,
|
| 1456 |
+
"last_seen_at": now,
|
| 1457 |
+
"last_alert_at": last_alert_at,
|
| 1458 |
+
}
|
| 1459 |
+
changed = True
|
| 1460 |
if changed:
|
| 1461 |
self.store.set_kv(ALERT_STATE_KEY, state)
|
| 1462 |
return sent
|
| 1463 |
|
| 1464 |
+
def _send_move_alert(self, snapshot, window, change_info, direction, rule):
|
| 1465 |
+
title = f"{snapshot['symbol']} {change_info.get('label') or window}涨跌超过 {rule['threshold']:g}%"
|
| 1466 |
text = (
|
| 1467 |
+
"寻找牛市标的\n\n"
|
| 1468 |
f"币种:{snapshot.get('name')} ({snapshot.get('symbol')})\n"
|
| 1469 |
f"方向:{direction}\n"
|
| 1470 |
f"周期:{change_info.get('label') or window}\n"
|
| 1471 |
+
f"触发档位:{rule['label']}({rule['frequency']}内最多通知一次)\n"
|
| 1472 |
f"涨跌幅:{format_pct(change_info.get('change_pct'))}\n"
|
| 1473 |
f"当前价格:{fmt_price(snapshot.get('price'))}\n"
|
| 1474 |
f"参考价格:{fmt_price(change_info.get('reference_price'))}\n"
|
app/monitors/strategy_treasury.py
CHANGED
|
@@ -13,6 +13,10 @@ from app.monitors.base import BaseMonitor
|
|
| 13 |
SERVICE_ID = "strategy:service"
|
| 14 |
BTC_MONITOR_ID = "strategy:btc_holdings"
|
| 15 |
CASH_MONITOR_ID = "strategy:cash_reserve"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
def utc_now_iso():
|
|
@@ -248,7 +252,7 @@ class StrategyTreasuryMonitor(BaseMonitor):
|
|
| 248 |
self._stop_event = threading.Event()
|
| 249 |
self._thread = None
|
| 250 |
self.client = StrategyTreasuryClient(self.log)
|
| 251 |
-
self.pusher = legacy.WeWorkBotPusher(
|
| 252 |
self.stats = {"loops": 0, "alerts": 0, "notify_fails": 0, "api_fails": 0}
|
| 253 |
self._register_monitors()
|
| 254 |
|
|
|
|
| 13 |
SERVICE_ID = "strategy:service"
|
| 14 |
BTC_MONITOR_ID = "strategy:btc_holdings"
|
| 15 |
CASH_MONITOR_ID = "strategy:cash_reserve"
|
| 16 |
+
STRATEGY_TREASURY_WEWORK_WEBHOOK = os.getenv(
|
| 17 |
+
"STRATEGY_TREASURY_WEWORK_WEBHOOK",
|
| 18 |
+
"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=39502f69-f08e-43f8-afb2-6546ec722dfa",
|
| 19 |
+
)
|
| 20 |
|
| 21 |
|
| 22 |
def utc_now_iso():
|
|
|
|
| 252 |
self._stop_event = threading.Event()
|
| 253 |
self._thread = None
|
| 254 |
self.client = StrategyTreasuryClient(self.log)
|
| 255 |
+
self.pusher = legacy.WeWorkBotPusher(STRATEGY_TREASURY_WEWORK_WEBHOOK)
|
| 256 |
self.stats = {"loops": 0, "alerts": 0, "notify_fails": 0, "api_fails": 0}
|
| 257 |
self._register_monitors()
|
| 258 |
|