Restructure into modules/ with single-process CTP and config/ layout.

Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-01 14:42:16 +08:00
parent b354d6c701
commit e5a586f903
209 changed files with 21962 additions and 20963 deletions
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
from modules.fees.routes import register
__all__ = ["register"]
+385
View File
@@ -0,0 +1,385 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
# 专有软件 — 未经授权禁止复制、传播、转售。
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
"""期货手续费:仅 CTP 柜台同步入库,前端只读展示。"""
import json
import os
import re
from datetime import datetime
from typing import Optional
from modules.core.contract_specs import get_contract_spec
from modules.core.db_conn import connect_db, is_benign_migration_error
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
DEFAULT_JSON = os.path.join(DATA_DIR, "fee_rates.json")
# 无配置时的兜底(已为交易所标准约 2 倍)
DEFAULT_FEE = {
"open_fixed": 2.0,
"open_ratio": 0.0,
"close_yesterday_fixed": 2.0,
"close_yesterday_ratio": 0.0,
"close_today_fixed": 4.0,
"close_today_ratio": 0.0,
}
_INDEX_PRODUCTS = {"if", "ih", "ic", "im"}
def product_from_code(ths_code: str) -> str:
code = (ths_code or "").strip()
m = re.match(r"^([A-Za-z]+)", code)
return m.group(1).lower() if m else ""
def _get_db():
return connect_db()
def ensure_fee_rates_schema(conn=None) -> None:
"""补齐 fee_rates 表结构(旧库可能缺少 source 列)。"""
close = False
if conn is None:
conn = _get_db()
close = True
try:
for sql in (
"ALTER TABLE fee_rates ADD COLUMN source TEXT DEFAULT 'local'",
):
try:
conn.execute(sql)
except Exception as exc:
if not is_benign_migration_error(exc):
raise
conn.commit()
finally:
if close:
conn.close()
def get_setting(key: str, default: str = "") -> str:
conn = _get_db()
row = conn.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
conn.close()
if not row:
return default
return (row["value"] or default) if row["value"] is not None else default
def set_setting(key: str, value: str) -> None:
conn = _get_db()
conn.execute(
"""INSERT INTO settings (key, value) VALUES (?,?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value""",
(key, value),
)
conn.commit()
conn.close()
def get_fee_multiplier() -> float:
conn = _get_db()
row = conn.execute(
"SELECT value FROM settings WHERE key='fee_multiplier'"
).fetchone()
conn.close()
if row and row["value"]:
try:
return max(0.0, float(row["value"]))
except ValueError:
pass
return 2.0
def get_fee_source_mode() -> str:
"""固定 CTP 柜台。"""
return "ctp"
def purge_non_ctp_fee_rates() -> int:
"""删除非 CTP 来源的费率缓存。"""
conn = _get_db()
cur = conn.execute(
"DELETE FROM fee_rates WHERE COALESCE(source, '') != 'ctp'"
)
n = cur.rowcount
conn.commit()
conn.close()
return n
def _row_to_spec(row, mult: int) -> dict:
return {
"product": row["product"],
"exchange": row["exchange"] or "",
"mult": int(row["mult"] or mult),
"open_fixed": float(row["open_fixed"] or 0),
"open_ratio": float(row["open_ratio"] or 0),
"close_yesterday_fixed": float(row["close_yesterday_fixed"] or 0),
"close_yesterday_ratio": float(row["close_yesterday_ratio"] or 0),
"close_today_fixed": float(row["close_today_fixed"] or 0),
"close_today_ratio": float(row["close_today_ratio"] or 0),
"source": row["source"] if "source" in row.keys() else "local",
}
def get_fee_spec(ths_code: str, *, trading_mode: str = "simulation") -> dict:
product = product_from_code(ths_code)
if not product:
spec = get_contract_spec(ths_code)
return {**DEFAULT_FEE, "mult": spec["mult"], "product": "", "exchange": "", "source": "default"}
mult = get_contract_spec(ths_code)["mult"]
conn = _get_db()
ensure_fee_rates_schema(conn)
row = conn.execute(
"SELECT * FROM fee_rates WHERE product=? AND source='ctp'",
(product,),
).fetchone()
conn.close()
if row:
return _row_to_spec(row, mult)
try:
from modules.ctp.ctp_fee_sync import sync_fee_for_symbol
fields = sync_fee_for_symbol(trading_mode, ths_code)
if fields:
return {"product": product, **fields}
except Exception:
pass
if product in _INDEX_PRODUCTS:
return {
"product": product,
"exchange": "CFFEX",
"mult": mult,
"open_fixed": 0.0,
"open_ratio": 0.000092,
"close_yesterday_fixed": 0.0,
"close_yesterday_ratio": 0.000092,
"close_today_fixed": 0.0,
"close_today_ratio": 0.000276,
}
return {
"product": product,
"exchange": "",
"mult": mult,
**DEFAULT_FEE,
"source": "default",
}
def calc_side_fee(
price: float,
lots: float,
mult: int,
fixed: float,
ratio: float,
) -> float:
lots = lots or 1.0
fixed = fixed or 0.0
ratio = ratio or 0.0
return fixed * lots + ratio * price * mult * lots
def is_same_day(open_time: str, close_time: str) -> bool:
if not open_time or not close_time:
return True
o = open_time.strip().replace(" ", "T")[:10]
c = close_time.strip().replace(" ", "T")[:10]
return o == c
def calc_round_trip_fee(
ths_code: str,
entry_price: float,
close_price: float,
lots: float,
open_time: str = "",
close_time: str = "",
trading_mode: str = "simulation",
) -> float:
if not entry_price or not close_price:
return 0.0
spec = get_fee_spec(ths_code, trading_mode=trading_mode)
mult = spec["mult"]
lots = lots or 1.0
open_fee = calc_side_fee(
entry_price, lots, mult,
spec["open_fixed"], spec["open_ratio"],
)
if is_same_day(open_time, close_time):
close_fee = calc_side_fee(
close_price, lots, mult,
spec["close_today_fixed"], spec["close_today_ratio"],
)
else:
close_fee = calc_side_fee(
close_price, lots, mult,
spec["close_yesterday_fixed"], spec["close_yesterday_ratio"],
)
return round(open_fee + close_fee, 2)
def calc_fee_breakdown(
ths_code: str,
entry_price: float,
close_price: float,
lots: float,
open_time: str = "",
close_time: str = "",
trading_mode: str = "simulation",
) -> dict:
spec = get_fee_spec(ths_code, trading_mode=trading_mode)
mult = spec["mult"]
lots = lots or 1.0
open_fee = calc_side_fee(
entry_price, lots, mult, spec["open_fixed"], spec["open_ratio"],
)
same_day = is_same_day(open_time, close_time)
if same_day:
close_fee = calc_side_fee(
close_price, lots, mult,
spec["close_today_fixed"], spec["close_today_ratio"],
)
close_type = "平今"
else:
close_fee = calc_side_fee(
close_price, lots, mult,
spec["close_yesterday_fixed"], spec["close_yesterday_ratio"],
)
close_type = "平昨"
total = round(open_fee + close_fee, 2)
return {
"open_fee": round(open_fee, 2),
"close_fee": round(close_fee, 2),
"close_type": close_type,
"total_fee": total,
"same_day": same_day,
"fee_source": spec.get("source", "local"),
}
def load_fee_rates_from_json(path: Optional[str] = None) -> int:
path = path or DEFAULT_JSON
if not os.path.isfile(path):
return 0
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
conn = _get_db()
now = datetime.now().isoformat(timespec="seconds")
count = 0
for product, item in data.items():
if not isinstance(item, dict):
continue
conn.execute(
"""INSERT INTO fee_rates
(product, exchange, mult,
open_fixed, open_ratio,
close_yesterday_fixed, close_yesterday_ratio,
close_today_fixed, close_today_ratio, updated_at, source)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(product) DO UPDATE SET
exchange=excluded.exchange, mult=excluded.mult,
open_fixed=excluded.open_fixed, open_ratio=excluded.open_ratio,
close_yesterday_fixed=excluded.close_yesterday_fixed,
close_yesterday_ratio=excluded.close_yesterday_ratio,
close_today_fixed=excluded.close_today_fixed,
close_today_ratio=excluded.close_today_ratio,
updated_at=excluded.updated_at,
source=excluded.source""",
(
product.lower(),
item.get("exchange", ""),
int(item.get("mult") or get_contract_spec(product)["mult"]),
float(item.get("open_fixed") or 0),
float(item.get("open_ratio") or 0),
float(item.get("close_yesterday_fixed") or 0),
float(item.get("close_yesterday_ratio") or 0),
float(item.get("close_today_fixed") or 0),
float(item.get("close_today_ratio") or 0),
now,
item.get("source", "json"),
),
)
count += 1
conn.commit()
conn.close()
return count
def list_ctp_fee_rates() -> list:
"""手续费页:仅展示 CTP 同步结果。"""
conn = _get_db()
rows = conn.execute(
"SELECT * FROM fee_rates WHERE source='ctp' ORDER BY product"
).fetchall()
conn.close()
return [dict(r) for r in rows]
def list_all_fee_rates() -> list:
conn = _get_db()
rows = conn.execute(
"SELECT * FROM fee_rates ORDER BY product"
).fetchall()
conn.close()
return [dict(r) for r in rows]
def list_fee_rates_for_ui() -> list:
return list_ctp_fee_rates()
def count_fee_rates_by_source() -> dict[str, int]:
conn = _get_db()
n = conn.execute(
"SELECT COUNT(*) FROM fee_rates WHERE source='ctp'"
).fetchone()[0]
conn.close()
return {"ctp": int(n or 0)}
def upsert_fee_rate(product: str, fields: dict) -> None:
product = product.lower().strip()
conn = _get_db()
now = datetime.now().isoformat(timespec="seconds")
source = fields.get("source", "manual")
conn.execute(
"""INSERT INTO fee_rates
(product, exchange, mult,
open_fixed, open_ratio,
close_yesterday_fixed, close_yesterday_ratio,
close_today_fixed, close_today_ratio, updated_at, source)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(product) DO UPDATE SET
exchange=excluded.exchange, mult=excluded.mult,
open_fixed=excluded.open_fixed, open_ratio=excluded.open_ratio,
close_yesterday_fixed=excluded.close_yesterday_fixed,
close_yesterday_ratio=excluded.close_yesterday_ratio,
close_today_fixed=excluded.close_today_fixed,
close_today_ratio=excluded.close_today_ratio,
updated_at=excluded.updated_at,
source=excluded.source""",
(
product,
fields.get("exchange", ""),
int(fields.get("mult") or 10),
float(fields.get("open_fixed") or 0),
float(fields.get("open_ratio") or 0),
float(fields.get("close_yesterday_fixed") or 0),
float(fields.get("close_yesterday_ratio") or 0),
float(fields.get("close_today_fixed") or 0),
float(fields.get("close_today_ratio") or 0),
now,
source,
),
)
conn.commit()
conn.close()
+91
View File
@@ -0,0 +1,91 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
# 专有软件 — 未经授权禁止复制、传播、转售。
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
"""从第三方(AKShare)同步交易所参考手续费,并按倍率写入本地表。"""
import re
from typing import Any, Optional
from modules.core.contract_specs import get_contract_spec
from modules.fees.fee_specs import get_fee_multiplier, upsert_fee_rate
def _to_float(val: Any) -> float:
if val is None:
return 0.0
s = str(val).strip().replace(",", "")
if not s or s in ("-", "None", "nan"):
return 0.0
try:
return float(s)
except ValueError:
return 0.0
def _parse_akshare_row(row: dict, multiplier: float) -> Optional[dict]:
code = str(row.get("合约代码") or row.get("代码") or "").strip()
if not code:
return None
m = re.match(r"^([A-Za-z]+)", code)
if not m:
return None
product = m.group(1).lower()
open_ratio = _to_float(row.get("手续费标准-开仓-万分之")) / 10000.0
open_fixed = _to_float(row.get("手续费标准-开仓-元"))
if open_fixed == 0 and row.get("开仓"):
open_fixed = _to_float(row.get("开仓"))
close_y_ratio = _to_float(row.get("手续费标准-平昨-万分之")) / 10000.0
close_y_fixed = _to_float(row.get("手续费标准-平昨-元"))
if close_y_fixed == 0 and row.get("平昨"):
close_y_fixed = _to_float(row.get("平昨"))
close_t_ratio = _to_float(row.get("手续费标准-平今-万分之")) / 10000.0
close_t_fixed = _to_float(row.get("手续费标准-平今-元"))
if close_t_fixed == 0 and row.get("平今"):
close_t_fixed = _to_float(row.get("平今"))
mult = int(get_contract_spec(code)["mult"])
exchange = str(row.get("交易所名称") or row.get("交易所") or "").strip()
return {
"product": product,
"exchange": exchange,
"mult": mult,
"open_fixed": round(open_fixed * multiplier, 6),
"open_ratio": round(open_ratio * multiplier, 8),
"close_yesterday_fixed": round(close_y_fixed * multiplier, 6),
"close_yesterday_ratio": round(close_y_ratio * multiplier, 8),
"close_today_fixed": round(close_t_fixed * multiplier, 6),
"close_today_ratio": round(close_t_ratio * multiplier, 8),
"source": "akshare",
}
def sync_fees_from_akshare(multiplier: Optional[float] = None) -> tuple[int, str]:
multiplier = multiplier if multiplier is not None else get_fee_multiplier()
try:
import akshare as ak
except ImportError:
return 0, "未安装 akshare,请执行 pip install akshare 后重试,或使用默认费率表"
try:
df = ak.futures_comm_info(symbol="所有")
except Exception as exc:
return 0, f"拉取第三方数据失败: {exc}"
if df is None or df.empty:
return 0, "第三方返回空数据"
seen: set[str] = set()
count = 0
for _, series in df.iterrows():
row = series.to_dict()
parsed = _parse_akshare_row(row, multiplier)
if not parsed or parsed["product"] in seen:
continue
seen.add(parsed["product"])
upsert_fee_rate(parsed["product"], parsed)
count += 1
return count, f"已同步 {count} 个品种(标准费率 × {multiplier}"
+95
View File
@@ -0,0 +1,95 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""HTTP routes for fees module."""
from __future__ import annotations
from datetime import date, datetime
from flask import (
Response,
flash,
jsonify,
redirect,
render_template,
request,
send_file,
session,
stream_with_context,
url_for,
)
def register(deps) -> None:
app = deps.app
login_required = deps.login_required
require_nav = deps.require_nav
get_db = deps.get_db
get_setting = deps.get_setting
set_setting = deps.set_setting
fetch_price = deps.fetch_price
send_wechat_msg = deps.send_wechat_msg
touch_stats_cache = deps.touch_stats_cache
get_stats_data = deps.get_stats_data
build_market_quote_payload = deps.build_market_quote_payload
today_str = deps.today_str
expire_old_plans = deps.expire_old_plans
TZ = deps.tz
DB_PATH = deps.db_path
UPLOAD_DIR = deps.upload_dir
OPEN_TYPES = deps.open_types
EXIT_TRIGGERS = deps.exit_triggers
BEHAVIOR_TAGS = deps.behavior_tags
KLINE_PERIODS = deps.kline_periods
KLINE_CUTOFFS = deps.kline_cutoffs
calc_holding_duration = deps.calc_holding_duration
holding_to_minutes = deps.holding_to_minutes
classify_close_result = deps.classify_close_result
calc_rr_ratio = deps.calc_rr_ratio
calc_theoretical_pnl = deps.calc_theoretical_pnl
parse_review_date_filter = deps.parse_review_date_filter
_trading_mode = deps.trading_mode
_ua_is_phone = deps.ua_is_phone
_static_asset_v = deps.static_asset_v
from modules.fees.fee_specs import count_fee_rates_by_source, list_fee_rates_for_ui
@app.route("/fees", methods=["GET", "POST"])
@login_required
@require_nav("fees")
def fees():
from modules.core.trading_context import get_trading_mode
from modules.ctp.ctp_fee_worker import (
schedule_ctp_fee_sync,
get_fee_last_sync,
fees_synced_today,
fee_sync_in_progress,
)
from modules.ctp.vnpy_bridge import ctp_status
mode = get_trading_mode(get_setting)
if request.method == "POST":
action = request.form.get("action")
if action == "sync_ctp":
force = request.form.get("force") == "1"
_, msg = schedule_ctp_fee_sync(
mode,
get_setting=get_setting,
set_setting=set_setting,
force=force,
)
flash(msg)
return redirect(url_for("fees"))
rates = list_fee_rates_for_ui()
fee_counts = count_fee_rates_by_source()
ctp_st = ctp_status(mode)
return render_template(
"fees.html",
rates=rates,
fee_counts=fee_counts,
fee_last_sync=get_fee_last_sync(get_setting),
fee_synced_today=fees_synced_today(get_setting),
fee_sync_running=fee_sync_in_progress(),
ctp_connected=bool(ctp_st.get("connected")),
)