- deploy/bot/ — XMPP bot核心(xmpp_agent_core + xmpp_zhiwei_bot) - deploy/profile-scripts/ — cron脚本(price_monitor等) - 运行时文件已替换为指向MoFin的符号链接 - 改代码只需改MoFin,系统自动生效
212 lines
7.1 KiB
Python
212 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
prepare_recommendation.py - 标准化操作建议前置流程
|
|
调用方式: python3 prepare_recommendation.py <code> <current_price>
|
|
输出: JSON,包含:
|
|
- strategy: 信号/止损/止盈/动作
|
|
- trade_constraints: 市场/最小交易单位/可否拆半手
|
|
- pnl: 成本/盈亏金额/盈亏比例 (仅持仓股)
|
|
- timing_signal: 系统策略信号
|
|
- action_note: 策略建议说明
|
|
|
|
依赖: per_stock_reassess.py, holdings/portfolio数据
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
|
|
DB_PATH = "/home/hmo/MoFin/data/mofin.db"
|
|
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
def get_stock_info(code):
|
|
"""从数据库获取股票信息"""
|
|
db = sqlite3.connect(DB_PATH)
|
|
db.row_factory = sqlite3.Row
|
|
try:
|
|
# 检查持仓
|
|
row = db.execute(
|
|
"SELECT * FROM holdings WHERE code=? AND is_active=1 AND shares>0",
|
|
(code,)
|
|
).fetchone()
|
|
if row:
|
|
hold = dict(row)
|
|
hold['is_holding'] = True
|
|
else:
|
|
hold = {'is_holding': False}
|
|
|
|
# 检查是否是港股
|
|
hk_codes = ['0' + str(i) for i in range(1, 100)] # 港股5位码
|
|
is_hk = code.isdigit() and not code.startswith(('6', '3', '0'))
|
|
if code.startswith(('01', '02', '09')):
|
|
is_hk = True
|
|
|
|
hold['market'] = 'HK' if is_hk else 'CN'
|
|
|
|
# 最小交易单位
|
|
if is_hk:
|
|
hold['min_lot'] = 100
|
|
hold['can_split'] = False
|
|
elif code.startswith('688'):
|
|
hold['min_lot'] = 200
|
|
hold['can_split'] = False
|
|
else:
|
|
hold['min_lot'] = 100
|
|
hold['can_split'] = True # A股可以拆散
|
|
|
|
return hold
|
|
finally:
|
|
db.close()
|
|
|
|
def run_reassess(code, price):
|
|
"""调用per_stock_reassess获取策略信号"""
|
|
reassess_script = os.path.join(SCRIPTS_DIR, "per_stock_reassess.py")
|
|
import subprocess
|
|
result = subprocess.run(
|
|
["python3", reassess_script, code, str(price)],
|
|
capture_output=True, text=True, timeout=60
|
|
)
|
|
return result.stdout
|
|
|
|
def parse_reassess_output(output):
|
|
"""从per_stock_reassess输出中提取关键信号"""
|
|
result = {
|
|
'timing_signal': 'unknown',
|
|
'action_note': '',
|
|
'stop_loss': None,
|
|
'take_profit': None,
|
|
'buy_zone_low': None,
|
|
'buy_zone_high': None,
|
|
'rr': None,
|
|
}
|
|
|
|
for line in output.split('\n'):
|
|
line = line.strip()
|
|
if '[OK]' in line:
|
|
# 格式: [OK] 688981 中芯国际: 盈利持有 | ⚠️盈亏比不足1:1.5 | ... | 信号:买入
|
|
parts = line.split(': ', 2)
|
|
if len(parts) >= 2:
|
|
action_part = parts[-1]
|
|
result['action_note'] = action_part
|
|
# 提取信号
|
|
if '信号:' in action_part:
|
|
signal = action_part.split('信号:')[-1].split()[0]
|
|
result['timing_signal'] = signal
|
|
# 提取止损
|
|
if '止损' in action_part:
|
|
import re
|
|
m = re.search(r'止损(\d+\.?\d*)', action_part)
|
|
if m:
|
|
result['stop_loss'] = float(m.group(1))
|
|
# 提取目标/止盈
|
|
if '目标' in action_part:
|
|
import re
|
|
m = re.search(r'目标(\d+\.?\d*)', action_part)
|
|
if m:
|
|
result['take_profit'] = float(m.group(1))
|
|
# 提取买入区
|
|
if '买入区' in action_part:
|
|
import re
|
|
m = re.search(r'买入区(\d+\.?\d*)~(\d+\.?\d*)', action_part)
|
|
if m:
|
|
result['buy_zone_low'] = float(m.group(1))
|
|
result['buy_zone_high'] = float(m.group(2))
|
|
# 提取RR
|
|
if 'RR' in action_part:
|
|
import re
|
|
m = re.search(r'RR(\d+\.?\d*)', action_part)
|
|
if m:
|
|
result['rr'] = float(m.group(1))
|
|
|
|
if 'factors=' in line:
|
|
import re
|
|
m = re.search(r"factors=\['(.*?)'\]", line)
|
|
if m:
|
|
factors = m.group(1).split("', '")
|
|
result['factors'] = factors
|
|
|
|
return result
|
|
|
|
def build_output(code, price, stock_info, strategy):
|
|
"""构建标准化JSON输出"""
|
|
output = {
|
|
'code': code,
|
|
'price': price,
|
|
'market': stock_info.get('market', 'CN'),
|
|
'strategy': {
|
|
'timing_signal': strategy.get('timing_signal', 'unknown'),
|
|
'action_note': strategy.get('action_note', ''),
|
|
'stop_loss': strategy.get('stop_loss'),
|
|
'take_profit': strategy.get('take_profit'),
|
|
'buy_zone': {
|
|
'low': strategy.get('buy_zone_low'),
|
|
'high': strategy.get('buy_zone_high')
|
|
},
|
|
'rr': strategy.get('rr'),
|
|
'factors': strategy.get('factors', []),
|
|
},
|
|
'trade_constraints': {
|
|
'market': '港股' if stock_info.get('market') == 'HK' else 'A股',
|
|
'min_trading_unit': stock_info.get('min_lot', 100),
|
|
'can_split_lot': stock_info.get('can_split', True),
|
|
'note': (
|
|
f"港股每手{stock_info.get('min_lot', 100)}股不能拆半手"
|
|
if stock_info.get('market') == 'HK'
|
|
else f"科创板每手{stock_info.get('min_lot', 200)}股不能拆半手"
|
|
if stock_info.get('min_lot') == 200
|
|
else "A股可拆散交易"
|
|
)
|
|
},
|
|
'pnl': None,
|
|
}
|
|
|
|
if stock_info.get('is_holding'):
|
|
cost = stock_info.get('cost', 0)
|
|
shares = stock_info.get('shares', 0)
|
|
market_value = price * shares
|
|
cost_value = cost * shares
|
|
pnl_amount = market_value - cost_value
|
|
pnl_pct = ((price / cost) - 1) * 100 if cost else 0
|
|
output['pnl'] = {
|
|
'cost_price': cost,
|
|
'shares': shares,
|
|
'market_value': round(market_value, 2),
|
|
'cost_value': round(cost_value, 2),
|
|
'pnl_amount': round(pnl_amount, 2),
|
|
'pnl_pct': round(pnl_pct, 2),
|
|
'status': '浮盈' if pnl_amount >= 0 else '浮亏',
|
|
}
|
|
|
|
return output
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
print(json.dumps({
|
|
'error': '用法: python3 prepare_recommendation.py <code> <price>'
|
|
}, ensure_ascii=False))
|
|
sys.exit(1)
|
|
|
|
code = sys.argv[1]
|
|
try:
|
|
price = float(sys.argv[2])
|
|
except ValueError:
|
|
print(json.dumps({'error': f'无效价格: {sys.argv[2]}'}, ensure_ascii=False))
|
|
sys.exit(1)
|
|
|
|
# 获取股票信息(市场/最小单位/持仓)
|
|
stock_info = get_stock_info(code)
|
|
|
|
# 运行策略重评
|
|
reassess_output = run_reassess(code, price)
|
|
strategy = parse_reassess_output(reassess_output)
|
|
|
|
# 构建输出
|
|
output = build_output(code, price, stock_info, strategy)
|
|
|
|
# 输出JSON
|
|
print(json.dumps(output, ensure_ascii=False, indent=2))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|