feat: independent strategy paper trading controls
- Each strategy has its own position count (max 4 each)
- paper_config.enabled_strategies: per-strategy toggle
- is_strategy_enabled() checks both master switch and strategy list
- API: /api/paper/config now supports enabled_strategies array
- Config auto-loads on API startup
Usage: POST /api/paper/config {enabled_strategies: ['v52_8signals']}
This commit is contained in:
parent
ee90b8dcfa
commit
a9c3523a24
@ -597,12 +597,23 @@ async def get_signal_trades(
|
|||||||
# 模拟盘配置状态(与signal_engine共享的运行时状态)
|
# 模拟盘配置状态(与signal_engine共享的运行时状态)
|
||||||
paper_config = {
|
paper_config = {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
|
"enabled_strategies": [], # 分策略开关: ["v51_baseline", "v52_8signals"]
|
||||||
"initial_balance": 10000,
|
"initial_balance": 10000,
|
||||||
"risk_per_trade": 0.02,
|
"risk_per_trade": 0.02,
|
||||||
"max_positions": 4,
|
"max_positions": 4,
|
||||||
"tier_multiplier": {"light": 0.5, "standard": 1.0, "heavy": 1.5},
|
"tier_multiplier": {"light": 0.5, "standard": 1.0, "heavy": 1.5},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 启动时加载已有配置
|
||||||
|
_config_path = os.path.join(os.path.dirname(__file__), "paper_config.json")
|
||||||
|
if os.path.exists(_config_path):
|
||||||
|
try:
|
||||||
|
with open(_config_path, "r") as _f:
|
||||||
|
_saved = json.load(_f)
|
||||||
|
paper_config.update(_saved)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/paper/config")
|
@app.get("/api/paper/config")
|
||||||
async def paper_get_config(user: dict = Depends(get_current_user)):
|
async def paper_get_config(user: dict = Depends(get_current_user)):
|
||||||
@ -616,11 +627,10 @@ async def paper_set_config(request: Request, user: dict = Depends(get_current_us
|
|||||||
if user.get("role") != "admin":
|
if user.get("role") != "admin":
|
||||||
raise HTTPException(status_code=403, detail="仅管理员可修改")
|
raise HTTPException(status_code=403, detail="仅管理员可修改")
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
for k in ["enabled", "initial_balance", "risk_per_trade", "max_positions"]:
|
for k in ["enabled", "enabled_strategies", "initial_balance", "risk_per_trade", "max_positions"]:
|
||||||
if k in body:
|
if k in body:
|
||||||
paper_config[k] = body[k]
|
paper_config[k] = body[k]
|
||||||
# 写入配置文件让signal_engine也能读到
|
# 写入配置文件让signal_engine也能读到
|
||||||
import json
|
|
||||||
config_path = os.path.join(os.path.dirname(__file__), "paper_config.json")
|
config_path = os.path.join(os.path.dirname(__file__), "paper_config.json")
|
||||||
with open(config_path, "w") as f:
|
with open(config_path, "w") as f:
|
||||||
json.dump(paper_config, f, indent=2)
|
json.dump(paper_config, f, indent=2)
|
||||||
|
|||||||
@ -67,10 +67,11 @@ def load_strategy_configs() -> list[dict]:
|
|||||||
return configs
|
return configs
|
||||||
|
|
||||||
# ─── 模拟盘配置 ───────────────────────────────────────────────────
|
# ─── 模拟盘配置 ───────────────────────────────────────────────────
|
||||||
PAPER_TRADING_ENABLED = False # 开关(范总确认后通过API开启)
|
PAPER_TRADING_ENABLED = False # 总开关(兼容旧逻辑)
|
||||||
|
PAPER_ENABLED_STRATEGIES = [] # 分策略开关: ["v51_baseline", "v52_8signals"]
|
||||||
PAPER_INITIAL_BALANCE = 10000 # 虚拟初始资金 USDT
|
PAPER_INITIAL_BALANCE = 10000 # 虚拟初始资金 USDT
|
||||||
PAPER_RISK_PER_TRADE = 0.02 # 单笔风险 2%(即200U)
|
PAPER_RISK_PER_TRADE = 0.02 # 单笔风险 2%(即200U)
|
||||||
PAPER_MAX_POSITIONS = 4 # 最大同时持仓数
|
PAPER_MAX_POSITIONS = 4 # 每套策略最大同时持仓数
|
||||||
PAPER_TIER_MULTIPLIER = { # 档位仓位倍数
|
PAPER_TIER_MULTIPLIER = { # 档位仓位倍数
|
||||||
"light": 0.5, # 轻仓: 1%
|
"light": 0.5, # 轻仓: 1%
|
||||||
"standard": 1.0, # 标准: 2%
|
"standard": 1.0, # 标准: 2%
|
||||||
@ -80,18 +81,29 @@ PAPER_FEE_RATE = 0.0005 # Taker手续费 0.05%(开仓+平仓各一
|
|||||||
|
|
||||||
def load_paper_config():
|
def load_paper_config():
|
||||||
"""从配置文件加载模拟盘开关和参数"""
|
"""从配置文件加载模拟盘开关和参数"""
|
||||||
global PAPER_TRADING_ENABLED, PAPER_INITIAL_BALANCE, PAPER_RISK_PER_TRADE, PAPER_MAX_POSITIONS
|
global PAPER_TRADING_ENABLED, PAPER_ENABLED_STRATEGIES, PAPER_INITIAL_BALANCE, PAPER_RISK_PER_TRADE, PAPER_MAX_POSITIONS
|
||||||
config_path = os.path.join(os.path.dirname(__file__), "paper_config.json")
|
config_path = os.path.join(os.path.dirname(__file__), "paper_config.json")
|
||||||
try:
|
try:
|
||||||
with open(config_path, "r") as f:
|
with open(config_path, "r") as f:
|
||||||
import json as _json2
|
import json as _json2
|
||||||
cfg = _json2.load(f)
|
cfg = _json2.load(f)
|
||||||
PAPER_TRADING_ENABLED = cfg.get("enabled", False)
|
PAPER_TRADING_ENABLED = cfg.get("enabled", False)
|
||||||
|
PAPER_ENABLED_STRATEGIES = cfg.get("enabled_strategies", [])
|
||||||
PAPER_INITIAL_BALANCE = cfg.get("initial_balance", 10000)
|
PAPER_INITIAL_BALANCE = cfg.get("initial_balance", 10000)
|
||||||
PAPER_RISK_PER_TRADE = cfg.get("risk_per_trade", 0.02)
|
PAPER_RISK_PER_TRADE = cfg.get("risk_per_trade", 0.02)
|
||||||
PAPER_MAX_POSITIONS = cfg.get("max_positions", 4)
|
PAPER_MAX_POSITIONS = cfg.get("max_positions", 4)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def is_strategy_enabled(strategy_name: str) -> bool:
|
||||||
|
"""检查某策略是否启用模拟盘"""
|
||||||
|
if not PAPER_TRADING_ENABLED:
|
||||||
|
return False
|
||||||
|
# 如果enabled_strategies为空,走旧逻辑(全部启用)
|
||||||
|
if not PAPER_ENABLED_STRATEGIES:
|
||||||
|
return True
|
||||||
|
return strategy_name in PAPER_ENABLED_STRATEGIES
|
||||||
# ─────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# 窗口大小(毫秒)
|
# 窗口大小(毫秒)
|
||||||
@ -870,11 +882,14 @@ def paper_close_by_signal(symbol: str, current_price: float, now_ms: int, strate
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def paper_active_count() -> int:
|
def paper_active_count(strategy: Optional[str] = None) -> int:
|
||||||
"""当前所有币种活跃持仓总数"""
|
"""当前活跃持仓总数(按策略独立计数)"""
|
||||||
with get_sync_conn() as conn:
|
with get_sync_conn() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT COUNT(*) FROM paper_trades WHERE status IN ('active','tp1_hit')")
|
if strategy:
|
||||||
|
cur.execute("SELECT COUNT(*) FROM paper_trades WHERE strategy=%s AND status IN ('active','tp1_hit')", (strategy,))
|
||||||
|
else:
|
||||||
|
cur.execute("SELECT COUNT(*) FROM paper_trades WHERE status IN ('active','tp1_hit')")
|
||||||
return cur.fetchone()[0]
|
return cur.fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
@ -929,9 +944,11 @@ def main():
|
|||||||
last_1m_save[sym] = bar_1m
|
last_1m_save[sym] = bar_1m
|
||||||
|
|
||||||
# 反向信号平仓:按策略独立判断,score>=75才触发
|
# 反向信号平仓:按策略独立判断,score>=75才触发
|
||||||
if PAPER_TRADING_ENABLED and warmup_cycles <= 0:
|
if warmup_cycles <= 0:
|
||||||
for strategy_cfg, result in strategy_results:
|
for strategy_cfg, result in strategy_results:
|
||||||
strategy_name = strategy_cfg.get("name", "v51_baseline")
|
strategy_name = strategy_cfg.get("name", "v51_baseline")
|
||||||
|
if not is_strategy_enabled(strategy_name):
|
||||||
|
continue
|
||||||
eval_dir = result.get("direction")
|
eval_dir = result.get("direction")
|
||||||
existing_dir = paper_get_active_direction(sym, strategy_name)
|
existing_dir = paper_get_active_direction(sym, strategy_name)
|
||||||
if existing_dir and eval_dir and existing_dir != eval_dir and result["score"] >= 75:
|
if existing_dir and eval_dir and existing_dir != eval_dir and result["score"] >= 75:
|
||||||
@ -948,10 +965,10 @@ def main():
|
|||||||
f"[{sym}] 🚨 信号[{strategy_name}]: {result['signal']} "
|
f"[{sym}] 🚨 信号[{strategy_name}]: {result['signal']} "
|
||||||
f"score={result['score']} price={result['price']:.1f}"
|
f"score={result['score']} price={result['price']:.1f}"
|
||||||
)
|
)
|
||||||
# 模拟盘开仓(需开关开启 + 跳过冷启动)
|
# 模拟盘开仓(需该策略启用 + 跳过冷启动)
|
||||||
if PAPER_TRADING_ENABLED and warmup_cycles <= 0:
|
if is_strategy_enabled(strategy_name) and warmup_cycles <= 0:
|
||||||
if not paper_has_active_position(sym, strategy_name):
|
if not paper_has_active_position(sym, strategy_name):
|
||||||
active_count = paper_active_count()
|
active_count = paper_active_count(strategy_name)
|
||||||
if active_count < PAPER_MAX_POSITIONS:
|
if active_count < PAPER_MAX_POSITIONS:
|
||||||
tier = result.get("tier", "standard")
|
tier = result.get("tier", "standard")
|
||||||
paper_open_trade(
|
paper_open_trade(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user