# Signal Staleness Pipeline (v4.4.0, 2026-07-08) End-to-end flow that auto-filters TG trading signals older than 30 minutes. ## Why Signals arrive via TelegramForwarder in the "交易信号" group. The TG signal source itself does not embed a timestamp in the message body — it just publishes structured 【币种】【方向】... blocks. Without intervention, `process_signal.py` treats every forwarded message as fresh, even if the source posted it hours ago and the price has since moved 3%. ## The chain ``` [signal source @ TG] │ posts message at T₀ (real wall-clock time) ▼ [forwarder `is_original_time=1, time_template='⏱信号时间: {time}'`] │ reads `event.message.date` (UTC, tz-aware) │ appends "\n\n⏱信号时间: 2026-07-08 17:30:00" to forwarded text ▼ [process_signal.py `parse_signal()`] │ regex: ⏱信号时间[::]\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}) │ parses → fields['signal_time'] = datetime(...) ▼ [is_signal_stale() in process_signal.py] │ if (now − signal_time) >= 30 min → STALE ▼ [format_stale_message()] → push to QQ → return early ▲ │ (no advisor call, no execute, no signal_tracker write of consequence) ``` ## Configuration touch-points ### Forwarder DB (rebuilt from container DB) ```python import sqlite3 conn = sqlite3.connect('/home/openclaw/TelegramForwarder/db/forward.db') conn.execute(""" UPDATE forward_rules SET is_original_time = 1, time_template = '⏱信号时间: {time}' WHERE id IN (1, 2) """) conn.commit() ``` After updating, restart the forwarder container: ```bash docker restart telegram-forwarder ``` Verify the columns exist (they do in Heavrnl/TelegramForwarder's schema): ```python import sqlite3 conn = sqlite3.connect('/home/openclaw/TelegramForwarder/db/forward.db') cols = [r[1] for r in conn.execute('PRAGMA table_info(forward_rules)')] assert 'is_original_time' in cols and 'time_template' in cols ``` InfoFilter implementation that reads these columns lives at `/app/filters/info_filter.py` inside the container. ### process_signal.py ```python SIGNAL_FRESH_MINUTES = 30 # ≥30 算过期(少误跟) def is_signal_stale(fields): st = fields.get('signal_time') if not st: return False # 无时间戳的旧信号源按新鲜处理 return (datetime.now() - st).total_seconds() >= SIGNAL_FRESH_MINUTES * 60 ``` ## Edge cases observed | Case | Behavior | |---|---| | Signal has no `⏱信号时间` (older source, forwarded differently) | Treated as fresh — does NOT block. Intentional, so old sources remain routable. | | Timestamp at exactly 30 min mark | Stale (boundary = ≥, not >). Avoids race on the threshold. | | Forwarder not yet restarted after DB update | Forwarded messages lack the timestamp → goes through normally. | | System clock skew between forwarder host and agent | Drift shows up as age_delta. NTP drift is small enough that 30 min is robust. If drift ever causes false STALE → bump `SIGNAL_FRESH_MINUTES` to 45. | ## Rolling back ```sql UPDATE forward_rules SET is_original_time = 0; -- DB rollback ``` Then in `process_signal.py`, delete the stale-check branch and the `format_stale_message` helper. ## Testing without real signals ```bash python3 -c " from datetime import datetime, timedelta import sys; sys.path.insert(0, '$HOME/.hermes/skills/trading/okx-auto-position/scripts') from process_signal import parse_signal, is_signal_stale for m in [0, 29, 30, 45, 120]: t = (datetime.now() - timedelta(minutes=m)).strftime('%Y-%m-%d %H:%M:%S') f = parse_signal(f'【X】⏱信号时间: {t} 【币种】: BTC 【方向】: 做多') print(f'{m:>3}min → stale={is_signal_stale(f)}') " ``` Expected: `False False True True True`. ## Why this is not a cron/script-only concern The staleness check belongs to `process_signal.py` itself, NOT to a cron wrapper, because the agent (`/skill_name open-position` etc.) can also run `process_signal.py` directly via terminal — that path also needs the guard.