AI Agents I Built (3/7) — Stock Director: From Discord Signals to Portfolio Scoring

Full architecture of Jipyogyeon Discord channel auto-collection, signal parsing, and portfolio-based alert classification.


Summary

  • Built an automated pipeline to collect quant trading signals from Jipyogyeon Discord channels
  • Parses Discord embed messages into structured data, matches against portfolio holdings, and classifies alert priority
  • Integrates with the home server dashboard to surface critical/high alerts and deliver daily briefings as summary cards

Background

Subscribing to quant signal services generates a flood of data, but there is no layer that quickly filters "signals relevant to my portfolio." Manually checking signals posted to Discord channels does not scale.

Jipyogyeon distributes dozens of signals per day across multiple channels — entries, exits, PEG, momentum, Top5 reports. The requirement was to separate signals that match portfolio holdings into immediate alerts, while queuing the rest for a post-market summary.

Stock Director automates this collection → classification → delivery pipeline.


Architecture

1. Overall Pipeline

Discord Channels (Jipyogyeon)
    ↓ 60s polling
discord_listener.py → signal_parser.py
    ↓ structured data
Portfolio Matching → Alert Classification
    ↓
    ├─ Immediate Alert → Discord Webhook + Home Server /api/webhooks
    └─ Log Only       → saved to daily_raw.json
    ↓ (15:50 KST)
Daily Briefing → Home Server /api/webhooks

2. discord_listener.py — Channel Polling

Uses the Discord HTTP API directly — not a library like discord.py. Messages are fetched via HTTP GET requests.

Core design constraint: read-only. All write operations — sending messages, reactions, typing indicators — are explicitly excluded. This is an intentional restriction to prevent Discord ToS violations.

Polling behavior: - Full channel sweep every 60 seconds - 1–1.5 second random delay between channels (rate limit avoidance) - Last-seen message ID tracked per channel; only new messages are processed - Only today's messages (KST) are processed — prior-day messages are skipped

Trading hours filter: Active only on Korean market hours, 09:00–16:00 KST, weekdays only. Outside these hours the listener sleeps until the next trading session starts. Message state resets at the start of each new trading day.

Channel configuration: Managed via channels.json. Currently monitors 10 channels:

Channel Content
์˜ค๋Š˜์˜์‹œ๊ทธ๋„ Daily market summary
์…‹์—…ํ˜•์„ฑ VCP / setup detection
์ง„์ž… Standard / breakout / aggressive entry signals
๋ถ„ํ• ๋งค์ˆ˜ Pyramiding / scale-in signals
์ฒญ์‚ฐ Final / breakout exit signals
๋ถ„ํ• ๋งค๋„ Partial profit-taking / scale-out
๋ชจ๋ฉ˜ํ…€ Momentum BUY / SELL
peg PEG-based signals
์ข…ํ•ฉ์•Œ๋ฆผ_4h 4-hour composite alerts
top5 Top 5 leader stock report

3. signal_parser.py — Discord Embed Parsing

Jipyogyeon signals arrive as Discord Rich Embeds, not plain text.

Embed structure:

embed.title:       conviction + signal type (e.g. "๐ŸŸก B ๐Ÿ’ฐ Standard Entry")
embed.description: ticker + price / SL / RR
embed.fields:      status (score, EMA alignment), signal tags, AI evaluation, description, metadata (market env, exchange)

The parser converts this structure into a Signal dataclass:

Field Source Example
conviction title emoji ๐ŸŸฃ→S, ๐ŸŸข→A, ๐ŸŸก→B, ๐ŸŸ →C, ๐Ÿ”ด→D
signal_type title signal name "Standard Entry", "Final Exit"
action signal_type mapping BUY / SELL / CHECK
ticker description parse "090430"
price / stop_loss / risk_reward description parse 142600 / 122851 / 2.1
score fields["์ƒํƒœ"] 60
ema_alignment fields["์ƒํƒœ"] aligned / inverted / crossed
ai_summary fields["AI ํ‰๊ฐ€"] "Honeypot pullback, 21 EMA support"
exchange fields["์ •๋ณด"] "KRX" / "NASDAQ"

