quant_system.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. #!/usr/bin/env python3
  2. """
  3. 量化交易系统 - 100万资金管理
  4. 策略:可转债双低 + 小市值动量 + 高股息防御
  5. 作者:Kimi Claw
  6. 日期:2026-03-07
  7. """
  8. import pandas as pd
  9. import numpy as np
  10. import akshare as ak
  11. from datetime import datetime, timedelta
  12. import json
  13. import logging
  14. import os
  15. from typing import List, Dict, Tuple
  16. import warnings
  17. warnings.filterwarnings('ignore')
  18. # 配置日志
  19. logging.basicConfig(
  20. level=logging.INFO,
  21. format='%(asctime)s - %(levelname)s - %(message)s',
  22. handlers=[
  23. logging.FileHandler('quant_trade.log', encoding='utf-8'),
  24. logging.StreamHandler()
  25. ]
  26. )
  27. logger = logging.getLogger(__name__)
  28. class PortfolioManager:
  29. """组合管理器 - 总资金100万分配"""
  30. def __init__(self, total_capital: float = 1000000):
  31. self.total_capital = total_capital
  32. self.allocations = {
  33. 'convertible_bond': 0.40, # 可转债40万
  34. 'small_cap_momentum': 0.30, # 小市值动量30万
  35. 'high_dividend': 0.20, # 高股息20万
  36. 'cash': 0.10 # 现金10万
  37. }
  38. self.positions = {} # 当前持仓
  39. self.trade_history = [] # 交易记录
  40. def get_strategy_capital(self, strategy_name: str) -> float:
  41. """获取指定策略的资金额度"""
  42. return self.total_capital * self.allocations.get(strategy_name, 0)
  43. def record_trade(self, strategy: str, action: str, code: str,
  44. name: str, price: float, shares: int, reason: str = ""):
  45. """记录交易"""
  46. trade = {
  47. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  48. 'strategy': strategy,
  49. 'action': action, # BUY/SELL
  50. 'code': code,
  51. 'name': name,
  52. 'price': price,
  53. 'shares': shares,
  54. 'amount': price * shares,
  55. 'reason': reason
  56. }
  57. self.trade_history.append(trade)
  58. logger.info(f"[交易记录] {action} {name}({code}): {shares}股 @ {price:.2f}")
  59. def save_state(self):
  60. """保存组合状态"""
  61. state = {
  62. 'total_capital': self.total_capital,
  63. 'allocations': self.allocations,
  64. 'positions': self.positions,
  65. 'trade_history': self.trade_history,
  66. 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  67. }
  68. with open('portfolio_state.json', 'w', encoding='utf-8') as f:
  69. json.dump(state, f, ensure_ascii=False, indent=2)
  70. logger.info("组合状态已保存")
  71. class ConvertibleBondStrategy:
  72. """可转债双低策略 - 40万资金"""
  73. def __init__(self, capital: float = 400000):
  74. self.capital = capital
  75. self.position_limit = 20 # 最多持有20只
  76. self.single_limit = capital / self.position_limit # 每只2万
  77. self.stop_loss = -0.08 # 8%止损
  78. self.take_profit = 0.15 # 15%止盈
  79. def get_candidates(self) -> pd.DataFrame:
  80. """获取候选可转债"""
  81. try:
  82. # 获取可转债数据
  83. df = ak.bond_cb_jsl(cookie="")
  84. # 筛选条件
  85. df['bond_price'] = pd.to_numeric(df['价格'], errors='coerce')
  86. df['premium_rate'] = pd.to_numeric(df['溢价率'].str.replace('%', ''), errors='coerce')
  87. df['remain_year'] = pd.to_numeric(df['剩余年限'], errors='coerce')
  88. # 双低分数 = 价格 + 溢价率
  89. df['double_low_score'] = df['bond_price'] + df['premium_rate']
  90. # 过滤条件
  91. mask = (
  92. (df['bond_price'] < 115) & # 价格低于115
  93. (df['premium_rate'] < 30) & # 溢价率低于30%
  94. (df['remain_year'] > 1) & # 剩余期限大于1年
  95. (df['评级'].isin(['AAA', 'AA+', 'AA', 'AA-'])) # 评级
  96. )
  97. candidates = df[mask].copy()
  98. candidates = candidates.nsmallest(30, 'double_low_score')
  99. logger.info(f"可转债候选池: {len(candidates)}只")
  100. return candidates[['代码', '名称', 'bond_price', 'premium_rate',
  101. 'double_low_score', '转股价值', '剩余规模']]
  102. except Exception as e:
  103. logger.error(f"获取可转债数据失败: {e}")
  104. return pd.DataFrame()
  105. def generate_signals(self, current_positions: List[str]) -> Tuple[List[Dict], List[str]]:
  106. """
  107. 生成交易信号
  108. Returns: (buy_list, sell_list)
  109. """
  110. candidates = self.get_candidates()
  111. if candidates.empty:
  112. return [], []
  113. buy_list = []
  114. sell_list = []
  115. # 目标持仓(前20只)
  116. target_codes = candidates.head(self.position_limit)['代码'].tolist()
  117. # 需要卖出的:不在目标列表中的当前持仓
  118. for code in current_positions:
  119. if code not in target_codes:
  120. sell_list.append(code)
  121. # 需要买入的:目标列表中不在当前持仓的
  122. for _, row in candidates.head(self.position_limit).iterrows():
  123. if row['代码'] not in current_positions:
  124. buy_list.append({
  125. 'code': row['代码'],
  126. 'name': row['名称'],
  127. 'price': row['bond_price'],
  128. 'amount': self.single_limit
  129. })
  130. return buy_list, sell_list
  131. class SmallCapMomentumStrategy:
  132. """小市值动量策略 - 30万资金"""
  133. def __init__(self, capital: float = 300000):
  134. self.capital = capital
  135. self.position_limit = 10 # 最多持有10只
  136. self.single_limit = capital / self.position_limit # 每只3万
  137. self.stop_loss = -0.08 # 8%止损
  138. self.market_filter = True # 启用市场过滤器
  139. def get_all_stocks(self) -> pd.DataFrame:
  140. """获取全市场股票列表"""
  141. try:
  142. df = ak.stock_zh_a_spot_em()
  143. df['market_cap'] = df['总市值'].astype(float) / 1e8 # 转为亿
  144. df['turnover'] = df['成交额'].astype(float) / 1e4 # 转为万
  145. df['change_20d'] = df['20日涨跌幅'].astype(float)
  146. # 过滤条件
  147. mask = (
  148. (df['market_cap'] < 50) & # 市值小于50亿
  149. (df['turnover'] > 1000) & # 成交额大于1000万
  150. (~df['名称'].str.contains('ST|退', na=False)) & # 排除ST和退市
  151. (~df['代码'].str.startswith('68')) & # 排除科创板
  152. (~df['代码'].str.startswith('8')) & # 排除北交所
  153. (~df['代码'].str.startswith('4')) & # 排除新三板
  154. (df['change_20d'] > -20) # 排除暴跌股
  155. )
  156. return df[mask].copy()
  157. except Exception as e:
  158. logger.error(f"获取股票数据失败: {e}")
  159. return pd.DataFrame()
  160. def market_trend_ok(self) -> bool:
  161. """检查市场趋势 - 中证1000是否站上20日均线"""
  162. try:
  163. # 获取中证1000指数
  164. df = ak.index_zh_a_hist(symbol="000852", period="daily")
  165. if len(df) < 30:
  166. return True # 数据不足,默认允许交易
  167. df['ma20'] = df['close'].rolling(20).mean()
  168. latest_close = df['close'].iloc[-1]
  169. latest_ma20 = df['ma20'].iloc[-1]
  170. is_bullish = latest_close > latest_ma20
  171. logger.info(f"中证1000: 收盘价{latest_close:.2f}, MA20:{latest_ma20:.2f}, 趋势:{'多头' if is_bullish else '空头'}")
  172. return is_bullish
  173. except Exception as e:
  174. logger.error(f"获取指数数据失败: {e}")
  175. return True
  176. def get_candidates(self) -> pd.DataFrame:
  177. """获取候选股票(20日涨幅排名)"""
  178. df = self.get_all_stocks()
  179. if df.empty:
  180. return df
  181. # 按20日涨幅排序,取前200
  182. df = df.nlargest(200, 'change_20d')
  183. logger.info(f"小市值动量候选池: {len(df)}只")
  184. return df[['代码', '名称', 'market_cap', 'change_20d', 'turnover', '最新价']]
  185. def generate_signals(self, current_positions: List[str]) -> Tuple[List[Dict], List[str], bool]:
  186. """
  187. 生成交易信号
  188. Returns: (buy_list, sell_list, market_ok)
  189. """
  190. market_ok = self.market_trend_ok()
  191. if not market_ok:
  192. logger.warning("市场趋势不佳,建议减仓或清仓")
  193. # 返回空买入列表,卖出所有持仓
  194. return [], current_positions, False
  195. candidates = self.get_candidates()
  196. if candidates.empty:
  197. return [], [], True
  198. buy_list = []
  199. sell_list = []
  200. # 目标持仓(前10只)
  201. target_codes = candidates.head(self.position_limit)['代码'].tolist()
  202. # 需要卖出的
  203. for code in current_positions:
  204. if code not in target_codes:
  205. sell_list.append(code)
  206. # 需要买入的
  207. for _, row in candidates.head(self.position_limit).iterrows():
  208. if row['代码'] not in current_positions:
  209. buy_list.append({
  210. 'code': row['代码'],
  211. 'name': row['名称'],
  212. 'price': row['最新价'],
  213. 'amount': self.single_limit
  214. })
  215. return buy_list, sell_list, True
  216. class HighDividendStrategy:
  217. """高股息防御策略 - 20万资金"""
  218. def __init__(self, capital: float = 200000):
  219. self.capital = capital
  220. self.target_dividend_yield = 0.05 # 目标股息率5%
  221. self.holdings = [] # 当前持仓
  222. # 核心标的池(历史高股息+稳定)
  223. self.core_pool = [
  224. {'code': '600900', 'name': '长江电力', 'sector': '水电'},
  225. {'code': '601088', 'name': '中国神华', 'sector': '煤炭'},
  226. {'code': '601288', 'name': '农业银行', 'sector': '银行'},
  227. {'code': '601006', 'name': '大秦铁路', 'sector': '交运'},
  228. {'code': '600377', 'name': '宁沪高速', 'sector': '高速'},
  229. {'code': '600887', 'name': '伊利股份', 'sector': '消费'},
  230. {'code': '000895', 'name': '双汇发展', 'sector': '食品'},
  231. {'code': '600048', 'name': '保利发展', 'sector': '地产'},
  232. ]
  233. def get_dividend_data(self, code: str) -> Dict:
  234. """获取个股股息数据"""
  235. try:
  236. # 获取历史分红数据
  237. df = ak.stock_dividend_cninfo(symbol=code)
  238. if df.empty:
  239. return {'dividend_yield': 0, 'years': 0}
  240. # 计算平均股息率(近3年)
  241. recent = df.head(3)
  242. avg_yield = recent['股息率'].mean() if '股息率' in recent.columns else 0
  243. return {
  244. 'dividend_yield': avg_yield,
  245. 'years': len(df)
  246. }
  247. except Exception as e:
  248. logger.error(f"获取{code}股息数据失败: {e}")
  249. return {'dividend_yield': 0, 'years': 0}
  250. def screen_stocks(self) -> List[Dict]:
  251. """筛选高股息股票"""
  252. results = []
  253. for stock in self.core_pool:
  254. try:
  255. # 获取实时价格
  256. df = ak.stock_zh_a_spot_em()
  257. stock_info = df[df['代码'] == stock['code']]
  258. if stock_info.empty:
  259. continue
  260. price = float(stock_info['最新价'].values[0])
  261. dividend_data = self.get_dividend_data(stock['code'])
  262. # 估算当前股息率(假设分红金额不变)
  263. if dividend_data['years'] > 0:
  264. # 简化计算:使用历史平均股息率
  265. current_yield = dividend_data['dividend_yield']
  266. else:
  267. current_yield = 0
  268. results.append({
  269. 'code': stock['code'],
  270. 'name': stock['name'],
  271. 'sector': stock['sector'],
  272. 'price': price,
  273. 'dividend_yield': current_yield,
  274. 'score': current_yield * 100 # 评分就是股息率
  275. })
  276. except Exception as e:
  277. logger.error(f"处理{stock['code']}失败: {e}")
  278. # 按股息率排序
  279. results = sorted(results, key=lambda x: x['dividend_yield'], reverse=True)
  280. return results
  281. def generate_allocation(self) -> List[Dict]:
  282. """生成配置方案"""
  283. stocks = self.screen_stocks()
  284. if not stocks:
  285. return []
  286. # 选择股息率前5的股票,每只4万
  287. selected = stocks[:5]
  288. allocation = []
  289. for stock in selected:
  290. if stock['dividend_yield'] >= 0.04: # 至少4%股息率
  291. shares = int(40000 / stock['price'] / 100) * 100 # 整手
  292. allocation.append({
  293. 'code': stock['code'],
  294. 'name': stock['name'],
  295. 'price': stock['price'],
  296. 'shares': shares,
  297. 'dividend_yield': stock['dividend_yield'],
  298. 'invest_amount': shares * stock['price']
  299. })
  300. return allocation
  301. class RiskManager:
  302. """风险管理器"""
  303. def __init__(self, portfolio: PortfolioManager):
  304. self.portfolio = portfolio
  305. self.max_drawdown_total = 0.12 # 总回撤12%红线
  306. self.max_drawdown_strategy = 0.15 # 单策略回撤15%
  307. self.max_loss_per_trade = 0.08 # 单笔8%止损
  308. def check_portfolio_risk(self, current_value: float) -> Dict:
  309. """检查组合风险"""
  310. initial_value = self.portfolio.total_capital
  311. drawdown = (initial_value - current_value) / initial_value
  312. alerts = []
  313. actions = []
  314. if drawdown > self.max_drawdown_total:
  315. alerts.append(f"⚠️ 总回撤 {drawdown*100:.1f}% 超过红线 {self.max_drawdown_total*100:.1f}%")
  316. actions.append("HALF_ALL") # 全部减仓50%
  317. elif drawdown > 0.08:
  318. alerts.append(f"⚠️ 总回撤 {drawdown*100:.1f}% 接近警戒线")
  319. return {
  320. 'drawdown': drawdown,
  321. 'alerts': alerts,
  322. 'actions': actions,
  323. 'is_safe': len(alerts) == 0
  324. }
  325. def check_stop_loss(self, position: Dict, current_price: float) -> bool:
  326. """检查是否需要止损"""
  327. entry_price = position.get('entry_price', current_price)
  328. loss_pct = (current_price - entry_price) / entry_price
  329. if loss_pct < -self.max_loss_per_trade:
  330. logger.warning(f"止损触发: {position['code']} 亏损 {loss_pct*100:.1f}%")
  331. return True
  332. return False
  333. class BacktestEngine:
  334. """回测引擎"""
  335. def __init__(self, strategy, start_date: str, end_date: str, initial_capital: float):
  336. self.strategy = strategy
  337. self.start_date = start_date
  338. self.end_date = end_date
  339. self.initial_capital = initial_capital
  340. self.capital = initial_capital
  341. self.positions = {} # 当前持仓
  342. self.trades = [] # 交易记录
  343. self.daily_values = [] # 每日净值
  344. def run(self) -> Dict:
  345. """运行回测"""
  346. logger.info(f"开始回测: {self.start_date} 至 {self.end_date}")
  347. # 这里简化处理,实际应该按日遍历
  348. # 由于AKShare历史数据获取限制,这里提供框架
  349. # 模拟回测结果
  350. results = {
  351. 'initial_capital': self.initial_capital,
  352. 'final_value': self.capital,
  353. 'total_return': 0,
  354. 'annual_return': 0,
  355. 'max_drawdown': 0,
  356. 'sharpe_ratio': 0,
  357. 'trade_count': len(self.trades),
  358. 'win_rate': 0
  359. }
  360. return results
  361. class QuantSystem:
  362. """量化交易系统主类"""
  363. def __init__(self):
  364. self.portfolio = PortfolioManager(total_capital=1000000)
  365. self.cb_strategy = ConvertibleBondStrategy(capital=400000)
  366. self.sc_strategy = SmallCapMomentumStrategy(capital=300000)
  367. self.hd_strategy = HighDividendStrategy(capital=200000)
  368. self.risk_manager = RiskManager(self.portfolio)
  369. def daily_run(self):
  370. """每日运行"""
  371. logger.info("=" * 60)
  372. logger.info("开始每日策略运行")
  373. logger.info("=" * 60)
  374. # 1. 检查风险
  375. # current_value = self.calculate_portfolio_value()
  376. # risk_status = self.risk_manager.check_portfolio_risk(current_value)
  377. # 2. 生成各策略信号
  378. logger.info("\n--- 可转债双低策略 ---")
  379. cb_buys, cb_sells = self.cb_strategy.generate_signals([])
  380. logger.info(f"买入信号: {len(cb_buys)}只, 卖出信号: {len(cb_sells)}只")
  381. logger.info("\n--- 小市值动量策略 ---")
  382. sc_buys, sc_sells, market_ok = self.sc_strategy.generate_signals([])
  383. logger.info(f"买入信号: {len(sc_buys)}只, 卖出信号: {len(sc_sells)}只, 市场状态: {'正常' if market_ok else '空头'}")
  384. logger.info("\n--- 高股息防御策略 ---")
  385. hd_allocation = self.hd_strategy.generate_allocation()
  386. logger.info(f"配置方案: {len(hd_allocation)}只股票")
  387. for item in hd_allocation:
  388. logger.info(f" {item['name']}({item['code']}): {item['shares']}股 @ {item['price']:.2f}, 股息率{item['dividend_yield']*100:.1f}%")
  389. # 3. 保存状态
  390. self.portfolio.save_state()
  391. logger.info("\n策略运行完成")
  392. def generate_report(self) -> str:
  393. """生成交易报告"""
  394. report = []
  395. report.append("=" * 60)
  396. report.append("量化交易系统日报")
  397. report.append(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  398. report.append("=" * 60)
  399. # 资金分配
  400. report.append("\n【资金分配】")
  401. for strategy, ratio in self.portfolio.allocations.items():
  402. amount = self.portfolio.total_capital * ratio
  403. report.append(f" {strategy}: {ratio*100:.0f}% = {amount:,.0f}元")
  404. # 策略详情
  405. report.append("\n【可转债双低策略】")
  406. cb_candidates = self.cb_strategy.get_candidates()
  407. if not cb_candidates.empty:
  408. report.append(f"候选池: {len(cb_candidates)}只")
  409. report.append("Top 5:")
  410. for _, row in cb_candidates.head(5).iterrows():
  411. report.append(f" {row['名称']}: 价格{row['bond_price']:.2f}, 溢价率{row['premium_rate']:.1f}%, 双低{row['double_low_score']:.1f}")
  412. report.append("\n【小市值动量策略】")
  413. market_ok = self.sc_strategy.market_trend_ok()
  414. report.append(f"市场趋势: {'多头' if market_ok else '空头/观望'}")
  415. sc_candidates = self.sc_strategy.get_candidates()
  416. if not sc_candidates.empty:
  417. report.append(f"候选池: {len(sc_candidates)}只")
  418. report.append("Top 5:")
  419. for _, row in sc_candidates.head(5).iterrows():
  420. report.append(f" {row['名称']}: 市值{row['market_cap']:.1f}亿, 20日涨幅{row['change_20d']:.1f}%")
  421. report.append("\n【高股息防御策略】")
  422. hd_allocation = self.hd_strategy.generate_allocation()
  423. if hd_allocation:
  424. report.append("推荐配置:")
  425. for item in hd_allocation:
  426. report.append(f" {item['name']}: {item['shares']}股, 约{item['invest_amount']:,.0f}元, 股息率{item['dividend_yield']*100:.1f}%")
  427. return "\n".join(report)
  428. def main():
  429. """主函数"""
  430. print("=" * 60)
  431. print("量化交易系统 v1.0")
  432. print("100万资金管理 - 可转债双低 + 小市值动量 + 高股息防御")
  433. print("=" * 60)
  434. system = QuantSystem()
  435. # 运行日报
  436. system.daily_run()
  437. # 生成报告
  438. report = system.generate_report()
  439. print("\n" + report)
  440. # 保存报告
  441. report_file = f"report_{datetime.now().strftime('%Y%m%d')}.txt"
  442. with open(report_file, 'w', encoding='utf-8') as f:
  443. f.write(report)
  444. print(f"\n报告已保存: {report_file}")
  445. if __name__ == "__main__":
  446. main()