feat: 首次提交,包含多因子状态机量化策略及 TG 机器人监听服务
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
|||||||
|
# ==========================================
|
||||||
|
# 🚨 核心核心:严禁上传的个人隐私与本地配置
|
||||||
|
# ==========================================
|
||||||
|
config.ini
|
||||||
|
*.conf
|
||||||
|
*.env
|
||||||
|
.env
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 📊 本地数据与状态文件(拒绝上传 SQLite 库和本地日志)
|
||||||
|
# ==========================================
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.sqlite3
|
||||||
|
*.log
|
||||||
|
quant_daily.log
|
||||||
|
bot_listener.log
|
||||||
|
*.csv
|
||||||
|
test.py
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 🐍 Python 环境与虚拟环境(只传代码,不传依赖包)
|
||||||
|
# ==========================================
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 💻 操作系统与 IDE 特有缓存
|
||||||
|
# ==========================================
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import configparser
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 0. 初始化基础环境与日志
|
||||||
|
# =====================================================================
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
log_filename = os.path.join(current_dir, "bot_listener.log")
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||||
|
handlers=[logging.FileHandler(log_filename, encoding='utf-8'), logging.StreamHandler(sys.stdout)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 读取 config.ini
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config_path = os.path.join(current_dir, "config.ini")
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
logging.error("❌ 错误:找不到 config.ini 配置文件!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
config.read(config_path, encoding='utf-8')
|
||||||
|
BOT_TOKEN = config.get("telegram", "bot_token")
|
||||||
|
CHAT_ID = int(config.get("telegram", "chat_id"))
|
||||||
|
DB_FILE = os.path.join(current_dir, config.get("database", "db_name", fallback="watchlist.db"))
|
||||||
|
|
||||||
|
# 👇 核心核心:读入代理配置并构建成 requests 专用的字典
|
||||||
|
PROXY_ENABLED = config.getint("proxy", "enabled", fallback=0)
|
||||||
|
PROXY_URL = config.get("proxy", "url", fallback="")
|
||||||
|
|
||||||
|
PROXIES = None
|
||||||
|
if PROXY_ENABLED and PROXY_URL:
|
||||||
|
PROXIES = {
|
||||||
|
"http": PROXY_URL,
|
||||||
|
"https": PROXY_URL
|
||||||
|
}
|
||||||
|
logging.info(f"🚀 机器人监听服务已成功挂载代理: {PROXY_URL}")
|
||||||
|
else:
|
||||||
|
logging.warning("⚠️ 机器人监听服务当前未启用代理,国内环境可能会连接超时。")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 1. 数据库管理指令封装
|
||||||
|
# =====================================================================
|
||||||
|
def db_add_stock(code, name):
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("INSERT OR REPLACE INTO watchlist (stock_code, stock_name, is_active) VALUES (?, ?, 1)",
|
||||||
|
(code, name))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return f"✅ 成功添加/激活股票:`{code}` ({name})"
|
||||||
|
|
||||||
|
|
||||||
|
def db_remove_stock(code):
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE watchlist SET is_active = 0 WHERE stock_code = ?", (code,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return f"❌ 成功将股票 `{code}` 移出自选监控"
|
||||||
|
|
||||||
|
|
||||||
|
def db_list_watchlist():
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT stock_code, stock_name FROM watchlist WHERE is_active = 1")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
if not rows:
|
||||||
|
return "📋 当前量化关注列表为空。\n使用 `/add 代码 名称` 添加。"
|
||||||
|
msg = "📋 *当前量化系统关注列表*:\n"
|
||||||
|
for row in rows:
|
||||||
|
msg += f"🔹 `{row[0]}` | {row[1]}\n"
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 2. 消息处理与分发
|
||||||
|
# =====================================================================
|
||||||
|
def send_reply(text):
|
||||||
|
"""发送回执消息(带代理)"""
|
||||||
|
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
||||||
|
try:
|
||||||
|
requests.post(
|
||||||
|
url,
|
||||||
|
json={"chat_id": CHAT_ID, "text": text, "parse_mode": "Markdown"},
|
||||||
|
proxies=PROXIES, # 👈 带上代理
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"发送回执失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_message(message):
|
||||||
|
from_id = message.get("from", {}).get("id")
|
||||||
|
# 安全风控
|
||||||
|
if from_id != CHAT_ID:
|
||||||
|
logging.warning(f"拒绝了未授权用户 {from_id} 的指令")
|
||||||
|
return
|
||||||
|
|
||||||
|
text = message.get("text", "").strip()
|
||||||
|
if not text.startswith("/"):
|
||||||
|
return
|
||||||
|
|
||||||
|
parts = text.split()
|
||||||
|
cmd = parts[0].lower()
|
||||||
|
|
||||||
|
if cmd == "/list":
|
||||||
|
reply = db_list_watchlist()
|
||||||
|
elif cmd == "/add":
|
||||||
|
if len(parts) < 3:
|
||||||
|
reply = "⚠️ 格式错误!请输入: `/add 代码 名称`\n例如: `/add 300750.SZ 宁德时代`"
|
||||||
|
else:
|
||||||
|
reply = db_add_stock(parts[1].upper(), parts[2])
|
||||||
|
elif cmd == "/del":
|
||||||
|
if len(parts) < 2:
|
||||||
|
reply = "⚠️ 格式错误!请输入: `/del 代码`\n例如: `/del 300750.SZ`"
|
||||||
|
else:
|
||||||
|
reply = db_remove_stock(parts[1].upper())
|
||||||
|
elif cmd in ["/start", "/help"]:
|
||||||
|
reply = "🤖 *量化做T小助手指令集*:\n\n" \
|
||||||
|
"🔍 查看列表:/list\n" \
|
||||||
|
"➕ 增加关注:`/add 代码 名称`\n" \
|
||||||
|
"➖ 移除关注:`/del 代码`"
|
||||||
|
else:
|
||||||
|
reply = "❓ 未知指令,输入 /help 查看帮助。"
|
||||||
|
|
||||||
|
send_reply(reply)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 3. 长轮询核心监听流
|
||||||
|
# =====================================================================
|
||||||
|
def main_loop():
|
||||||
|
logging.info("Telegram 监听守护进程进入主循环...")
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
url = f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates"
|
||||||
|
params = {"offset": offset, "timeout": 20}
|
||||||
|
|
||||||
|
# 👇 核心核心:获取消息的 get 请求也必须带上代理
|
||||||
|
response = requests.get(
|
||||||
|
url,
|
||||||
|
params=params,
|
||||||
|
proxies=PROXIES,
|
||||||
|
timeout=25
|
||||||
|
).json()
|
||||||
|
|
||||||
|
if response.get("ok") and response.get("result"):
|
||||||
|
for update in response["result"]:
|
||||||
|
offset = update["update_id"] + 1
|
||||||
|
if "message" in update:
|
||||||
|
handle_message(update["message"])
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"网络连接异常(可能代理挂了),5秒后重试: {e}")
|
||||||
|
time.sleep(5)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"监听进程捕获未知错误: {e}", exc_info=True)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main_loop()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[telegram]
|
||||||
|
# 你的 Telegram Bot Token (找 @BotFather 申请得到)
|
||||||
|
bot_token =
|
||||||
|
# 你的个人 Chat ID 或频道/群组 ID
|
||||||
|
chat_id =
|
||||||
|
|
||||||
|
[database]
|
||||||
|
# SQLite 数据库文件名
|
||||||
|
db_name = watchlist.db
|
||||||
|
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
# 🚨 开启代理开关:1 为启用,0 为关闭
|
||||||
|
enabled = 0
|
||||||
|
# 🚨 填入你 Linux 本地或局域网的代理软件端口(支持 http 或 socks5)
|
||||||
|
# 比如你本地运行了 Clash/V2Ray,通常 http 端口是 7890 或 10809
|
||||||
|
url = http://127.0.0.1:7890
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
import logging
|
||||||
|
import configparser
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from quant_strategy import QuantStateMachine
|
||||||
|
from util import get_history_k, get_5min_k
|
||||||
|
# =====================================================================
|
||||||
|
# 初始化基础环境与日志
|
||||||
|
# =====================================================================
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
log_filename = os.path.join(current_dir, "quant_daily.log")
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s',
|
||||||
|
handlers=[logging.FileHandler(log_filename, encoding='utf-8')])
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(os.path.join(current_dir, "config.ini"), encoding='utf-8')
|
||||||
|
BOT_TOKEN = config.get("telegram", "bot_token")
|
||||||
|
CHAT_ID = config.get("telegram", "chat_id")
|
||||||
|
DB_FILE = os.path.join(current_dir, config.get("database", "db_name", fallback="watchlist.db"))
|
||||||
|
|
||||||
|
PROXY_ENABLED = config.getint("proxy", "enabled", fallback=0)
|
||||||
|
PROXY_URL = config.get("proxy", "url", fallback="")
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 补全缺失的函数 1: 真实/模拟数据接口 (请在此替换为你实际的 adata 调用代码)
|
||||||
|
# =====================================================================
|
||||||
|
def fetch_data_from_adata(stock_code):
|
||||||
|
# 🚨 注意:这里是模拟数据,实际请使用你的真实 adata 接口替换
|
||||||
|
end_date = datetime.today().strftime('%Y%m%d')
|
||||||
|
start_date = (datetime.today() - timedelta(days=100)).strftime('%Y%m%d')
|
||||||
|
daily_df = get_history_k(str(stock_code), start_date=start_date, end_date=end_date)
|
||||||
|
min_df = get_5min_k(stock_code, start_date='20260703', end_date='20260703')
|
||||||
|
return daily_df, min_df
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 补全缺失的类 2: 针对 Telegram 消息推送优化的子类状态机
|
||||||
|
# =====================================================================
|
||||||
|
class TelegramQuantStateMachine(QuantStateMachine):
|
||||||
|
def generate_telegram_report(self, daily_df, min_df=None):
|
||||||
|
from quant_strategy import FeatureEngine, StateRuleBook
|
||||||
|
engine = FeatureEngine(daily_df, min_df)
|
||||||
|
f = engine.calculate_all_features()
|
||||||
|
|
||||||
|
next_state, comment = StateRuleBook.evaluate_next_state(self.current_state, f)
|
||||||
|
self.current_state = next_state
|
||||||
|
|
||||||
|
msg = f"📊 *【量化做T复盘报告】* \n"
|
||||||
|
msg += f"🤖 股票代码: `{self.stock_code}`\n"
|
||||||
|
msg += f"🕒 诊断时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
|
||||||
|
msg += f"📈 因子: `%B`={f['percent_b']:.2f} | 分时偏离={f['min_bias']:.2%} | 带宽分位={f['bw_quantile']:.2%}\n"
|
||||||
|
msg += f"🔍 状态: *{self.current_state}*\n"
|
||||||
|
msg += f"📝 诊断: _{comment}_\n"
|
||||||
|
msg += "-" * 30 + "\n"
|
||||||
|
msg += f"💡 *[明日手动操作指南]*:\n"
|
||||||
|
|
||||||
|
if self.current_state in ["STATE_1_OSCILLATION", "STATE_1_OSCILLATION_SQUEEZE"]:
|
||||||
|
if self.current_state == "STATE_1_OSCILLATION_SQUEEZE":
|
||||||
|
msg += "⚠️ *[变盘警告]*:弹簧已压紧,随时大突破,做T手速要快!\n"
|
||||||
|
if f['percent_b'] >= 1.0 or f['min_bias'] > 0.035:
|
||||||
|
msg += f"🟢 *【建议高抛】*:当前处于震荡高位(价格:{f['close']}),建议尾盘或明日开盘*手动卖出网格仓*!"
|
||||||
|
elif f['percent_b'] <= 0.0 or f['min_bias'] < -0.035:
|
||||||
|
msg += f"🔴 *【建议低吸】*:当前处于震荡超跌区(价格:{f['close']}),建议尾盘或明日开盘*手动买回筹码*!"
|
||||||
|
else:
|
||||||
|
msg += "⚪ *【建议观望】*:处于安全中枢内,未触及边界,明天*不要乱动*。"
|
||||||
|
elif self.current_state == "STATE_3_MAIN_WAVE":
|
||||||
|
msg += "🔥 *【强烈建议死守】*:科技股主升浪狂飙中!*禁止日内做T高抛*,锁仓死拿,享受主升浪最大利润!"
|
||||||
|
elif self.current_state == "STATE_4_WAVE_END":
|
||||||
|
msg += "⚠️ *【建议减仓】*:主升浪确认破位结束。建议手动*大举高抛/清空做T仓位*,落袋为安。"
|
||||||
|
elif self.current_state == "STATE_2_DOWN_BREAK":
|
||||||
|
msg += "🛑 *【严禁抄底】*:技术形态向下破位崩塌!明天*千万不要低吸接飞刀*,保持观望。"
|
||||||
|
elif self.current_state == "STATE_5_BOTTOMING":
|
||||||
|
msg += "🌱 *【建议潜伏】*:个股地量筑底阶段。不建议激进日内做T,但适合长线资金手动分批*定投埋伏*。"
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 补全缺失的函数 3: 数据库状态加载与固化
|
||||||
|
# =====================================================================
|
||||||
|
def load_saved_state(stock_code):
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT current_state FROM stock_states WHERE stock_code = ?", (stock_code,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return row[0] if row else "STATE_1_OSCILLATION"
|
||||||
|
|
||||||
|
|
||||||
|
def save_current_state(stock_code, state):
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("INSERT OR REPLACE INTO stock_states (stock_code, current_state, update_time) VALUES (?, ?, ?)",
|
||||||
|
(stock_code, state, datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 主运行入口
|
||||||
|
# =====================================================================
|
||||||
|
def main():
|
||||||
|
logging.info("量化定时任务触发...")
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
df = pd.read_sql_query("SELECT stock_code FROM watchlist WHERE is_active = 1", conn)
|
||||||
|
conn.close()
|
||||||
|
watchlist = df['stock_code'].tolist()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"读取数据库自选列表失败: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not watchlist:
|
||||||
|
logging.warning("当前没有激活的关注股票。")
|
||||||
|
return
|
||||||
|
|
||||||
|
for stock_code in watchlist:
|
||||||
|
try:
|
||||||
|
daily_df, min_df = fetch_data_from_adata(stock_code)
|
||||||
|
if daily_df.empty:
|
||||||
|
logging.warning(f"[{stock_code}] 数据为空,跳过")
|
||||||
|
continue
|
||||||
|
|
||||||
|
saved_state = load_saved_state(stock_code)
|
||||||
|
machine = TelegramQuantStateMachine(initial_state=saved_state)
|
||||||
|
machine.stock_code = stock_code
|
||||||
|
tg_report = machine.generate_telegram_report(daily_df, min_df)
|
||||||
|
|
||||||
|
# 推送大字报到 TG
|
||||||
|
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
||||||
|
proxies = None
|
||||||
|
if PROXY_ENABLED and PROXY_URL:
|
||||||
|
proxies = {
|
||||||
|
"http": PROXY_URL,
|
||||||
|
"https": PROXY_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
# 将 proxies 字典作为参数传入 requests
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
json={"chat_id": CHAT_ID, "text": tg_report, "parse_mode": "Markdown"},
|
||||||
|
proxies=proxies,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
save_current_state(stock_code, machine.current_state)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[{stock_code}] 运行时异常: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def init_database_safely():
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS watchlist (
|
||||||
|
stock_code TEXT PRIMARY KEY,
|
||||||
|
stock_name TEXT,
|
||||||
|
is_active INTEGER DEFAULT 1
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS stock_states (
|
||||||
|
stock_code TEXT PRIMARY KEY,
|
||||||
|
current_state TEXT,
|
||||||
|
update_time TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_database_safely() # 👈 核心:让 main.py 每次运行时也自己检查并建表
|
||||||
|
main()
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 1. 指标引擎:未来所有新发掘的指标,全写在这里
|
||||||
|
# =====================================================================
|
||||||
|
class FeatureEngine:
|
||||||
|
|
||||||
|
def __init__(self, daily_df, min5_df):
|
||||||
|
"""
|
||||||
|
daily_df: 日线数据 DataFrame
|
||||||
|
min5_df: 当天 5分钟 级别 K 线数据 DataFrame (需要包含 'close', 'volume')
|
||||||
|
"""
|
||||||
|
self.df = daily_df.copy()
|
||||||
|
self.min5_df = min5_df.copy()
|
||||||
|
self._extract_basic_arrays()
|
||||||
|
|
||||||
|
def _extract_basic_arrays(self):
|
||||||
|
self.close = self.df["close"].astype(float).values
|
||||||
|
self.high = self.df["high"].astype(float).values
|
||||||
|
self.low = self.df["low"].astype(float).values
|
||||||
|
self.amount = self.df["amount"].astype(float).values
|
||||||
|
|
||||||
|
def calculate_all_features(self):
|
||||||
|
features = {}
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 1. 🔄 【核心修改】:用 5分钟 K 线精确估算今日分时均价
|
||||||
|
# =====================================================================
|
||||||
|
if not self.min5_df.empty:
|
||||||
|
# 计算每 5 分钟的成交金额(收盘价 * 成交量)
|
||||||
|
# 注意:如果你的 5分钟接口直接自带 'amount'(成交额) 列,请直接用 self.min5_df['amount']
|
||||||
|
m5_close = self.min5_df["close"].astype(float)
|
||||||
|
m5_volume = self.min5_df["volume"].astype(float)
|
||||||
|
|
||||||
|
total_amount = (m5_close * m5_volume).sum()
|
||||||
|
total_volume = m5_volume.sum()
|
||||||
|
|
||||||
|
if total_volume > 0:
|
||||||
|
# 算出截止到当前(14:48)的 A 股全天分时均价
|
||||||
|
vwap_today = total_amount / total_volume
|
||||||
|
# 当前最新价格(最后一根 5分钟线的收盘价,最接近实时现价)
|
||||||
|
current_price = m5_close.iloc[-1]
|
||||||
|
|
||||||
|
# 计算偏离度:(最新价 - 分时均价) / 分时均价
|
||||||
|
features["min_bias"] = (current_price - vwap_today) / vwap_today
|
||||||
|
else:
|
||||||
|
features["min_bias"] = 0.0
|
||||||
|
else:
|
||||||
|
features["min_bias"] = 0.0
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 2. 日线常规指标计算(保持你的经典布林带逻辑不变)
|
||||||
|
# =====================================================================
|
||||||
|
up, mid, low = talib.BBANDS(
|
||||||
|
self.close, timeperiod=20, nbdevup=2, nbdevdn=2, matype=0
|
||||||
|
)
|
||||||
|
self.df["bb_up"], self.df["bb_mid"], self.df["bb_low"] = up, mid, low
|
||||||
|
|
||||||
|
self.df["percent_b"] = (self.df["close"] - low) / (up - low)
|
||||||
|
self.df["bandwidth"] = (up - low) / mid
|
||||||
|
self.df["amount_avg_20d"] = (
|
||||||
|
self.df["amount"].rolling(20).mean().shift(1)
|
||||||
|
)
|
||||||
|
self.df["price_max_60d"] = self.df["close"].rolling(60).max().shift(1)
|
||||||
|
self.df["low_min_10d"] = self.df["low"].rolling(10).min().shift(1)
|
||||||
|
|
||||||
|
latest = self.df.iloc[-1]
|
||||||
|
prev = self.df.iloc[-2]
|
||||||
|
|
||||||
|
# 如果 5分钟线数据拿到了,现价以 5分钟最新收盘价为准(更接近 14:48 真实盘面)
|
||||||
|
# 如果没拿到,退化使用日线昨日收盘(做测试用)
|
||||||
|
features["close"] = (
|
||||||
|
current_price if not self.min5_df.empty else latest["close"]
|
||||||
|
)
|
||||||
|
|
||||||
|
features["prev_close"] = prev["close"]
|
||||||
|
features["amount"] = latest["amount"]
|
||||||
|
features["amount_avg_20d"] = latest["amount_avg_20d"]
|
||||||
|
features["ma5"] = latest["ma5"] if "ma5" in latest else latest["close"]
|
||||||
|
features["ma10"] = latest["ma10"] if "ma10" in latest else latest["close"] # 👈 核心:补上这一行!
|
||||||
|
features["ma20"] = latest["bb_mid"]
|
||||||
|
features["ma60"] = latest["ma60"] if "ma60" in latest else latest["close"] # 👈 顺便把ma60也安全带上
|
||||||
|
features["percent_b"] = latest["percent_b"]
|
||||||
|
features["bandwidth"] = latest["bandwidth"]
|
||||||
|
features["is_price_60d_max"] = (
|
||||||
|
latest["close"] >= latest["price_max_60d"]
|
||||||
|
)
|
||||||
|
features["is_not_new_low_10d"] = latest["close"] > latest["low_min_10d"]
|
||||||
|
|
||||||
|
history_bw = self.df["bandwidth"].iloc[-250:]
|
||||||
|
features["bw_quantile"] = (history_bw < latest["bandwidth"]).mean()
|
||||||
|
|
||||||
|
return features
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 2. 状态规则书:这里只根据指标数据定义状态切换门槛
|
||||||
|
# =====================================================================
|
||||||
|
class StateRuleBook:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def evaluate_next_state(current_state, f):
|
||||||
|
"""f 传入的是 FeatureEngine 计算出来的最新特征字典"""
|
||||||
|
|
||||||
|
# 计算主升浪基础多头条件
|
||||||
|
is_ma_bull = (f["ma5"] > f["ma10"] > f["ma20"]) and (
|
||||||
|
f["ma5"] > f["prev_ma5"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 核心决策流:利用解耦后的字典 f 进行条件拆解
|
||||||
|
# 【判断是否从主升浪跌破】
|
||||||
|
if (
|
||||||
|
current_state == "STATE_3_MAIN_WAVE"
|
||||||
|
and f["close"] < f["ma10"]
|
||||||
|
and f["prev_close"] > f["prev_ma10"]
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
"STATE_4_WAVE_END",
|
||||||
|
"⚠️ 主升浪确认结束!清空做T仓,准备重新激活做T。",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 【判断是否爆发主升浪】
|
||||||
|
if (
|
||||||
|
is_ma_bull
|
||||||
|
and f["is_price_60d_max"]
|
||||||
|
and (f["amount"] > f["amount_avg_20d"] * 1.8)
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
"STATE_3_MAIN_WAVE",
|
||||||
|
"🚀 主升浪开启 / 放量向上突破!做T脚本自动拉闸休眠,锁仓死拿!",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 【判断是否向下破位】
|
||||||
|
if f["percent_b"] < 0.0: # 跌破布林下轨
|
||||||
|
return "STATE_2_DOWN_BREAK", "❌ 向下破位!停止低吸做T,转为观望。"
|
||||||
|
|
||||||
|
# 【判断是否低量筑底】
|
||||||
|
is_low_volume = f["amount"] < (f["amount_avg_20d"] * 0.5)
|
||||||
|
is_ma_converge = abs(f["ma5"] - f["ma20"]) / f["ma20"] < 0.03
|
||||||
|
if f["is_not_new_low_10d"] and is_low_volume and is_ma_converge:
|
||||||
|
return (
|
||||||
|
"STATE_5_BOTTOMING",
|
||||||
|
"🌱 下跌结束,正在地量筑底。允许开始尝试轻仓做T。",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 【判断是否处于变盘前夜】
|
||||||
|
if f["bw_quantile"] < 0.12:
|
||||||
|
return (
|
||||||
|
"STATE_1_OSCILLATION_SQUEEZE",
|
||||||
|
"🎚️ 变盘前夜:带宽极度压缩。保持做T,但防范单边突破。",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 默认返回横盘震荡
|
||||||
|
return (
|
||||||
|
"STATE_1_OSCILLATION",
|
||||||
|
"☕ 正常横盘震荡期。激活做T脚本,正常执行高抛低吸。",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 3. 策略状态机:调度核心
|
||||||
|
# =====================================================================
|
||||||
|
class QuantStateMachine:
|
||||||
|
|
||||||
|
def __init__(self, initial_state="STATE_1_OSCILLATION"):
|
||||||
|
self.current_state = initial_state
|
||||||
|
|
||||||
|
def run_daily_diagnostic(self, daily_df, min_df=None):
|
||||||
|
# 1. 扔给传感器计算指标
|
||||||
|
engine = FeatureEngine(daily_df, min_df)
|
||||||
|
features = engine.calculate_all_features()
|
||||||
|
|
||||||
|
# 2. 扔给规则书判定状态
|
||||||
|
next_state, comment = StateRuleBook.evaluate_next_state(
|
||||||
|
self.current_state, features
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 更新并固化状态
|
||||||
|
self.current_state = next_state
|
||||||
|
|
||||||
|
# 4. 根据最终状态,分发当天的交易指令
|
||||||
|
self._execute_trading_action(features, comment)
|
||||||
|
|
||||||
|
def _execute_trading_action(self, f, comment):
|
||||||
|
print(f"\n[当前系统状态]: {self.current_state}")
|
||||||
|
print(f"[状态诊断提示]: {comment}")
|
||||||
|
|
||||||
|
# 具体的交易动作分发
|
||||||
|
if self.current_state in [
|
||||||
|
"STATE_1_OSCILLATION",
|
||||||
|
"STATE_1_OSCILLATION_SQUEEZE",
|
||||||
|
]:
|
||||||
|
# 只有在震荡期,才读取 %B 或分时执行做T
|
||||||
|
if f["percent_b"] >= 1.0 or f["min_bias"] > 0.035:
|
||||||
|
print(">>> 💰 【执行动作】:尾盘高抛,卖出 20% 网格仓。")
|
||||||
|
elif f["percent_b"] <= 0.0 or f["min_bias"] < -0.035:
|
||||||
|
print(">>> 🛒 【执行动作】:尾盘低吸,接回 20% 网格仓。")
|
||||||
|
else:
|
||||||
|
print(">>> ☕ 【执行动作】:未触及极端做T边界,长线持股观望。")
|
||||||
|
|
||||||
|
elif self.current_state == "STATE_3_MAIN_WAVE":
|
||||||
|
print(">>> 🔒 【执行动作】:主升浪锁仓护航中,禁止任何人乱动做T筹码。")
|
||||||
|
|
||||||
|
elif self.current_state == "STATE_2_DOWN_BREAK":
|
||||||
|
print(">>> 🛑 【执行动作】:市场向下破位,做T有被套风险,禁止低吸!")
|
||||||
|
|
||||||
|
elif self.current_state == "STATE_5_BOTTOMING":
|
||||||
|
print(">>> 🔬 【执行动作】:地量筑底阶段,允许小仓位底部分批低吸。")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
adata==2.9.5
|
||||||
|
akracer==0.0.14
|
||||||
|
akshare==1.18.64
|
||||||
|
beautifulsoup4==4.15.0
|
||||||
|
build==1.5.0
|
||||||
|
certifi==2026.6.17
|
||||||
|
cffi==2.0.0
|
||||||
|
charset-normalizer==3.4.7
|
||||||
|
curl_cffi==0.15.0
|
||||||
|
decorator==5.3.1
|
||||||
|
et_xmlfile==2.0.0
|
||||||
|
html5lib==1.1
|
||||||
|
idna==3.18
|
||||||
|
jsonpath==0.82.2
|
||||||
|
lxml==6.1.1
|
||||||
|
markdown-it-py==4.2.0
|
||||||
|
mdurl==0.1.2
|
||||||
|
numpy==2.2.6
|
||||||
|
openpyxl==3.1.5
|
||||||
|
packaging==26.2
|
||||||
|
pandas==2.3.3
|
||||||
|
py-mini-racer==0.6.0
|
||||||
|
pycparser==3.0
|
||||||
|
Pygments==2.20.0
|
||||||
|
pyproject_hooks==1.2.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
pytz==2026.2
|
||||||
|
requests==2.34.2
|
||||||
|
rich==15.0.0
|
||||||
|
six==1.17.0
|
||||||
|
soupsieve==2.8.4
|
||||||
|
TA-Lib==0.7.0
|
||||||
|
tabulate==0.10.0
|
||||||
|
tomli==2.4.1
|
||||||
|
tqdm==4.68.3
|
||||||
|
typing_extensions==4.16.0
|
||||||
|
tzdata==2026.2
|
||||||
|
urllib3==2.7.0
|
||||||
|
webencodings==0.5.1
|
||||||
|
xlrd==2.0.2
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import requests
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
CN_CODE_TYPE = {
|
||||||
|
"600":'sh',
|
||||||
|
"601":'sh',
|
||||||
|
"603":'sh',
|
||||||
|
"605":'sh',
|
||||||
|
"000":'sz',
|
||||||
|
"300":'sz',
|
||||||
|
"688":'sh',
|
||||||
|
"002":'sz',
|
||||||
|
"50":"sh",
|
||||||
|
"51":"sh",
|
||||||
|
"52":"sh",
|
||||||
|
"16":"sz",
|
||||||
|
"15":"sz",
|
||||||
|
"18":"sz"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_history_k(code, start_date='19700101', end_date=None):
|
||||||
|
|
||||||
|
if '.' in code:
|
||||||
|
code = code.split('.')[0]
|
||||||
|
|
||||||
|
stock_code = ''
|
||||||
|
|
||||||
|
if code[0:2] in CN_CODE_TYPE:
|
||||||
|
stock_code = code + '.' + CN_CODE_TYPE[code[0:2]]
|
||||||
|
elif code[0:3] in CN_CODE_TYPE:
|
||||||
|
stock_code = code + '.' + CN_CODE_TYPE[code[0:3]]
|
||||||
|
print(stock_code)
|
||||||
|
if stock_code != '':
|
||||||
|
|
||||||
|
url = "https://api.zhituapi.com/hs/history/%s/d/f?token=6E0E86BC-15AD-4275-8A95-B02D168D63C1&st=%s"%(stock_code, start_date)
|
||||||
|
|
||||||
|
if end_date is not None:
|
||||||
|
url += "&end_date=%s"%(end_date)
|
||||||
|
|
||||||
|
response = requests.get(url)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
print(data)
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
|
||||||
|
df['t'] = df['t'].apply(pd.to_datetime)
|
||||||
|
df.rename({'t': 'date', 'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 'v': 'volume', 'a': 'amount'},
|
||||||
|
axis='columns', inplace=True)
|
||||||
|
df = df[['date', 'open', 'high', 'low', 'close', 'volume', 'amount']].set_index('date')
|
||||||
|
|
||||||
|
else:
|
||||||
|
df = pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def get_5min_k(code, start_date='19700101', end_date=None):
|
||||||
|
stock_code = ''
|
||||||
|
|
||||||
|
if code[0:2] in CN_CODE_TYPE:
|
||||||
|
stock_code = code + '.' + CN_CODE_TYPE[code[0:2]]
|
||||||
|
elif code[0:3] in CN_CODE_TYPE:
|
||||||
|
stock_code = code + '.' + CN_CODE_TYPE[code[0:3]]
|
||||||
|
|
||||||
|
if stock_code != '':
|
||||||
|
|
||||||
|
url = "https://api.zhituapi.com/hs/history/%s/5/n?token=6E0E86BC-15AD-4275-8A95-B02D168D63C1&st=%s"%(stock_code, start_date)
|
||||||
|
|
||||||
|
if end_date is not None:
|
||||||
|
url += "&end_date=%s"%(end_date)
|
||||||
|
|
||||||
|
response = requests.get(url)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
df['t'] = df['t'].apply(pd.to_datetime)
|
||||||
|
df.rename({'t': 'date', 'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 'v': 'volume', 'a': 'amount'},
|
||||||
|
axis='columns', inplace=True)
|
||||||
|
df = df[['date','close', 'volume']].set_index('date')
|
||||||
|
|
||||||
|
else:
|
||||||
|
df = pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
|
return df
|
||||||
Reference in New Issue
Block a user