112 lines
4.0 KiB
Python
112 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import httpx
|
|
|
|
from backend.bootstrap.application import create_application
|
|
from backend.bootstrap.settings import Settings
|
|
from tests.support import run_scenario
|
|
from tests.test_accounts import (
|
|
ADMIN_PASSWORD,
|
|
USER_PASSWORD,
|
|
csrf_headers,
|
|
register,
|
|
use_session,
|
|
)
|
|
|
|
|
|
def _formula() -> dict:
|
|
return {
|
|
"universe": {"exclude_st": True, "listed_days_min": 120},
|
|
"filters": [{"field": "close", "op": ">", "value": 1}],
|
|
"score": [{"field": "close", "weight": 1, "direction": "desc"}],
|
|
"limit": 10,
|
|
"min_score": 0.5,
|
|
}
|
|
|
|
|
|
class FormulaProvider:
|
|
def stream(self, _profile, _messages):
|
|
yield json.dumps(
|
|
{
|
|
"universe": {"exclude_st": True, "listed_days_min": 120},
|
|
"filters": [{"field": "amount_billion", "op": ">=", "value": 3}],
|
|
"score": [
|
|
{"field": "return_20d", "weight": 60, "direction": "desc"},
|
|
{"field": "sector_strength", "weight": 40, "direction": "desc"},
|
|
],
|
|
"limit": 20,
|
|
"min_score": 55,
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
|
|
|
|
def test_screener_catalog_lock_and_custom_strategy_boundary(tmp_path) -> None:
|
|
application = create_application(Settings.for_test(tmp_path))
|
|
|
|
async def scenario(client: httpx.AsyncClient) -> None:
|
|
_, admin = await register(client, "admin-screener", ADMIN_PASSWORD)
|
|
client.cookies.clear()
|
|
_, regular = await register(client, "regular-screener", USER_PASSWORD)
|
|
|
|
catalog = await client.get("/api/screener/catalog")
|
|
assert catalog.status_code == 200
|
|
assert len(catalog.json()["factors"]) == 109
|
|
|
|
locked = await client.get("/api/screener?date=2026-07-30")
|
|
assert locked.status_code == 403
|
|
assert locked.json()["error"]["code"] == "membership_required"
|
|
rejected = await client.put(
|
|
"/api/screener/custom",
|
|
headers=csrf_headers(regular),
|
|
json={"name": "越权策略", "formula": _formula()},
|
|
)
|
|
assert rejected.status_code == 403
|
|
denied_compile = await client.post(
|
|
"/api/screener/formula/compile",
|
|
headers=csrf_headers(regular),
|
|
json={"description": "选择成交活跃且趋势较强的股票"},
|
|
)
|
|
assert denied_compile.status_code == 403
|
|
|
|
use_session(client, admin)
|
|
model = await client.post(
|
|
"/api/admin/models",
|
|
headers=csrf_headers(admin),
|
|
json={
|
|
"display_name": "公式模型",
|
|
"base_url": "https://model.example.com/v1",
|
|
"model_identifier": "formula-model",
|
|
"api_key": "formula-secret",
|
|
},
|
|
)
|
|
assert model.status_code == 201
|
|
application.state.container.llm._provider = FormulaProvider()
|
|
compiled = await client.post(
|
|
"/api/screener/formula/compile",
|
|
headers=csrf_headers(admin),
|
|
json={"description": "成交额至少3亿元,偏重20日走势和板块强度"},
|
|
)
|
|
assert compiled.status_code == 200
|
|
assert [item["weight"] for item in compiled.json()["formula"]["score"]] == [
|
|
0.6,
|
|
0.4,
|
|
]
|
|
assert compiled.json()["formula"]["min_score"] == 0.55
|
|
workspace = await client.get("/api/screener?date=2026-07-30")
|
|
assert workspace.status_code == 200
|
|
assert workspace.json()["trade_date"] is None
|
|
saved = await client.put(
|
|
"/api/screener/custom",
|
|
headers=csrf_headers(admin),
|
|
json={"name": "我的策略", "formula": _formula()},
|
|
)
|
|
assert saved.status_code == 200
|
|
assert saved.json()["name"] == "我的策略"
|
|
refreshed = await client.get("/api/screener?date=2026-07-30")
|
|
assert [item["name"] for item in refreshed.json()["custom_strategies"]] == ["我的策略"]
|
|
|
|
run_scenario(application, scenario)
|