migrate: last 4 JSON files — live_prices, market, mtf_cache, capital_flow → DB
This commit is contained in:
+186
-182
@@ -1,182 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""capital_flow_collector.py — 个股资金流数据采集器
|
||||
|
||||
每30分钟拉一次持仓+自选的超大单/大单/中单/小单资金流向。
|
||||
输出到 capital_flow_cache.json 供 price_monitor 和报告使用。
|
||||
|
||||
API: push2his.eastmoney.com 个股资金流日线
|
||||
"""
|
||||
import json, os, sys, time, urllib.request
|
||||
from datetime import datetime
|
||||
from urllib.request import urlopen, Request
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from threading import Semaphore
|
||||
from mo_data import read_portfolio, read_decisions, read_watchlist
|
||||
|
||||
DATA_DIR = "/home/hmo/web-dashboard/data"
|
||||
DECISIONS_PATH = f"{DATA_DIR}/decisions.json"
|
||||
CACHE_PATH = f"{DATA_DIR}/capital_flow_cache.json"
|
||||
|
||||
UA = "Mozilla/5.0"
|
||||
# 限速器:最多5个并发,每请求后强制间隔0.3s
|
||||
RATE_LIMIT = Semaphore(5)
|
||||
MIN_INTERVAL = 0.3
|
||||
_last_req = 0
|
||||
|
||||
def _rate_limited_request(url):
|
||||
"""带速率限制的HTTP GET,用Semaphore控制并发数"""
|
||||
global _last_req
|
||||
with RATE_LIMIT:
|
||||
elapsed = time.time() - _last_req
|
||||
if elapsed < MIN_INTERVAL:
|
||||
time.sleep(MIN_INTERVAL - elapsed)
|
||||
proxy_handler = urllib.request.ProxyHandler({})
|
||||
opener = urllib.request.build_opener(proxy_handler)
|
||||
req = Request(url, headers={"User-Agent": UA, "Referer": "https://data.eastmoney.com/"})
|
||||
try:
|
||||
resp = opener.open(req, timeout=8)
|
||||
_last_req = time.time()
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# eastmoney secid: 1=上海 0=深圳
|
||||
def secid(code):
|
||||
code = str(code).strip()
|
||||
if code.startswith(("6", "9")):
|
||||
return f"1.{code}"
|
||||
return f"0.{code}"
|
||||
|
||||
def fetch_flow(code, days=5):
|
||||
"""拉取个股近N日资金流(带限速+代理绕过)"""
|
||||
sid = secid(code)
|
||||
url = f"http://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&lmt={days}"
|
||||
data = _rate_limited_request(url)
|
||||
if not data:
|
||||
return None
|
||||
klines = data.get("data", {}).get("klines", [])
|
||||
if not klines:
|
||||
return None
|
||||
result = []
|
||||
for k in klines:
|
||||
p = k.split(",")
|
||||
if len(p) >= 7:
|
||||
result.append({
|
||||
"date": p[0],
|
||||
"main_net": float(p[1]), # 主力净流入(元)
|
||||
"super_large": float(p[2]), # 超大单净流入(元)
|
||||
"large": float(p[3]), # 大单净流入(元)
|
||||
"medium": float(p[4]), # 中单净流入(元)
|
||||
"small": float(p[5]), # 小单净流入(元)
|
||||
})
|
||||
return result
|
||||
|
||||
def fetch_flow_intraday(code):
|
||||
"""拉取当日分时资金流(用于盘中判断)"""
|
||||
sid = secid(code)
|
||||
url = f"http://push2.eastmoney.com/api/qt/stock/fflow/kline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&klt=1&lmt=120"
|
||||
try:
|
||||
resp = urlopen(url, timeout=5)
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
klines = data.get("data", {}).get("klines", [])
|
||||
if not klines:
|
||||
return None
|
||||
latest = klines[-1].split(",")
|
||||
return {
|
||||
"main_net": float(latest[1]),
|
||||
"super_large": float(latest[2]),
|
||||
"large": float(latest[3]),
|
||||
}
|
||||
except:
|
||||
return None
|
||||
|
||||
def analyze_flow(flow_data):
|
||||
"""分析资金流模式"""
|
||||
if not flow_data or len(flow_data) < 2:
|
||||
return {}
|
||||
|
||||
result = {"alerts": [], "pattern": ""}
|
||||
|
||||
# 最近两日对比
|
||||
d1 = flow_data[-1] # 最新日
|
||||
d2 = flow_data[-2] # 前一日
|
||||
|
||||
# 超大单信号
|
||||
sl1 = d1["super_large"]
|
||||
sl2 = d2["super_large"]
|
||||
|
||||
# 连续形态判断
|
||||
main_trend = sum(d["main_net"] for d in flow_data[-3:])
|
||||
sl_trend = sum(d["super_large"] for d in flow_data[-3:])
|
||||
|
||||
# 1. 主力连续流入
|
||||
if main_trend > 50000000 and sl1 > 0 and sl2 > 0:
|
||||
result["pattern"] = "主力持续流入"
|
||||
result["alerts"].append("主力连续3日净流入")
|
||||
|
||||
# 2. 超大单突然转向(连续流入→流出 或 流出→流入)
|
||||
if sl1 * sl2 < 0: # 方向反转
|
||||
if sl1 > 0 and sl2 < 0:
|
||||
result["pattern"] = "超大单由出转入"
|
||||
result["alerts"].append("超大单转为净买入(暗示消息即将落地)")
|
||||
elif sl1 < 0 and sl2 > 0:
|
||||
result["pattern"] = "超大单由入转出"
|
||||
result["alerts"].append("超大单转为净卖出(利好出货嫌疑)")
|
||||
|
||||
# 3. 价格与资金流背离(缺当前价格作比较,在主脚本中完成)
|
||||
# 4. 单日暴量
|
||||
max_sl = max(abs(d["super_large"]) for d in flow_data)
|
||||
if max_sl == abs(sl1) and abs(sl1) > 100000000:
|
||||
result["pattern"] = "单日资金暴量"
|
||||
result["alerts"].append(f"今日超大单异常: {sl1/100000000:.2f}亿")
|
||||
|
||||
return result
|
||||
|
||||
def main():
|
||||
codes = set()
|
||||
# 读取持仓+自选
|
||||
try:
|
||||
dec = mo_data.read_decisions()
|
||||
for d in dec.get("decisions", []):
|
||||
c = d.get("code", "")
|
||||
if c:
|
||||
codes.add(c)
|
||||
except:
|
||||
pass
|
||||
|
||||
all_flows = {}
|
||||
|
||||
# 并行抓取:ThreadPoolExecutor + 内置限速器(Semaphore 5 + 0.3s间隔)
|
||||
code_list = sorted(codes)
|
||||
if not code_list:
|
||||
print("[capital_flow] 无代码需要采集")
|
||||
return
|
||||
|
||||
def fetch_one(code):
|
||||
flow = fetch_flow(code, days=5)
|
||||
if flow:
|
||||
analysis = analyze_flow(flow)
|
||||
return (code, {
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
"flow": flow,
|
||||
"analysis": analysis,
|
||||
})
|
||||
return (code, None)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=5) as pool:
|
||||
futures = {pool.submit(fetch_one, c): c for c in code_list}
|
||||
for f in as_completed(futures):
|
||||
code, result = f.result()
|
||||
if result:
|
||||
all_flows[code] = result
|
||||
|
||||
# 写缓存
|
||||
cache = {
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
"stocks": all_flows,
|
||||
}
|
||||
json.dump(cache, open(CACHE_PATH, "w"), indent=2, ensure_ascii=False)
|
||||
print(f"[capital_flow] {len(all_flows)}/{len(code_list)}只更新完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
#!/usr/bin/env python3
|
||||
"""capital_flow_collector.py — 个股资金流数据采集器
|
||||
|
||||
每30分钟拉一次持仓+自选的超大单/大单/中单/小单资金流向。
|
||||
输出到 capital_flow_cache.json 供 price_monitor 和报告使用。
|
||||
|
||||
API: push2his.eastmoney.com 个股资金流日线
|
||||
"""
|
||||
import json, os, sys, time, urllib.request
|
||||
from datetime import datetime
|
||||
from urllib.request import urlopen, Request
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from threading import Semaphore
|
||||
from mo_data import read_portfolio, read_decisions, read_watchlist
|
||||
from mofin_db import get_conn, write_capital_flow_cache
|
||||
|
||||
DATA_DIR = "/home/hmo/web-dashboard/data"
|
||||
DECISIONS_PATH = f"{DATA_DIR}/decisions.json"
|
||||
CACHE_PATH = f"{DATA_DIR}/capital_flow_cache.json"
|
||||
|
||||
UA = "Mozilla/5.0"
|
||||
# 限速器:最多5个并发,每请求后强制间隔0.3s
|
||||
RATE_LIMIT = Semaphore(5)
|
||||
MIN_INTERVAL = 0.3
|
||||
_last_req = 0
|
||||
|
||||
def _rate_limited_request(url):
|
||||
"""带速率限制的HTTP GET,用Semaphore控制并发数"""
|
||||
global _last_req
|
||||
with RATE_LIMIT:
|
||||
elapsed = time.time() - _last_req
|
||||
if elapsed < MIN_INTERVAL:
|
||||
time.sleep(MIN_INTERVAL - elapsed)
|
||||
proxy_handler = urllib.request.ProxyHandler({})
|
||||
opener = urllib.request.build_opener(proxy_handler)
|
||||
req = Request(url, headers={"User-Agent": UA, "Referer": "https://data.eastmoney.com/"})
|
||||
try:
|
||||
resp = opener.open(req, timeout=8)
|
||||
_last_req = time.time()
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# eastmoney secid: 1=上海 0=深圳
|
||||
def secid(code):
|
||||
code = str(code).strip()
|
||||
if code.startswith(("6", "9")):
|
||||
return f"1.{code}"
|
||||
return f"0.{code}"
|
||||
|
||||
def fetch_flow(code, days=5):
|
||||
"""拉取个股近N日资金流(带限速+代理绕过)"""
|
||||
sid = secid(code)
|
||||
url = f"http://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&lmt={days}"
|
||||
data = _rate_limited_request(url)
|
||||
if not data:
|
||||
return None
|
||||
klines = data.get("data", {}).get("klines", [])
|
||||
if not klines:
|
||||
return None
|
||||
result = []
|
||||
for k in klines:
|
||||
p = k.split(",")
|
||||
if len(p) >= 7:
|
||||
result.append({
|
||||
"date": p[0],
|
||||
"main_net": float(p[1]), # 主力净流入(元)
|
||||
"super_large": float(p[2]), # 超大单净流入(元)
|
||||
"large": float(p[3]), # 大单净流入(元)
|
||||
"medium": float(p[4]), # 中单净流入(元)
|
||||
"small": float(p[5]), # 小单净流入(元)
|
||||
})
|
||||
return result
|
||||
|
||||
def fetch_flow_intraday(code):
|
||||
"""拉取当日分时资金流(用于盘中判断)"""
|
||||
sid = secid(code)
|
||||
url = f"http://push2.eastmoney.com/api/qt/stock/fflow/kline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&klt=1&lmt=120"
|
||||
try:
|
||||
resp = urlopen(url, timeout=5)
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
klines = data.get("data", {}).get("klines", [])
|
||||
if not klines:
|
||||
return None
|
||||
latest = klines[-1].split(",")
|
||||
return {
|
||||
"main_net": float(latest[1]),
|
||||
"super_large": float(latest[2]),
|
||||
"large": float(latest[3]),
|
||||
}
|
||||
except:
|
||||
return None
|
||||
|
||||
def analyze_flow(flow_data):
|
||||
"""分析资金流模式"""
|
||||
if not flow_data or len(flow_data) < 2:
|
||||
return {}
|
||||
|
||||
result = {"alerts": [], "pattern": ""}
|
||||
|
||||
# 最近两日对比
|
||||
d1 = flow_data[-1] # 最新日
|
||||
d2 = flow_data[-2] # 前一日
|
||||
|
||||
# 超大单信号
|
||||
sl1 = d1["super_large"]
|
||||
sl2 = d2["super_large"]
|
||||
|
||||
# 连续形态判断
|
||||
main_trend = sum(d["main_net"] for d in flow_data[-3:])
|
||||
sl_trend = sum(d["super_large"] for d in flow_data[-3:])
|
||||
|
||||
# 1. 主力连续流入
|
||||
if main_trend > 50000000 and sl1 > 0 and sl2 > 0:
|
||||
result["pattern"] = "主力持续流入"
|
||||
result["alerts"].append("主力连续3日净流入")
|
||||
|
||||
# 2. 超大单突然转向(连续流入→流出 或 流出→流入)
|
||||
if sl1 * sl2 < 0: # 方向反转
|
||||
if sl1 > 0 and sl2 < 0:
|
||||
result["pattern"] = "超大单由出转入"
|
||||
result["alerts"].append("超大单转为净买入(暗示消息即将落地)")
|
||||
elif sl1 < 0 and sl2 > 0:
|
||||
result["pattern"] = "超大单由入转出"
|
||||
result["alerts"].append("超大单转为净卖出(利好出货嫌疑)")
|
||||
|
||||
# 3. 价格与资金流背离(缺当前价格作比较,在主脚本中完成)
|
||||
# 4. 单日暴量
|
||||
max_sl = max(abs(d["super_large"]) for d in flow_data)
|
||||
if max_sl == abs(sl1) and abs(sl1) > 100000000:
|
||||
result["pattern"] = "单日资金暴量"
|
||||
result["alerts"].append(f"今日超大单异常: {sl1/100000000:.2f}亿")
|
||||
|
||||
return result
|
||||
|
||||
def main():
|
||||
codes = set()
|
||||
# 读取持仓+自选
|
||||
try:
|
||||
dec = mo_data.read_decisions()
|
||||
for d in dec.get("decisions", []):
|
||||
c = d.get("code", "")
|
||||
if c:
|
||||
codes.add(c)
|
||||
except:
|
||||
pass
|
||||
|
||||
all_flows = {}
|
||||
|
||||
# 并行抓取:ThreadPoolExecutor + 内置限速器(Semaphore 5 + 0.3s间隔)
|
||||
code_list = sorted(codes)
|
||||
if not code_list:
|
||||
print("[capital_flow] 无代码需要采集")
|
||||
return
|
||||
|
||||
def fetch_one(code):
|
||||
flow = fetch_flow(code, days=5)
|
||||
if flow:
|
||||
analysis = analyze_flow(flow)
|
||||
return (code, {
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
"flow": flow,
|
||||
"analysis": analysis,
|
||||
})
|
||||
return (code, None)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=5) as pool:
|
||||
futures = {pool.submit(fetch_one, c): c for c in code_list}
|
||||
for f in as_completed(futures):
|
||||
code, result = f.result()
|
||||
if result:
|
||||
all_flows[code] = result
|
||||
|
||||
# 写缓存
|
||||
cache = {
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
"stocks": all_flows,
|
||||
}
|
||||
# 写 DB(替代 capital_flow_cache.json)
|
||||
conn = get_conn()
|
||||
write_capital_flow_cache(conn, cache)
|
||||
conn.close()
|
||||
print(f"[capital_flow] {len(all_flows)}/{len(code_list)}只更新完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import sys, traceback
|
||||
sys.path.insert(0, '/home/hmo/MoFin')
|
||||
|
||||
errors = []
|
||||
for mod, name in [
|
||||
('mo_data', 'read_portfolio'),
|
||||
('mo_data', 'read_decisions'),
|
||||
('mo_data', 'read_watchlist'),
|
||||
('mofin_db', 'get_conn'),
|
||||
('mofin_db', 'write_holdings_batch'),
|
||||
('mofin_db', 'write_portfolio_summary'),
|
||||
('mofin_db', 'write_watchlist_stock'),
|
||||
('mofin_db', 'write_holding_strategy'),
|
||||
('mo_models', 'is_hk_stock'),
|
||||
('mo_models', 'get_hk_rate'),
|
||||
]:
|
||||
try:
|
||||
m = __import__(mod, fromlist=[name])
|
||||
getattr(m, name)
|
||||
print(f"OK: {mod}.{name}")
|
||||
except Exception as e:
|
||||
print(f"FAIL: {mod}.{name} -> {e}")
|
||||
errors.append(str(e))
|
||||
|
||||
print(f"\n=== price_monitor.py import test ===")
|
||||
try:
|
||||
import price_monitor
|
||||
print("price_monitor imported OK")
|
||||
except Exception as e:
|
||||
print(f"FAIL: {traceback.format_exc()}")
|
||||
|
||||
if errors:
|
||||
print(f"\n{len(errors)} import errors!")
|
||||
@@ -0,0 +1,15 @@
|
||||
import sqlite3
|
||||
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
|
||||
|
||||
# Check when portfolio was last updated
|
||||
r = db.execute("SELECT updated_at, total_assets, total_mv, cash FROM portfolio_summary WHERE id=1").fetchone()
|
||||
print(f"Portfolio last updated: {r[0]}")
|
||||
print(f"total_assets={r[1]} total_mv={r[2]} cash={r[3]}")
|
||||
|
||||
# Check hold prices
|
||||
print("\nAll holdings:")
|
||||
for r in db.execute("SELECT code, name, price, change_pct, cost, shares FROM holdings WHERE is_active=1 ORDER BY code"):
|
||||
mv = (r[2] or 0) * (r[5] or 0)
|
||||
print(f" {r[0]} {r[1]}: price={r[2]} chg={r[3]} cost={r[4]} shares={r[5]} mv={mv}")
|
||||
|
||||
db.close()
|
||||
@@ -1,7 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove held stocks from watchlist"""
|
||||
|
||||
import json, os
|
||||
import json, os, sys
|
||||
|
||||
# 确保 MoFin 根目录在模块搜索路径中(兼容 cron 环境)
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from mo_data import read_portfolio, read_decisions, read_watchlist
|
||||
from mofin_db import get_conn, write_watchlist_stock, write_holding_strategy
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Diagnose: check HK stock costs, prices, total assets calculation"""
|
||||
import sys
|
||||
sys.path.insert(0, '/home/hmo/MoFin')
|
||||
from mo_models import calc_total_assets, calc_total_mv, is_hk_stock, to_cny, get_hk_rate
|
||||
from mo_data import read_portfolio
|
||||
import json
|
||||
|
||||
pf = read_portfolio()
|
||||
holdings = pf.get('holdings', [])
|
||||
rate = get_hk_rate()
|
||||
|
||||
print(f"HK_RATE: {rate}")
|
||||
print(f"total_assets (from DB): {pf.get('total_assets')}")
|
||||
print(f"total_mv (from DB): {pf.get('total_mv')}")
|
||||
print(f"cash: {pf.get('cash')}")
|
||||
print(f"frozen_cash: {pf.get('frozen_cash')}")
|
||||
print(f"position_pct: {pf.get('position_pct')}")
|
||||
|
||||
total_mv_calc = calc_total_mv(holdings)
|
||||
total_assets_calc = calc_total_assets(pf)
|
||||
print(f"\ncalc_total_mv: {total_mv_calc}")
|
||||
print(f"calc_total_assets: {total_assets_calc}")
|
||||
|
||||
print(f"\n=== HK stocks ===")
|
||||
for h in holdings:
|
||||
code = h.get('code', '')
|
||||
if is_hk_stock(str(code)):
|
||||
cost = h.get('cost', 0) or 0
|
||||
price = h.get('price', 0) or 0
|
||||
shares = h.get('shares', 0) or 0
|
||||
mv = price * shares
|
||||
cost_calc = cost * shares
|
||||
pnl = (price - cost) * shares if cost > 0 else 0
|
||||
pnl_pct = (price - cost) / cost * 100 if cost > 0 else 0
|
||||
print(f" {code} {h.get('name')}: cost={cost} price={price} shares={shares} mv={mv} pnl={pnl:.1f} ({pnl_pct:+.1f}%)")
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Quick verification after JSON→DB migration"""
|
||||
import sys
|
||||
sys.path.insert(0, '/home/hmo/MoFin')
|
||||
|
||||
from mo_data import read_portfolio, read_decisions, read_watchlist
|
||||
|
||||
ok = 0
|
||||
err = 0
|
||||
|
||||
for name, fn in [("portfolio", read_portfolio), ("decisions", read_decisions), ("watchlist", read_watchlist)]:
|
||||
try:
|
||||
r = fn()
|
||||
if name == "portfolio":
|
||||
n = len(r.get('holdings', []))
|
||||
elif name == "decisions":
|
||||
n = len(r.get('decisions', []))
|
||||
else:
|
||||
n = len(r.get('stocks', []))
|
||||
print(f" {name}: {n} records OK")
|
||||
ok += 1
|
||||
except Exception as e:
|
||||
print(f" {name}: ERROR -> {e}")
|
||||
err += 1
|
||||
|
||||
print(f"\n{ok}/3 passed, {err} errors")
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Test Eastmoney API response time for HK stocks"""
|
||||
import urllib.request, json, time
|
||||
|
||||
codes = ['00700', '01888', '00981']
|
||||
UA = 'Mozilla/5.0'
|
||||
|
||||
for code in codes:
|
||||
url = f"https://push2.eastmoney.com/api/qt/stock/get?secid=116.{code}&fields=f43,f170&fltt=2"
|
||||
start = time.time()
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
resp = json.loads(r.read().decode("utf-8"))
|
||||
elapsed = time.time() - start
|
||||
price = resp.get('data', {}).get('f43', '?')
|
||||
print(f"{code}: {elapsed:.1f}s, price={price}, rc={resp.get('rc')}")
|
||||
except Exception as e:
|
||||
elapsed = time.time() - start
|
||||
print(f"{code}: {elapsed:.1f}s, ERROR: {type(e).__name__}: {e}")
|
||||
|
||||
# Also test Tencent fallback
|
||||
print("\nTencent fallback:")
|
||||
url = "http://qt.gtimg.cn/q=hk00700,hk01888,hk00981"
|
||||
start = time.time()
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
text = r.read().decode("gbk")
|
||||
elapsed = time.time() - start
|
||||
print(f"Tencent: {elapsed:.1f}s, {len(text)} bytes")
|
||||
# Parse first line
|
||||
line = text.strip().split('\n')[0]
|
||||
print(f" sample: {line[:80]}...")
|
||||
except Exception as e:
|
||||
elapsed = time.time() - start
|
||||
print(f"Tencent: {elapsed:.1f}s, ERROR: {e}")
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Test if steady 5s interval avoids rate limit"""
|
||||
import urllib.request, json, time
|
||||
|
||||
UA = 'Mozilla/5.0'
|
||||
codes = ['00700', '01888', '00981']
|
||||
|
||||
for i, code in enumerate(codes):
|
||||
url = f"https://push2.eastmoney.com/api/qt/stock/get?secid=116.{code}&fields=f43,f170&fltt=2"
|
||||
start = time.time()
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
resp = json.loads(r.read().decode("utf-8"))
|
||||
elapsed = time.time() - start
|
||||
price = resp.get('data', {}).get('f43', '?')
|
||||
print(f"#{i+1} {code}: OK in {elapsed:.1f}s, price={price}")
|
||||
except Exception as e:
|
||||
elapsed = time.time() - start
|
||||
print(f"#{i+1} {code}: FAIL in {elapsed:.1f}s — {type(e).__name__}")
|
||||
break
|
||||
if i < len(codes) - 1:
|
||||
time.sleep(5)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Test Eastmoney rate limit — find safe interval between requests"""
|
||||
import urllib.request, json, time
|
||||
|
||||
UA = 'Mozilla/5.0'
|
||||
CODE = '00700'
|
||||
url = f"https://push2.eastmoney.com/api/qt/stock/get?secid=116.{CODE}&fields=f43,f170&fltt=2"
|
||||
|
||||
def try_fetch():
|
||||
start = time.time()
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
resp = json.loads(r.read().decode("utf-8"))
|
||||
elapsed = time.time() - start
|
||||
return True, elapsed, resp.get('data', {}).get('f43', '?'), ''
|
||||
except Exception as e:
|
||||
elapsed = time.time() - start
|
||||
return False, elapsed, 0, str(e)[:50]
|
||||
|
||||
# Test 1: single request
|
||||
ok, t, price, err = try_fetch()
|
||||
print(f"Single: {'OK' if ok else 'FAIL'} in {t:.2f}s price={price} {err}")
|
||||
|
||||
if ok:
|
||||
# If it works, test minimal interval
|
||||
for delay in [0.5, 1, 2, 3, 5]:
|
||||
time.sleep(delay)
|
||||
ok2, t2, p2, e2 = try_fetch()
|
||||
print(f"After {delay:.1f}s: {'OK' if ok2 else 'FAIL'} in {t2:.2f}s price={p2} {e2}")
|
||||
if not ok2:
|
||||
print(f" -> Rate limited! Need > {delay}s between requests")
|
||||
else:
|
||||
# Currently blocked, wait and retry
|
||||
print("Currently blocked. Waiting 10s then retry...")
|
||||
time.sleep(10)
|
||||
ok, t, price, err = try_fetch()
|
||||
print(f"After 10s: {'OK' if ok else 'FAIL'} in {t:.2f}s price={price} {err}")
|
||||
if ok:
|
||||
time.sleep(2)
|
||||
ok2, t2, p2, e2 = try_fetch()
|
||||
print(f"After +2s: {'OK' if ok2 else 'FAIL'} in {t2:.2f}s price={p2} {e2}")
|
||||
else:
|
||||
for w in [20, 30, 60]:
|
||||
print(f"Waiting {w}s...")
|
||||
time.sleep(w)
|
||||
ok, t, price, err = try_fetch()
|
||||
print(f"After {w}s: {'OK' if ok else 'FAIL'} in {t:.2f}s price={price} {err}")
|
||||
if ok:
|
||||
break
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Verify total assets and P&L calculation"""
|
||||
import sqlite3, sys
|
||||
sys.path.insert(0, '/home/hmo/MoFin')
|
||||
from mo_models import calc_total_assets, calc_total_mv
|
||||
|
||||
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
|
||||
r = db.execute("SELECT * FROM portfolio_summary WHERE id=1").fetchone()
|
||||
keys = [d[0] for d in db.execute("SELECT * FROM portfolio_summary LIMIT 0").description]
|
||||
summary = dict(zip(keys, r))
|
||||
|
||||
print(f"total_assets (stored): {summary.get('total_assets')}")
|
||||
print(f"total_mv (stored): {summary.get('total_mv')}")
|
||||
print(f"total_pnl (stored): {summary.get('total_pnl')}")
|
||||
print(f"cash: {summary.get('cash')}")
|
||||
print(f"frozen_cash: {summary.get('frozen_cash')}")
|
||||
|
||||
holdings = []
|
||||
for r in db.execute("SELECT code, name, cost, price, shares FROM holdings WHERE is_active=1"):
|
||||
holdings.append({'code': r[0], 'name': r[1], 'cost': r[2] or 0, 'price': r[3] or 0, 'shares': r[4] or 0})
|
||||
|
||||
mv = sum(h['price'] * h['shares'] for h in holdings)
|
||||
total_cost = sum(h['cost'] * h['shares'] for h in holdings)
|
||||
pnl = mv - total_cost
|
||||
ta = mv + (summary.get('cash') or 0) + (summary.get('frozen_cash') or 0)
|
||||
|
||||
print(f"\nCalculated:")
|
||||
print(f"total_mv = {mv:.2f}")
|
||||
print(f"total_cost = {total_cost:.2f}")
|
||||
print(f"total_pnl = {pnl:.2f}")
|
||||
print(f"total_assets = {ta:.2f}")
|
||||
|
||||
# Check for HK stocks with cost=0 (never converted)
|
||||
print("\nStocks with cost=0 or None:")
|
||||
for h in holdings:
|
||||
if h['cost'] <= 0 and h['shares'] > 0:
|
||||
print(f" {h['code']} {h['name']}: cost={h['cost']} shares={h['shares']} price={h['price']}")
|
||||
|
||||
db.close()
|
||||
@@ -13,6 +13,10 @@ no_agent模式:有发现→输出,无→静默
|
||||
import json, os, sqlite3, sys, time, urllib.request
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 确保 MoFin 根目录在模块搜索路径中(兼容 cron 环境)
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from mo_data import read_watchlist
|
||||
from mofin_db import write_watchlist_stock
|
||||
|
||||
@@ -34,8 +38,13 @@ def clean_proxy():
|
||||
def fetch_quote(code):
|
||||
"""拉行情。DB 优先,腾讯 fallback"""
|
||||
# DB 优先
|
||||
try: from mofin_db import get_price_from_db; p, chg = get_price_from_db(code); return {"name":"", "code":code, "price":p, "change_pct":chg or 0} if p else None
|
||||
except: pass
|
||||
try:
|
||||
from mofin_db import get_price_from_db
|
||||
p, chg = get_price_from_db(code)
|
||||
if p:
|
||||
return {"name":"", "code":code, "price":p, "change_pct":chg or 0}
|
||||
except:
|
||||
pass
|
||||
# Fallback: 腾讯
|
||||
try:
|
||||
prefix = "sh" if code.startswith(('60','68','51','56','50')) else "sz" if code.startswith(('00','30','15')) else "hk"
|
||||
|
||||
Reference in New Issue
Block a user