Files
Hermes-Skills/okx-auto-position/references/signal-staleness-pipeline.md
T

4.0 KiB
Raw Blame History

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)

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:

docker restart telegram-forwarder

Verify the columns exist (they do in Heavrnl/TelegramForwarder's schema):

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

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

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

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.