feat(openinsider): 新增 OpenInsider 內部人交易爬蟲,支援多標的與每日排程
- 新增 app/crawlers/openinsider.py,來源 http://openinsider.com/search?q={symbol} - 支援多標的:以 SYMBOLS=PLTR,NVDA,... 同時追多檔(或使用 SYMBOL 單一) - runner: 多實例排程與啟動;/check 會依序觸發全部爬蟲 - API: /info、/stats、/check、/notify_test 支援多爬蟲回應 - config/base: 新增 RUN_DAILY_AT 每日固定時間;未設定則用 CHECK_INTERVAL - notifications: 新增 send_custom_email、send_text_webhook、send_text_discord - README 與 .env.template 更新;.env 改為 CRAWLER_TYPE=openinsider - 移除 quiver_insiders 爬蟲與相關設定 BREAKING CHANGE: 不再支援 CRAWLER_TYPE=quiver_insiders;請改用 openinsider。
This commit is contained in:
@@ -8,6 +8,10 @@ from app.services import notifications as notif
|
||||
|
||||
def create_app(crawler) -> Flask:
|
||||
app = Flask(__name__)
|
||||
# Support single crawler or a list of crawlers
|
||||
crawlers = None
|
||||
if isinstance(crawler, (list, tuple)):
|
||||
crawlers = list(crawler)
|
||||
|
||||
@app.get('/health')
|
||||
def health():
|
||||
@@ -15,12 +19,47 @@ def create_app(crawler) -> Flask:
|
||||
|
||||
@app.get('/stats')
|
||||
def stats():
|
||||
if crawlers is not None:
|
||||
return jsonify({
|
||||
(getattr(c, 'symbol', getattr(c, 'name', f"crawler_{i}")) or f"crawler_{i}"):
|
||||
c.stats for i, c in enumerate(crawlers)
|
||||
})
|
||||
if crawler:
|
||||
return jsonify(crawler.stats)
|
||||
return jsonify({"error": "Crawler not initialized"}), 500
|
||||
|
||||
@app.get('/info')
|
||||
def info():
|
||||
if crawlers is not None:
|
||||
out = []
|
||||
for c in crawlers:
|
||||
out.append({
|
||||
"name": getattr(c, 'name', 'unknown'),
|
||||
"type": c.__class__.__name__,
|
||||
"symbol": getattr(c, 'symbol', None),
|
||||
"schedule": getattr(c.config, 'run_daily_at', None) or f"every {c.config.check_interval}s",
|
||||
})
|
||||
return jsonify(out)
|
||||
if not crawler:
|
||||
return jsonify({"error": "Crawler not initialized"}), 500
|
||||
return jsonify({
|
||||
"name": getattr(crawler, 'name', 'unknown'),
|
||||
"type": crawler.__class__.__name__,
|
||||
"symbol": getattr(crawler, 'symbol', None),
|
||||
"schedule": getattr(crawler.config, 'run_daily_at', None) or f"every {crawler.config.check_interval}s",
|
||||
})
|
||||
|
||||
@app.get('/check')
|
||||
def manual_check():
|
||||
if crawlers is not None:
|
||||
results = []
|
||||
for c in crawlers:
|
||||
r = c.run_check() or []
|
||||
results.append({
|
||||
"symbol": getattr(c, 'symbol', None),
|
||||
"new": len(r)
|
||||
})
|
||||
return jsonify({"results": results})
|
||||
if not crawler:
|
||||
return jsonify({"error": "Crawler not initialized"}), 500
|
||||
result = crawler.run_check() or []
|
||||
@@ -28,29 +67,49 @@ def create_app(crawler) -> Flask:
|
||||
|
||||
@app.get('/notify_test')
|
||||
def notify_test():
|
||||
channel = (request.args.get('channel') or 'email').lower()
|
||||
target = request.args.get('target')
|
||||
test_pick = [notif.build_test_pick()]
|
||||
|
||||
def _send_for(c):
|
||||
if channel == 'email':
|
||||
if not c.config.email:
|
||||
return {"error": "Email config not set"}
|
||||
notif.send_email(test_pick, c.config.email)
|
||||
elif channel == 'webhook':
|
||||
if not c.config.webhook_url:
|
||||
return {"error": "Webhook URL not set"}
|
||||
notif.send_webhook(test_pick, c.config.webhook_url)
|
||||
elif channel == 'discord':
|
||||
if not c.config.discord_webhook:
|
||||
return {"error": "Discord webhook not set"}
|
||||
notif.send_discord(test_pick, c.config.discord_webhook)
|
||||
else:
|
||||
return {"error": f"Unsupported channel: {channel}"}
|
||||
return {"result": f"Test notification sent via {channel}"}
|
||||
|
||||
if crawlers is not None:
|
||||
results = {}
|
||||
for c in crawlers:
|
||||
key = getattr(c, 'symbol', getattr(c, 'name', 'unknown'))
|
||||
if target and key != target:
|
||||
continue
|
||||
try:
|
||||
results[key] = _send_for(c)
|
||||
except Exception as e:
|
||||
c.logger.error(f"測試通知發送失敗({key}): {e}")
|
||||
results[key] = {"error": str(e)}
|
||||
return jsonify(results)
|
||||
|
||||
if not crawler:
|
||||
return jsonify({"error": "Crawler not initialized"}), 500
|
||||
channel = (request.args.get('channel') or 'email').lower()
|
||||
test_pick = [notif.build_test_pick()]
|
||||
try:
|
||||
if channel == 'email':
|
||||
if not crawler.config.email:
|
||||
return jsonify({"error": "Email config not set"}), 400
|
||||
notif.send_email(test_pick, crawler.config.email)
|
||||
elif channel == 'webhook':
|
||||
if not crawler.config.webhook_url:
|
||||
return jsonify({"error": "Webhook URL not set"}), 400
|
||||
notif.send_webhook(test_pick, crawler.config.webhook_url)
|
||||
elif channel == 'discord':
|
||||
if not crawler.config.discord_webhook:
|
||||
return jsonify({"error": "Discord webhook not set"}), 400
|
||||
notif.send_discord(test_pick, crawler.config.discord_webhook)
|
||||
else:
|
||||
return jsonify({"error": f"Unsupported channel: {channel}"}), 400
|
||||
return jsonify({"result": f"Test notification sent via {channel}"})
|
||||
res = _send_for(crawler)
|
||||
if 'error' in res:
|
||||
return jsonify(res), 400
|
||||
return jsonify(res)
|
||||
except Exception as e:
|
||||
crawler.logger.error(f"測試通知發送失敗: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
return app
|
||||
|
||||
|
Reference in New Issue
Block a user