Top5 reports are handled by a dedicated parser (parse_top5_embed), extracting rank, conviction, ticker, sector, signal type, price, stop-loss, and target price.

4. Portfolio-Based Alert Classification

Parsed signals are matched against holdings and watchlist tickers in portfolio.json to determine alert priority.

Alert rules:

Priority Condition Alert
critical SELL signal on a held position Immediate alert (exit warning)
high Any signal on a held position Immediate alert (portfolio hit)
high S/A conviction + score ≥ 60 + BUY Immediate alert (high-conviction entry)
low All other signals Log only (no alert)

Only signals requiring immediate action are forwarded to the Discord webhook and home server. The rest are stored in daily_raw.json for use in the daily briefing.

Investment strategy reflected: Korean equities support short / medium / long-term positions. US equities support long-term only. Crypto is not handled.

5. Home Server Integration

Signal data is delivered to the home server via webhook API.

POST /api/webhooks
{
  "content": "{signal JSON}",
  "tags": ["stock", "signal", "buy", "kr", "alert"],
  "source": "stock-director"
}

Tags are auto-generated from signal attributes: market (kr/us), action (buy/sell/check), urgency (alert/urgent), conviction (high-conviction).

Daily briefing: At 15:50 KST — just before market close — all signals collected during the session are aggregated into a summary:

{
  "date": "2026-04-06",
  "total": 42,
  "buy_count": 18,
  "sell_count": 8,
  "check_count": 16,
  "portfolio_hits": [...],
  "high_conviction_entries": [...]
}

The home server dashboard renders this briefing as a summary card. Critical/high alerts are visually highlighted.

6. Always-On Execution

discord_listener.py runs as a persistent service managed by macOS launchd. It starts automatically on system boot and restarts on crash.

External drive TCC restriction: macOS applies TCC (Transparency, Consent, and Control) permission constraints to files on external drives. Running scripts directly from an external drive via launchd can trigger permission failures. The workaround: maintain an execution copy on internal storage, synchronized with the canonical source on the external drive.


Engineering Challenges

Discord embed parsing complexity: Jipyogyeon does not use a single message format. Standard signals, PEG signals, and Top5 reports each have distinct embed structures. A single unified parser failed. Splitting into per-type parsers resolved the issue.

Rate limit management: Sweeping 10 channels in rapid succession hits Discord API rate limits. Adding a 1–1.5 second random inter-channel delay resolved this. On 429 responses, the listener waits for the duration specified in retry_after.

Korean ticker name mapping: Jipyogyeon uses English company names (e.g. "SK Hynix Inc."). Portfolio alerts are more readable with Korean names (e.g. "SKํ•˜์ด๋‹‰์Šค"). Korean names were added to portfolio.json and referenced during alert generation.

Off-hours resource waste: The initial implementation polled 24/7. No signals are published outside trading hours, making those API calls unnecessary. Adding the trading hours filter restricted execution to weekdays 09:00–16:00 KST.


Conclusion

The core function of Stock Director is separating signal from noise. Dozens of signals arrive daily; only a few require immediate attention.

The structure is straightforward: collect (listener) → parse (parser) → classify → deliver (webhook). Each stage has a single responsibility.

In investment information management, volume is not the metric that matters. What matters is how quickly and accurately signals that affect your portfolio are identified. This pipeline automates that filtering layer.

Series overview: Series index

๋Œ“๊ธ€

์ด ๋ธ”๋กœ๊ทธ์˜ ์ธ๊ธฐ ๊ฒŒ์‹œ๋ฌผ

Agent Memory Engine (2/10) — Building an AI Agent Memory System with SQLite Alone

"ML Foundations (9/9) — PyTorch vs TensorFlow, and the Road to Local LLMs"

"RAG Core Study (14/26) — Evaluation Sets with RAGAS & DeepEval"

"ML Foundations (8/9) — Deep Learning Architectures: CNN, RNN, Attention"

"ML Foundations (7/9) — Deep Learning Training: Optimizers, Regularization, Initialization"

OpenClaw to Hermes Migration (2/13) — What to Preserve, Partially Port, or Discard

AI Agents I Built (5/7) — Building an Automated Blogger API Publishing System