168 lines
6.2 KiB
Python
168 lines
6.2 KiB
Python
"""趋势策略持仓管理逻辑,对应 Go 版本的 ``logic/positions.go``。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from math import floor
|
||
|
||
from libs.calc import calc_buy_volume,calculate_min_profit_rate
|
||
from libs.grid_take_profit import GridState, GridTrailingTracker
|
||
from sdk import OP_BUY, OP_SELL
|
||
import config
|
||
from .order import PlaceOrderRequest
|
||
from .state import STATUS_ING, STATUS_NONE, STATUS_OK
|
||
from .runtime import Runtime
|
||
|
||
LEG_BASE = "base"
|
||
LEG_ADDED = "add"
|
||
|
||
# 止盈网格跟踪器延迟初始化,避免导入模块时账户配置尚未加载。
|
||
profit_tracker = None
|
||
|
||
# 分级补仓档位(百分比)
|
||
LOSS_TIERS = [-30, -50]
|
||
# 补仓反弹确认阈值(百分比)
|
||
LOSS_REBOUND_THRESHOLD = 0.5
|
||
|
||
def manage_positions(run:Runtime, ticks, positions, market_ok: bool,available:float) -> None:
|
||
"""执行持仓计算。"""
|
||
logging.info(f"持仓:{len(positions)} 支股票,开始处理")
|
||
global profit_tracker
|
||
profit_tracker = GridTrailingTracker(step=run.account_cfg.grid_step_pct)
|
||
for idx,pos in positions:
|
||
code = pos['stock_code']
|
||
avg_price = pos.get('avg_price', 0)
|
||
volume = pos.get('volume', 0)
|
||
can_use_volume = pos.get('can_use_volume', 0)
|
||
current_price = ticks.get(code, {}).get('lastPrice', 0)
|
||
strategy_name = pos.get('strategy_name', '')
|
||
market_value = pos.get('market_value',0)
|
||
profit = pos.get('profit_rate', 0)
|
||
|
||
# 排除指定股票
|
||
if code in config.account_config.excluded_codes:
|
||
continue
|
||
|
||
# 过滤无效仓位
|
||
if avg_price == 0 or can_use_volume == 0 or current_price == 0 or volume == 0:
|
||
continue
|
||
|
||
# 计算盈亏率(百分比)
|
||
pnl_ratio = (current_price - avg_price) / avg_price * 100 if avg_price != 0 else 0
|
||
pnl_ratio = round(pnl_ratio, 2)
|
||
|
||
# 计算最小利润率:1倍
|
||
min_profit_rate_val = calculate_min_profit_rate(avg_price, 1)
|
||
|
||
# 盈利处理
|
||
is_closed, message = handle_profit(run,code,avg_price, pnl_ratio, min_profit_rate_val, can_use_volume, strategy_name)
|
||
if is_closed:
|
||
logging.info("profit", code, f"止盈执行 | {message}")
|
||
if message != "":
|
||
logging.info("profit", code, message)
|
||
|
||
# 补仓处理
|
||
if config.account_config.enable_loss_add_position and market_ok:
|
||
is_replenished, message = handle_loss(run,code,current_price,pnl_ratio,market_value,market_ok,available)
|
||
if is_replenished:
|
||
logging.info("loss", code, f"补仓执行 | {message}")
|
||
if message != "":
|
||
logging.info("loss", code, message)
|
||
|
||
# 盈利处理
|
||
def handle_profit(run:Runtime, code: str, pnl_rate: float,
|
||
min_profit_rate: float, vol: int) -> tuple[bool, str]:
|
||
"""
|
||
盈利处理 - 基于网格的止盈策略
|
||
|
||
Args:
|
||
code: 股票代码
|
||
open_price: 开仓价格
|
||
pnl_rate: 当前盈亏率(百分比)
|
||
min_profit_rate: 最小利润率阈值
|
||
vol: 可用股数
|
||
strategy_name: str
|
||
|
||
Returns:
|
||
tuple[bool, str]: (是否执行平仓, 操作说明)
|
||
"""
|
||
# 预检查:未达到最小利润率
|
||
if pnl_rate < min_profit_rate:
|
||
return False, ""
|
||
|
||
position_key = f"{run.account_cfg.account_id}:{code}"
|
||
observation = profit_tracker.observe(position_key, pnl_rate)
|
||
|
||
if observation.state == GridState.ARMED:
|
||
msg = f"首次达到{pnl_rate}%,设置峰值网格{observation.current_grid}"
|
||
return False, msg
|
||
|
||
if observation.state == GridState.RAISED:
|
||
return False, f"上涨至{pnl_rate}%,更新峰值网格{observation.current_grid}"
|
||
|
||
# 执行平仓
|
||
if observation.state == GridState.RETREAT:
|
||
order_id = run.orders.new_order_id(LEG_BASE)
|
||
request = PlaceOrderRequest(run.client, OP_SELL, code, vol, order_id)
|
||
result = run.orders.place(request)
|
||
if result :
|
||
success_msg = f"✓ 委托成功 | {vol}股 订单号:{result} 等待成交"
|
||
logging.info("profit", code, success_msg)
|
||
return True, success_msg
|
||
else:
|
||
fail_msg = f"止盈委托失败: {code}"
|
||
logging.error("profit", code, "✗ 止盈委托失败")
|
||
return False, fail_msg
|
||
|
||
|
||
def handle_loss(run:Runtime, code: str, current_price,pnl_rate,market_value: float,market_ok: bool, available: float) -> tuple[bool, str]:
|
||
"""满足条件时提交补仓委托,并返回扣减后的剩余预算。"""
|
||
state = run.state.get(code)
|
||
added_num = state.get('added_num',0)
|
||
# 预检查:未达到最低补仓阈值
|
||
if pnl_rate > LOSS_TIERS[added_num]:
|
||
return False, ""
|
||
|
||
# 强制条件
|
||
if current_price>200 or market_value>=60000:
|
||
return False, f"成本价{current_price}>200,仓位价值{market_value}>=60000, 不补仓"
|
||
|
||
# 1. 大盘必须允许开仓,且价格已从观察低点达到反弹阈值。
|
||
if not market_ok or not run.add_watch.triggered("补仓", code, current_price):
|
||
return False
|
||
|
||
# 2. 计算补仓数量和预计占用金额。
|
||
volume = calc_buy_volume(current_price, run.account_cfg.buy_value)
|
||
amount = current_price * volume
|
||
|
||
# 3. 检查预算。
|
||
if amount > available:
|
||
return False, f"f{code} f{amount} 仓位资金不够补仓"
|
||
|
||
# 是否已有未完成的买入委托
|
||
if run.orders.busy(run, code, "BUY"):
|
||
return False, f"{code}订单锁定中"
|
||
|
||
# 4. 生成补仓订单号并提交买入委托。
|
||
order_id = run.orders.new_order_id(LEG_ADDED)
|
||
request = PlaceOrderRequest(run.client, OP_BUY, code, volume, order_id)
|
||
result = run.orders.place(request)
|
||
if result :
|
||
state.added_num = +1
|
||
state.added_status = run.state.STATUS_ING
|
||
state.added_order_id = order_id
|
||
run.state.set(state)
|
||
run.state.save()
|
||
run.add_watch.forget(code)
|
||
return True,f"补仓委托成功: {code} {volume}手, 等待成交确认"
|
||
else:
|
||
return False,f"补仓失败: {code}"
|
||
|
||
|
||
def forget(run, code: str) -> None:
|
||
"""持仓退出后清理开仓、补仓观察记录和止盈峰值。"""
|
||
|
||
|
||
|
||
run.peak_grids.pop(f"{code}|{LEG_ADDED}", None)
|