feat
This commit is contained in:
5
py-client/libs/__init__.py
Normal file
5
py-client/libs/__init__.py
Normal 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"]
|
||||
BIN
py-client/libs/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/libs/__pycache__/calc.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/calc.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/libs/__pycache__/grid_take_profit.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/grid_take_profit.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/libs/__pycache__/http.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/http.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/libs/__pycache__/market.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/market.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/libs/__pycache__/signal.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/signal.cpython-311.pyc
Normal file
Binary file not shown.
32
py-client/libs/calc.py
Normal file
32
py-client/libs/calc.py
Normal 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%
|
||||
80
py-client/libs/grid_take_profit.py
Normal file
80
py-client/libs/grid_take_profit.py
Normal 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
8
py-client/libs/http.py
Normal 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
24
py-client/libs/market.py
Normal 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
29
py-client/libs/signal.py
Normal 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
|
||||
Reference in New Issue
Block a user