This commit is contained in:
2026-08-28 18:52:27 +08:00
parent e0ecdfba52
commit d09f271569
89 changed files with 1716 additions and 2515 deletions

View File

@@ -0,0 +1,5 @@
from .calc import calc_buy_volume, trading_time
from .market import market_allow_open, status
from .signal import SignalItem, SignalResult, fetch_signal, init_signals
__all__ = ["calc_buy_volume", "trading_time", "market_allow_open", "status", "SignalItem", "SignalResult", "fetch_signal", "init_signals"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

32
py-client/libs/calc.py Normal file
View File

@@ -0,0 +1,32 @@
from datetime import datetime, time
from math import floor
def trading_time(now: datetime) -> bool:
if now.weekday() >= 5: return False
return time(9, 30) <= now.time() <= time(11, 30) or time(13) <= now.time() <= time(15)
def calc_buy_volume(price: float, buy_value: float) -> int:
if price <= 0 or buy_value <= 0: return 0
return max(1, floor(buy_value / (price * 100))) * 100
def calculate_min_profit_rate(price: float, profit_mult: int) -> float:
"""
根据价格返回最小利润率
Args:
price: 股票价格
profit_mult: 利润倍数配置
Returns:
float: 最小利润率(百分比)
"""
if price >= 300:
return 3 * profit_mult # 3%
if price >= 200:
return 5 * profit_mult # 5%
elif price >= 100:
return 7 * profit_mult # 7%
else:
return 9 * profit_mult # 9%

View File

@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""网格回撤止盈状态机。
该模块只负责记录每个持仓的最高盈利网格,并判断当前盈亏率是否从
峰值网格回撤。它不包含下单逻辑,由主策略和 Upmax 根据返回的状态决定是否卖出。
"""
from dataclasses import dataclass
from enum import Enum
import math
import threading
class GridState(str, Enum):
"""单次盈亏率观察后的网格状态。"""
ARMED = "armed" # 首次记录该持仓的峰值网格
RAISED = "raised" # 盈利继续上升,峰值网格已抬高
RETREAT = "retreat" # 从峰值网格回撤,应由调用方执行止盈
STEADY = "steady" # 仍处于当前峰值网格,继续持有
@dataclass(frozen=True)
class GridObservation:
"""一次网格观察的不可变结果。"""
state: GridState
current_grid: int # 当前盈亏率所处的网格
peak_grid: int # 该持仓自观察以来的最高网格
class GridTrailingTracker:
"""按持仓键隔离、线程安全的峰值网格跟踪器。"""
def __init__(self, step: float = 1.0):
"""
Args:
step: 单个网格的盈亏率跨度(百分点),必须大于 0。
"""
if step <= 0:
raise ValueError("grid step must be positive")
self._step = step
# key 由调用方组成“账户 + 股票代码”,防止多账户状态串扰。
self._peaks: dict[str, int] = {}
# 主策略和回调线程可能并发访问,所有峰值读写均在同一把锁内。
self._lock = threading.Lock()
def observe(self, position_key: str, pnl_rate: float) -> GridObservation:
"""记录当前盈亏率,并返回相对于历史峰值的状态。"""
# floor 保证负数盈亏率也按完整网格向下归档。
current_grid = math.floor(pnl_rate / self._step)
with self._lock:
peak_grid = self._peaks.get(position_key)
# 第一次看到该持仓:建立基准,不触发止盈。
if peak_grid is None:
self._peaks[position_key] = current_grid
return GridObservation(GridState.ARMED, current_grid, current_grid)
# 进入更高网格:更新峰值,继续持有。
if current_grid > peak_grid:
self._peaks[position_key] = current_grid
return GridObservation(GridState.RAISED, current_grid, current_grid)
# 跌破峰值网格:报告回撤,但保留峰值直到卖出成功后 clear。
if current_grid < peak_grid:
return GridObservation(GridState.RETREAT, current_grid, peak_grid)
return GridObservation(GridState.STEADY, current_grid, peak_grid)
def clear(self, position_key: str) -> None:
"""持仓卖出成功后删除峰值,使下次建仓从新状态开始。"""
with self._lock:
self._peaks.pop(position_key, None)
def retain(self, position_keys) -> None:
"""删除已不在券商持仓中的峰值,避免同代码重新开仓继承旧状态。"""
active = set(position_keys)
with self._lock:
self._peaks = {key: value for key, value in self._peaks.items() if key in active}

8
py-client/libs/http.py Normal file
View File

@@ -0,0 +1,8 @@
import json
from urllib.request import Request, urlopen
def get_json(url: str, timeout: float = 5.0):
request = Request(url, headers={"Accept": "application/json", "User-Agent": "big-qmt-python/1"})
with urlopen(request, timeout=timeout) as response:
return json.load(response)

24
py-client/libs/market.py Normal file
View File

@@ -0,0 +1,24 @@
import logging
import secrets
from .http import get_json
API_HOST = "http://139.224.247.176:13499"
MARKET_URL, PERIOD, HTTP_TIMEOUT = "/a/market", "60m", 5.0
def status(payload) -> str:
value = payload.get("data", payload) if isinstance(payload, dict) else payload
if isinstance(value, list): value = value[-1] if value else None
if isinstance(value, dict): value = value.get("action", value.get("status", value.get("signal")))
result = str(value).strip().upper()
return result if result in {"UP", "DOWN", "NEUTRAL"} else "UNKNOWN"
def market_allow_open(api_host: str = API_HOST) -> bool:
url = f"{api_host}{MARKET_URL}?period={PERIOD}&t={secrets.token_urlsafe(12)}"
try: result = status(get_json(url, HTTP_TIMEOUT))
except Exception as exc:
logging.error("获取大盘指数失败: %s %s", url, exc); return False
logging.info("大盘信号: url=%s status=%s", url, result)
return result == "UP"

29
py-client/libs/signal.py Normal file
View File

@@ -0,0 +1,29 @@
from dataclasses import dataclass, field
import secrets
from .http import get_json
@dataclass
class SignalItem:
signal_key: str = ""; code: str = ""; name: str = ""; desc: str = ""; last_close: float = 0
tech_indicator: dict[str, float] = field(default_factory=dict)
@dataclass
class SignalResult:
code: str = ""; total: int = 0; updated: str = ""; data: dict[str, SignalItem] = field(default_factory=dict); message: str = ""
def fetch_signal(api_host: str, sub_url: str, timeout: float = 5.0) -> SignalResult:
url = f"{api_host}{sub_url}?t={secrets.token_urlsafe(12)}"
raw = get_json(url, timeout)
items = {code: SignalItem(**item) for code, item in (raw.get("data") or {}).items()}
return SignalResult(raw.get("code", ""), raw.get("total", 0), raw.get("updated", ""), items, raw.get("message", ""))
def init_signals(global_config, allow: list[str]) -> list[SignalItem]:
result = []
for key, cfg in global_config.signals.items():
if key in allow:
for item in fetch_signal(global_config.api_host, cfg.url).data.values(): item.signal_key = key; result.append(item)
return result