Skip to content

Commit cbe8bbc

Browse files
committed
v5.0.4
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
1 parent cb2aa5c commit cbe8bbc

13 files changed

Lines changed: 727 additions & 70 deletions

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,5 @@
9292
# BACKEND_TAG=5.0.1
9393
#
9494
# Override the GHCR image paths (e.g. for forks):
95-
# BACKEND_IMAGE=ghcr.io/brokermr810/quantdinger-backend
96-
# FRONTEND_IMAGE=ghcr.io/brokermr810/quantdinger-frontend
95+
# BACKEND_IMAGE=ghcr.io/openbyteinc/quantdinger-backend
96+
# FRONTEND_IMAGE=ghcr.io/openbyteinc/quantdinger-frontend

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ This repository contains:
102102
- `backend_api_python/`: Flask backend + strategy runtime
103103
- `docker-compose.yml` / `docker-compose.ghcr.yml`: deployment stacks
104104

105-
The web UI source lives in the separate private **QuantDinger-Vue** repo, which publishes `ghcr.io/brokermr810/quantdinger-frontend` to GHCR on every `v*` tag — both Compose files pull that image directly.
105+
The web UI source lives in the separate private **QuantDinger-Vue** repo, which publishes `ghcr.io/openbyteinc/quantdinger-frontend` to GHCR on every `v*` tag — both Compose files pull that image directly.
106106

107107
### Backend (Python)
108108

@@ -115,7 +115,7 @@ python run.py
115115

116116
### Frontend
117117

118-
The SPA lives in the private **QuantDinger-Vue** repo. Tagging a release there (`git tag vX.Y.Z && git push --tags`) triggers `.github/workflows/release-frontend.yml`, which builds a multi-arch image and pushes it to `ghcr.io/brokermr810/quantdinger-frontend`. No frontend artefacts are committed here — pin the consumed tag via `IMAGE_TAG` (or `FRONTEND_TAG` for a per-side override) in a root-level `.env`.
118+
The SPA lives in the private **QuantDinger-Vue** repo. Tagging a release there (`git tag vX.Y.Z && git push --tags`) triggers `.github/workflows/release-frontend.yml`, which builds a multi-arch image and pushes it to `ghcr.io/openbyteinc/quantdinger-frontend`. No frontend artefacts are committed here — pin the consumed tag via `IMAGE_TAG` (or `FRONTEND_TAG` for a per-side override) in a root-level `.env`.
119119

120120
For local iteration without publishing, clone the Vue repo into `./QuantDinger-Vue/` (gitignored) and run `docker compose -f docker-compose.yml -f docker-compose.build.yml up --build` — see **DEVELOPMENT.md → Building frontend from local source**.
121121

DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ git tag v5.0.1
8181
git push origin v5.0.1
8282
```
8383

84-
The `release-frontend.yml` workflow there builds `linux/amd64 + linux/arm64` images via buildx and pushes them to `ghcr.io/brokermr810/quantdinger-frontend`, tagged with the semver value, `{major}.{minor}`, and `latest`.
84+
The `release-frontend.yml` workflow there builds `linux/amd64 + linux/arm64` images via buildx and pushes them to `ghcr.io/openbyteinc/quantdinger-frontend`, tagged with the semver value, `{major}.{minor}`, and `latest`.
8585

8686
This repo's `docker-compose.yml` (and `docker-compose.ghcr.yml`) references that image by default. To pin a version while testing:
8787

backend_api_python/app/services/ai_generation_contracts.py

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,27 @@
2121
2222
## Event model
2323
- CTA strategies implement `handle_data(context, data)`.
24-
- Portfolio strategies normally register `run_daily`, `run_weekly`, or `run_monthly` callbacks in `initialize` and rebalance inside the callback.
24+
- Single-symbol signal strategies normally implement `handle_data(context, data)`. Do not add a schedule unless the user requests one or the strategy is explicitly a periodic portfolio rebalance.
25+
- Portfolio strategies may register the global helpers `run_daily(callback, time="HH:MM")`, `run_weekly(callback, weekday=1, time="HH:MM")`, or `run_monthly(callback, monthday=1, time="HH:MM")` in `initialize` and rebalance inside the callback. These are runtime-bound global helpers; call them directly and never as `context.run_daily`, `context.run_weekly`, or `context.run_monthly`.
2526
- Optional lifecycle handlers are `before_trading_start(context, data)` and `after_trading_end(context, data)`.
2627
- Store per-run state on the global `g` namespace.
2728
- Confirm decisions from visible completed data only. Never read future rows, use negative shifts, or otherwise introduce look-ahead bias.
2829
2930
## Data and factors
30-
- Use `get_history(count, frequency, field, security_list)` or `history(...)` for historical bars.
31+
- Historical-bar signatures are exact: `get_history(count, frequency=None, field=None, security_list=None)` and `data.history(symbols, count, fields=None)`.
32+
- In `get_history(...)`, `count` is always the first argument and must be an integer. In `data.history(...)`, symbols are first and the integer count is second. Prefer explicit keywords when using `data.history`, for example `data.history(symbol, count=60, fields=["close"])`.
33+
- A history request for one symbol returns a pandas `DataFrame` directly. Use `bars["close"]`; never index the result again with `bars[symbol]`. Multiple-symbol requests return a dictionary keyed by canonical symbol.
3134
- Use `indicator(name, symbol, **params)`, `factor(name, symbol, **params)`, or `get_factors(symbols, names, **params)` for technical factors.
3235
- TA-Lib indicators and factors are available through the registered 129-function adapter; use canonical TA-Lib names and valid parameters.
3336
- Use `get_fundamentals(fields, symbols)` only for real point-in-time fundamental fields supported by the platform. Do not invent fields or use future reports.
3437
- Use `get_index_stocks(reference)` for dynamic index constituents.
3538
- Use `get_universe_stocks()` for the currently selected platform universe pool. Do not copy pool constituents into source code.
3639
3740
## Orders and positions
38-
- Use `order`, `order_value`, `order_target`, `order_target_value`, or `order_target_percent`.
39-
- Use `get_position(symbol)` or `get_positions(...)` to inspect holdings.
41+
- Order-helper signatures are exact: `order(symbol, amount)`, `order_value(symbol, value)`, `order_target(symbol, amount)`, `order_target_value(symbol, value)`, and `order_target_percent(symbol, percent)`.
42+
- These are runtime-bound global helpers. Never pass `context` as their first argument. Optional execution and protection values must be keyword arguments after the two required arguments.
43+
- `get_position(symbol)` returns a `Position` object. Read `position.amount`, `position.avg_cost`, and `position.last_price` directly; never use dictionary membership, subscripting, `.get(...)`, or `getattr(...)` on it.
44+
- Use `get_positions(...)` when a dictionary of multiple positions is required.
4045
- Values passed to value-based order APIs are quote-currency exposure targets. Keep sizing bounded by available capital and explicit allocation rules.
4146
- Keep long entry, long exit, short entry, and short exit conditions independent. A bearish long exit is not automatically a short entry.
4247
- Spot and all non-crypto markets are long-only for now.
@@ -54,17 +59,6 @@
5459
- Do not use `eval`, `exec`, `compile`, `open`, `getattr`, `setattr`, dunder access, or unsafe imports.
5560
"""
5661

57-
INDICATOR_TO_STRATEGY_CONTRACT = """# Indicator-to-Strategy API V2 conversion
58-
59-
- Convert the indicator's signal meaning into Strategy API V2 source with `initialize(context)` and executable handlers.
60-
- Remove chart-only `output`, plot, layer, and marker structures from the result.
61-
- Preserve event algebra and recursive indicator semantics without look-ahead.
62-
- Preserve the source timeframe in `context.subscribe(...)` when it is declared by the source; otherwise choose a conservative strategy-owned default.
63-
- Map an explicit bullish entry to a long entry and an explicit bearish exit to a long exit. Do not invent short, leverage, reversal, grid, DCA, or martingale behavior.
64-
- Add short logic only when the user explicitly requests it and supplies a distinct bearish entry rule.
65-
- Keep visual-only colors, label offsets, and layout parameters out of executable code.
66-
"""
67-
6862
SCRIPT_STRATEGY_QUICK_TOOL_SYSTEM_PROMPT = SCRIPT_STRATEGY_SYSTEM_PROMPT + """
6963
7064
# Homepage quick-tool entry
@@ -73,24 +67,17 @@
7367
- Do not return a research memo, checklist, or pseudo-code.
7468
"""
7569

76-
INDICATOR_TO_STRATEGY_SYSTEM_PROMPT = (
77-
SCRIPT_STRATEGY_SYSTEM_PROMPT
78-
+ "\n\n"
79-
+ INDICATOR_TO_STRATEGY_CONTRACT
80-
+ """
81-
82-
# Indicator conversion entry
83-
- The generated source may be saved directly and must compile as Strategy API V2.
84-
- Preserve the source indicator's visible signal meaning before adding execution behavior.
85-
"""
86-
)
87-
8870
SCRIPT_STRATEGY_REPAIR_REQUIREMENTS = """# Strategy API V2 repair requirements
8971
- Return Python source only.
9072
- Require a metadata docstring and `initialize(context)`.
9173
- Require a source-owned universe and subscription.
9274
- Require at least one executable handler or registered schedule callback.
9375
- Use only Strategy API V2 data, factor, fundamental, position, and order APIs.
76+
- Prefer `handle_data(context, data)` for single-symbol signal strategies. Use schedules only for an explicitly requested schedule or periodic portfolio rebalance.
77+
- Schedule helpers are global calls: `run_daily(callback, time="HH:MM")`, `run_weekly(callback, weekday=1, time="HH:MM")`, and `run_monthly(callback, monthday=1, time="HH:MM")`. Never call them through `context`.
78+
- Enforce exact history signatures: `get_history(count, frequency, field, security_list)` and `data.history(symbols, count, fields)`. A single-symbol result is already a DataFrame.
79+
- Enforce exact order signatures such as `order_target_percent(symbol, percent)` and never pass `context` to a global order helper.
80+
- Treat `get_position(symbol)` as a `Position` object with direct `.amount`, `.avg_cost`, and `.last_price` attributes. Never treat it as a dictionary or use `getattr`.
9481
- Preserve completed-data-only execution and remove look-ahead.
9582
- Keep symbol, market, frequency, schedule, and universe in source code.
9683
- Permit user-adjustable leverage only for Crypto `@swap` instruments and only after `context.allow_leverage(max_leverage=N)`.

backend_api_python/app/services/llm.py

Lines changed: 128 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@
1717
logger = get_logger(__name__)
1818

1919

20+
class LLMAPIError(ValueError):
21+
"""Provider HTTP error with status and request metadata preserved."""
22+
23+
def __init__(self, message: str, *, status_code: int, request_id: str = ""):
24+
super().__init__(message)
25+
self.status_code = status_code
26+
self.request_id = request_id
27+
28+
2029
class LLMProvider(Enum):
2130
"""Supported LLM providers"""
2231
OPENROUTER = "openrouter"
@@ -305,18 +314,22 @@ def _call_openai_compatible(self, messages: list, model: str, temperature: float
305314

306315
# Handle non-2xx with provider/model-aware details
307316
if response.status_code >= 400:
308-
provider_name = "OpenRouter" if "openrouter" in (base_url or "").lower() else "LLM"
309-
error_msg = f"{provider_name} API {response.status_code}"
310-
err_text = ""
311-
try:
312-
error_data = response.json() or {}
313-
error_detail = error_data.get("error")
314-
if isinstance(error_detail, dict):
315-
err_text = str(error_detail.get("message") or "").strip()
316-
elif isinstance(error_detail, str):
317-
err_text = error_detail.strip()
318-
except Exception:
319-
err_text = (response.text or "").strip()[:300]
317+
normalized_base_url = (base_url or "").lower()
318+
if "atlascloud" in normalized_base_url:
319+
provider_name = "AtlasCloud"
320+
elif "openrouter" in normalized_base_url:
321+
provider_name = "OpenRouter"
322+
else:
323+
provider_name = "LLM"
324+
err_text = self._extract_provider_error(response)
325+
request_id = self._provider_request_id(response)
326+
metadata = [f"model={model}"]
327+
if request_id:
328+
metadata.append(f"request_id={request_id}")
329+
error_msg = (
330+
f"{provider_name} API {response.status_code} "
331+
f"({', '.join(metadata)})"
332+
)
320333

321334
if err_text:
322335
error_msg = f"{error_msg}: {err_text}"
@@ -331,7 +344,11 @@ def _call_openai_compatible(self, messages: list, model: str, temperature: float
331344
elif response.status_code == 404:
332345
error_msg += ". 可能原因:模型不可用或账户隐私/数据策略限制。请检查 https://openrouter.ai/settings/privacy"
333346

334-
raise ValueError(error_msg)
347+
raise LLMAPIError(
348+
error_msg,
349+
status_code=response.status_code,
350+
request_id=request_id,
351+
)
335352

336353
result = response.json()
337354
if "choices" in result and len(result["choices"]) > 0:
@@ -342,6 +359,64 @@ def _call_openai_compatible(self, messages: list, model: str, temperature: float
342359
else:
343360
raise ValueError("API response is missing 'choices'")
344361

362+
@staticmethod
363+
def _provider_request_id(response) -> str:
364+
headers = getattr(response, "headers", None) or {}
365+
for name in (
366+
"x-request-id",
367+
"request-id",
368+
"x-correlation-id",
369+
"cf-ray",
370+
):
371+
value = headers.get(name) or headers.get(name.title())
372+
if value:
373+
return str(value).strip()[:200]
374+
return ""
375+
376+
@classmethod
377+
def _extract_provider_error(cls, response) -> str:
378+
payload = None
379+
try:
380+
payload = response.json()
381+
except Exception:
382+
pass
383+
384+
detail = cls._format_provider_error_value(payload)
385+
if not detail:
386+
detail = str(getattr(response, "text", "") or "").strip()
387+
return " ".join(detail.split())[:1000]
388+
389+
@classmethod
390+
def _format_provider_error_value(cls, value) -> str:
391+
if isinstance(value, str):
392+
return value.strip()
393+
if isinstance(value, list):
394+
parts = [cls._format_provider_error_value(item) for item in value]
395+
return "; ".join(part for part in parts if part)
396+
if not isinstance(value, dict):
397+
return ""
398+
399+
parts = []
400+
location = value.get("loc") or value.get("location")
401+
if isinstance(location, (list, tuple)):
402+
location = ".".join(str(item) for item in location)
403+
if location:
404+
parts.append(str(location).strip())
405+
406+
for key in ("error", "message", "msg", "detail", "reason"):
407+
if key not in value:
408+
continue
409+
text = cls._format_provider_error_value(value.get(key))
410+
if text and text not in parts:
411+
parts.append(text)
412+
413+
if not parts:
414+
for key in ("code", "type", "status"):
415+
item = value.get(key)
416+
if isinstance(item, (str, int, float)) and str(item).strip():
417+
parts.append(f"{key}={item}")
418+
return ": ".join(parts)
419+
345420
def _call_google_gemini(self, messages: list, model: str, temperature: float,
346421
api_key: str, base_url: str, timeout: int) -> str:
347422
"""Call Google Gemini API."""
@@ -707,6 +782,46 @@ def call_llm_api(self, messages: list, model: str = None, temperature: float = 0
707782
use_json_mode=use_json_mode
708783
)
709784

785+
except LLMAPIError as e:
786+
status_code = e.status_code
787+
last_status_code = status_code
788+
last_error = str(e)
789+
logger.warning(
790+
"%s API HTTP error (%s): %s",
791+
p.value,
792+
current_model,
793+
e,
794+
)
795+
796+
if (
797+
status_code in (402, 403)
798+
and try_alternative_providers
799+
and current_model == models_to_try[-1]
800+
):
801+
logger.warning(
802+
"%s returned %s. Trying alternative providers...",
803+
p.value,
804+
status_code,
805+
)
806+
return self._try_alternative_providers(
807+
messages,
808+
original_model,
809+
temperature,
810+
use_json_mode,
811+
excluded_provider=p,
812+
)
813+
814+
if not use_fallback or current_model == models_to_try[-1]:
815+
raise
816+
817+
logger.warning(
818+
"%s returned %s for model %s; trying fallback model...",
819+
p.value,
820+
status_code,
821+
current_model,
822+
)
823+
continue
824+
710825
except requests.exceptions.HTTPError as e:
711826
error_detail = e.response.text if e.response else str(e)
712827
status_code = e.response.status_code if e.response else None

0 commit comments

Comments
 (0)