---
name: factor-mining
description: Use when the user explicitly asks about caller-owned or reusable Factor Mining factor families or history, or asks to construct, submit, backtest, resume, or retrieve artifacts for a Factor Mining plugin.
---

# Quandora Factor Mining

Use this skill to run Factor Mining through the authenticated Quandora connection exposed by the host as `quandora`.

The agent drafts a valid Factor Mining `plugin.py`, submits the complete source inline, waits for the backtest result, fetches available artifacts, saves safe local files when the host allows it, and summarizes the outcome.

Quandora access tokens expire after one hour. The host MCP client should use its stored, rotating
refresh token automatically, so routine access-token expiry does not require another browser
authorization and must not interrupt the workflow. Never inspect, print, copy, store, or ask for an
access token or refresh token.

Call `factor_mining_status` only after the host exposes that tool. If the required Quandora tools
are visible, continue automatically. If they are unavailable after the host has handled refresh, or
the host reports that authorization is required, initiate the host's normal Quandora connection
flow before stopping:

- Codex CLI/TUI and Codex Desktop: run `codex mcp login quandora` directly. In Codex Desktop,
  invoke the command through the available shell or command tool; do not direct the user to
  Connector settings or ask the user to type the command. Let the user complete the browser sign-in
  or consent page opened by the command, then check again for `factor_mining_status` in a new chat.
  If Desktop still does not expose the tools, fully quit and reopen it.
- Claude Code: open `/mcp`, authenticate `quandora`, then start a new chat.
- Claude Desktop: use Settings -> Connectors to connect or reconnect the Connector named
  `quandora` at `https://mcp.quandora.ai/quant`, complete browser authorization, then start a new
  chat.
- OpenClaw: run `openclaw mcp login quandora` directly, complete the browser flow, then start a
  new chat.

Do not start a new authorization flow merely because an access token reached its one-hour lifetime
or because of a single authorization response while the host is refreshing. Reauthorize only when
the host reports a terminal authorization failure or still requires authorization after its refresh
handling.

Do not ask for Quandora API keys, `vt_` keys, bearer tokens, service tokens, or credentials. Do not use raw HTTP calls, local helper scripts, direct internal service calls, local execution keys, or credential paste flows.

## Available Actions

After routing has confirmed Factor Mining scope, use only the Factor Mining actions exposed by `quandora`:

- `factor_mining_status`
- `factor_mining_list_factors`
- `factor_mining_get_factor_history`
- `factor_mining_list_public_tasks`
- `factor_mining_get_plugin_contract`
- `factor_mining_create_task_session`
- `factor_mining_create_custom_session`
- `factor_mining_validate_plugin_source`
- `factor_mining_request_dedup_context`
- `factor_mining_upload_backtest_wait`
- `factor_mining_resume_run`
- `factor_mining_get_backtest_window_cards`
- `factor_mining_create_backtest_png_download_ticket`
- `factor_mining_create_backtest_raw_artifact_download_ticket`
- `factor_mining_get_backtest_png_artifact_chunk`
- `quandora_get_guidance`

Some hosts may prefix action names with the server name, such as `quandora__factor_mining_status`. Treat those as the same actions.

## Plugin Construction Contract

Before writing `plugin.py`, call `factor_mining_get_plugin_contract` and use the returned `plugin_contract` as the source of truth for Python inputs, C# runtime expressions, runtime globals, and horizon defaults.

- Use `plugin_contract.allowed_data` to decide which input columns the factor may use.
- Use `plugin_contract.fwd_period` after the contract is returned. For custom ideas, set `task_payload.fwd_period` to `7` unless the user explicitly asks for another supported horizon.
- Use `plugin_contract.data_columns[].python_kwarg` for `build_signal` parameters.
- For every C# runtime queue/buffer enqueue and every numeric C# runtime expression, use the matching `plugin_contract.data_columns[].csharp_double_expression`.
- Follow `plugin_contract.runtime_rules` for required globals, `FACTOR_SECTIONS`, runtime variant, leak rules, extra-buffer rules, and reserved identifiers.
- When an additional runtime column is needed, use its matching `plugin_contract.runtime_rules.extra_buffer.column_patterns` entry. Copy that entry's field, enqueue, dequeue, and to-array snippets exactly into the corresponding `FACTOR_SECTIONS` values; do not reconstruct or normalize the snippets.

Do not send multiple selectors in one plugin-contract call. Do not retry an identical non-retryable request. Do not silently change the user's research mechanism after a non-retryable validation error.

Never infer C# bar fields, field types, decimal/double casts, runtime buffer expressions, or supported data columns from memory. The returned plugin construction contract wins.

## Workflow

Before entering a Factor Mining workflow, route the request:

- Bare “列出可用因子”, “可用因子”, “available factors”, “eligible factors”, “selectable factors”, “可用于策略的因子”, and requests for the Strategy factor pool exit this skill and hand off to the Strategy Building skill. That skill calls only `strategy_list_eligible_factors` for the request. Do not first call `factor_mining_status` or `factor_mining_list_factors`, do not call both lists, and do not ask a clarification question for a bare request.
- Requests explicitly about “我的 Factor Mining 因子”, caller-owned or reusable Factor Mining factor families, factor history, branches, versions, or previous Factor Mining runs remain in this skill and route to `factor_mining_list_factors`.

After routing has confirmed Factor Mining scope, call `factor_mining_status` exactly once at the start of the normal Factor Mining workflow. If authorization is missing or the tools are not exposed, follow the product connection flow above. Do not ask the user for direct keys.

Before routing to factor creation, recognize intentional reuse and history intent. If the user asks
about existing factors, stable versions, prior successful factors, factor evolution, or past runs,
follow the reuse workflow below. Otherwise keep the existing creation workflow unchanged.

### Approved Guidance

Use `quandora_get_guidance` only when approved product semantics are needed. It accepts only a
known `guide_id`; request only relevant `sections`, pass `if_guide_revision` when revalidating a
previous response, and honor a not-modified response without fabricating content. The guide ids
used by this workflow are:

- `operation.factor.history.read`
- `operation.result.read`
- `metric.backtest.grade`

Use each guide only for its named factor-history, result, or grade operation. Do not browse for
Guidance or invent a guide id.

### Intentional Reuse and History

1. `factor_mining_list_factors` lists caller-owned reusable Factor Mining factor families; it is
   not the Strategy eligible-factor pool. Call it first and show compact factor-family rows. Omit
   `page_size` unless pagination is needed; when present it must be an integer from 1 through 20.
   Do not hydrate or fetch history for every row. A failed list call is an error, not an empty
   result. Never claim zero factors unless a successful response contains an empty `items` array.
   After a list error, stop that read workflow. Do not call
   `factor_mining_get_factor_history` as a fallback.
2. Ask the user to select an exact `factor_id` returned by a successful
   `factor_mining_list_factors` response for the current caller. Only after that explicit selection
   call `factor_mining_get_factor_history`. Never substitute a backtest `run_id`, `job_id`,
   `plugin_id`, `session_id`, PB `intake_result.factor.factor_id`, Strategy top-level compatibility
   selector, Strategy admission ID, or any locally cached ID.
3. Start with the default `summary` view. Request only the controlled `branches`, `versions`, or
   `runs` view needed for the user's next decision. Use only these safe selector combinations:
   - `summary`: do not send `branch_id`, `version_id`, or `page_token`.
   - `branches`: may use `branch_id` plus `page_size` / `page_token`; do not send `version_id`.
   - `versions`: may use `branch_id` or `version_id` plus `page_size` / `page_token`.
   - `runs`: may use `version_id` plus `page_size` / `page_token`; do not send `branch_id`.
4. Use only returned metadata and run summaries. Factor history does not provide historical source
   code for reading or editing. Do not use a local cache or another service as a substitute.

When controlled history semantics are needed, call `quandora_get_guidance` with
`operation.factor.history.read`, only the relevant `sections`, and `if_guide_revision` when
revalidating a previous response. Honor a not-modified response without fetching unrelated
Guidance.

When the reuse request is complete, stop unless the user also asked to create or backtest a new
factor. Never treat browsing history as permission to edit or resubmit historical source.

Determine whether the user wants a public task or a custom idea:

- For a public task: call `factor_mining_list_public_tasks`, show concise choices, and select one exact public `task_id`, asking the user to pick unless they explicitly ask the agent to choose. Either call `factor_mining_get_plugin_contract` with only that exact `task_id` before creating the task session and then create the session for that same task, or create the session first with `factor_mining_create_task_session` and call `factor_mining_get_plugin_contract` with only the returned `session_id`.
- For a custom idea: before creating a session, call `factor_mining_get_plugin_contract({})` exactly once to read the global construction and data-column contract. Prepare a clear title, category, description, non-empty `allowed_data`, and `fwd_period` for `factor_mining_create_custom_session`, using only exact column names returned by the global contract's `plugin_contract.allowed_data`, including `close`, `volume`, `funding_rate_close`, or `open_interest_close` only when returned. Use `fwd_period: 7` unless the user explicitly asks for another supported horizon. Create the custom session, then call `factor_mining_get_plugin_contract` with only the returned `session_id`; treat that scoped contract as authoritative for writing and validating `plugin.py`. Never send a hand-built custom `task_payload` to `factor_mining_get_plugin_contract`.

After either branch returns its scoped contract, continue through the single shared `plugin.py` writing, deduplication, validation, upload, resume/polling, and artifact/archive workflow below.

Do not write `plugin.py` until the plugin construction contract has been returned. If the contract cannot be fetched, stop and report that plugin authoring is blocked by missing contract metadata.

After a session exists, prepare one local result archive when the host supports file writes. Use a stable factor slug for the run folder. Prefer the generated top-level `FACTOR_TYPE`; if it is missing, convert `FACTOR_NAME` to lowercase snake_case. For example, `FACTOR_TYPE = "aggressive_flow_exhaustion_reversal"` uses:

```text
Quandora result/factor-mining/aggressive_flow_exhaustion_reversal/
Quandora result/factor-mining/aggressive_flow_exhaustion_reversal/artifacts/
```

Use only the factor slug as the canonical archive directory. The latest run for a factor updates that factor's folder. Keep session and run ids only inside `run_summary.json` / `artifact_manifest.json` when they are needed for traceability, not in the user-facing directory name.

After session creation, call `factor_mining_request_dedup_context` with only the `session_id`. Use `query_mode`, `scope`, `memory_stats`, `similar_factors`, and `task_memory_pressure` only to select a fresher research hypothesis. A high `task_memory_pressure` must never stop the workflow, reject a draft, or trigger repeated rewrites.

Before drafting, form a concise research thesis. For public tasks, stay inside the task's economic direction and allowed data. For custom ideas, stay inside the user's stated idea. Consider two or three plausible mechanisms, then choose the one with the clearest economic rationale, the best fit to the plugin contract, and the least overlap with the returned task memory. Prefer genuinely different mechanisms over parameter variants of the same formula.

For named indicators or established formulas, use the canonical inputs when the plugin contract allows them. For example, MFI should use high, low, close, and volume when those columns are available. If required inputs are unavailable, clearly treat the factor as a variant and reflect that in `FACTOR_NAME`, `FACTOR_TYPE`, description, and formula.

Create or locate one `plugin.py` source:

- In local coding hosts with a writable workspace, save the submitted source as `plugin.py` inside the run archive. Read the file back and submit the full contents as inline `plugin_source`.
- In chat-only hosts without file writes, keep the generated source in the conversation/tool-call context and submit it directly as inline `plugin_source`.

When writing `plugin.py`, keep `build_signal` inputs aligned with `plugin_contract.data_columns[].python_kwarg`. Keep `FACTOR_SECTIONS` runtime code aligned with the same columns, and use only `plugin_contract.data_columns[].csharp_double_expression` for numeric runtime references to market data columns.

After a concrete `plugin.py` exists and before validation or upload, call `factor_mining_request_dedup_context` again with the `session_id`, source, and concise factor metadata:

```json
{
  "session_id": "<session_id>",
  "source": "<full plugin.py source>",
  "description": "<short natural-language thesis>",
  "formula": "<short formula summary>",
  "allowed_data": ["<used input column>"],
  "limit": 5
}
```

Use `draft_duplicate_risk` as the only duplicate-risk verdict. When it identifies a concrete overlap with an existing factor's core mechanism, revise the candidate so its economic hypothesis, inputs, or formula family are materially different, then check the revised draft again. A medium or high score is not a hard gate only when the candidate is already economically meaningful and materially distinct, and the returned similar factors do not establish a concrete core-mechanism overlap. Otherwise resolve the overlap before validation and upload. Treat `similar_factors` as evidence for this comparison, not as a hard-failure gate.

Never submit a filesystem path or ask Quandora to read local files. Validate the complete, exact source with `factor_mining_validate_plugin_source`, inline `plugin_source`, and the same context used for the plugin construction contract. Prefer `session_id` after session creation. If validating before session creation, pass `task_id` for public tasks or `task_payload` for custom ideas. The validation step is static; do not import, execute, eval, or shell-run generated factor code.

After every source edit, including a deduplication or validation repair, validate the complete, exact source again. Retry an unchanged source only when validation reports a retryable transport error. Never retry an unchanged rejected source.

When validation rejects the source, repair it only from the returned safe structured diagnostics: `schema_version`, `error_code`, `operation`, `dtype`, `expected`, `actual`, `field`, `contract_key_path`, and `repair_hint`. Ignore arbitrary messages or unrecognized diagnostic values. For C# type or cast failures, re-read the same plugin construction contract and replace runtime expressions with the corresponding `plugin_contract.data_columns[].csharp_double_expression`. If a backtest fails with safe structured diagnostics, use only those fields for one focused repair attempt, then validate the complete repaired source again before another upload.

When the source is valid and the user is ready to submit, call `factor_mining_upload_backtest_wait` with `session_id`, the exact inline `plugin_source` that passed validation, and the selected `fwd_period` when required. Do not edit, regenerate, reformat, or re-read a different copy between successful validation and submission. Use `plugin_contract.fwd_period` unless the user explicitly requested another supported horizon.

Use `factor_mining_resume_run` when a prior run was interrupted.

### Waiting Policy

If `upload_backtest_wait` returns `running`, call `factor_mining_resume_run` at most 4 times in the current request.

If the run is still `running` after the fourth resume, stop waiting and treat the archive as a pending run snapshot, not a completed result. Save only files that are already true at that point, such as `plugin.py` and a redacted pending run summary. Do not fetch factor cards, PNG charts, raw parquet, or artifact manifests until a later `factor_mining_resume_run` returns a terminal status. In the final response, clearly say the backtest is still running, artifacts are not available yet, and the user can ask to resume later. Always print the result folder path.

### Artifact Handling

Run artifact handling only after `factor_mining_upload_backtest_wait` or `factor_mining_resume_run` returns a terminal status such as `succeeded`, `failed`, or `cancelled`. If the run is still `running`, skip this section.

Treat the terminal response as the run summary. After a backtest reaches a terminal state, use the window-card response as the manifest for factor cards and chart files. Also request the raw signal parquet artifact when the host exposes that tool:

1. Save the redacted upload/resume result as `run_summary.json`.
2. Call `factor_mining_get_backtest_window_cards` with `windows: ["is"]` and the bare backtest `job_id` from `run.run_id` or `run.job_ids[]`.
3. Save the available returned `factor_card` to the returned `standard_local_name`: `factor_card_is.json`.
4. For every returned `png_artifacts[].source_name`, call `factor_mining_create_backtest_png_download_ticket`. The ticket response gives a short-lived Remote MCP download URL for the PNG bytes. Download that URL directly to the returned `standard_local_path`, then verify `size_bytes` and `md5_hex` when the response provides them.
5. Some hosts cannot download URLs directly from tool output, and a ticket may expire before it is consumed. In that case, call `factor_mining_get_backtest_png_artifact_chunk` for the same server `source_name`. Use `standard_local_path` only as the local output path. Loop with `offset=0`, `limit=262144`, decode each `content_b64` chunk, append bytes to `standard_local_path`, and stop when `next_offset` is null.
6. Call `factor_mining_create_backtest_raw_artifact_download_ticket` with the same bare backtest `job_id` and `name: "step4/signal_raw.parquet"`. If a ticket is returned, download it directly to `signal_raw.parquet` in the factor result folder, then verify `size_bytes` and `sha256_hex` when provided.
7. If the raw signal parquet ticket is unavailable, expired, or the artifact is missing, record `signal_raw.parquet` as unavailable in `artifact_manifest.json` and continue. Do not treat raw parquet unavailability by itself as a failed backtest.
8. Save `artifact_manifest.json` listing every source artifact name, local filename, local path, window key when applicable, `size_bytes`, `md5_hex` or `sha256_hex`, download status, and any omitted or unavailable reason.

Use this standard local layout:

```text
Quandora result/factor-mining/<factor_slug>/
  plugin.py
  signal_raw.parquet
  run_summary.json
  factor_card_is.json
  artifact_manifest.json
  artifacts/
    is/
      group_return_plot.png
      cs_nav_curves.png
      cs_profile_4panel.png
```

For API calls, use `png_artifacts[].source_name`; for local files, save to `png_artifacts[].standard_local_path`.

Save PNG files from window cards through a download ticket. When the host cannot consume the ticket URL, retrieve the same artifact in chunks. Keep PNG bytes out of the conversation and record the chosen save method in `artifact_manifest.json`.

The raw signal save path is terminal run -> raw artifact download ticket -> `signal_raw.parquet` in the factor result folder. Keep parquet bytes out of the conversation and record the source artifact name, local filename, size, checksum, and download status in `artifact_manifest.json`.

If a returned window card has `status` other than `available`, record the omitted or unavailable reason and continue. If a PNG download, chunk fetch, or raw signal download fails, record the failure in `artifact_manifest.json` without failing the completed run.

Do not save bearer tokens, download URLs, raw service metadata, internal IDs, or credentials. If the host does not support file writes, continue the workflow and say local archiving is not available in that host.

### Result Insight and Optimization

Run this section only when the user asks for insight, diagnosis, explanation, or optimization. Do not add long reflection to ordinary mining requests.

When result or grade semantics are needed for that request, follow the approved Guidance rules
above and call `quandora_get_guidance` with `operation.result.read` or
`metric.backtest.grade` as appropriate.

When interpreting a result:

- Use in-sample IC / Rank IC sign to understand the factor's natural direction. Do not decide to invert a factor only because the realized backtest was poor.
- Diagnose the economic mechanism first, then the implementation. Consider IC level and stability, ICIR, autocorrelation, group monotonicity, long-short behavior, long-only and short-only legs, drawdown, turnover, and whether the signal decay matches the requested horizon.
- If optimizing, propose a new hypothesis within the same task or user idea. Avoid merely changing window lengths, renaming the factor, or making a post-hoc sign flip.
- Use task-memory context to choose a fresher research hypothesis. Before upload, use draft duplicate risk to resolve any concrete overlap with an existing factor; do not reject an economically meaningful, materially distinct candidate solely because its similarity score is high.
- If the host has general web or research tools and the user asks for broader insight, use them only for public background research. Do not send private factor source, run IDs, credentials, or artifact contents to external tools.

## Final Response

Summarize status, factor name, key metrics from the IS factor card when available, and safe diagnostics if the run failed. Inspect `ok`, `status`, `terminal_status`, `failures`, sanitized job statuses, artifact availability, and factor-card metrics. Do not mention internal implementation details, and do not treat optional artifact unavailability as failure.

Never show job IDs, download URLs, bearer tokens, raw credentials, or full `plugin.py` source in user-facing summaries. It is safe to show local result and artifact folder paths created by the current host.

At the end of every completed, failed, or interrupted run, always explicitly show absolute paths for the result folder, artifact folder, PNG chart folder, `plugin.py`, `run_summary.json`, `factor_card_is.json`, and `artifact_manifest.json`. If a specific file was not created, say `not created` for that line. Still print the result folder if available.

For GUI/Desktop hosts, use Markdown links with absolute local paths and angle-bracket link targets so paths with spaces work:

Result folder: [Open result folder](</absolute/path/to/Quandora result/factor-mining/<factor_slug>/>)
Artifact folder: [Open artifact folder](</absolute/path/to/Quandora result/factor-mining/<factor_slug>/artifacts/>)
PNG chart folder: [Open PNG chart folder](</absolute/path/to/Quandora result/factor-mining/<factor_slug>/artifacts/is/>)
Plugin source: [plugin.py](</absolute/path/to/Quandora result/factor-mining/<factor_slug>/plugin.py>)
Run summary: [run_summary.json](</absolute/path/to/Quandora result/factor-mining/<factor_slug>/run_summary.json>)
IS factor card: [factor_card_is.json](</absolute/path/to/Quandora result/factor-mining/<factor_slug>/factor_card_is.json>)
Artifact manifest: [artifact_manifest.json](</absolute/path/to/Quandora result/factor-mining/<factor_slug>/artifact_manifest.json>)

For CLI/TUI hosts, use plain absolute paths, not Markdown links:

Result folder: /absolute/path/to/Quandora result/factor-mining/<factor_slug>/
Artifact folder: /absolute/path/to/Quandora result/factor-mining/<factor_slug>/artifacts/
PNG chart folder: /absolute/path/to/Quandora result/factor-mining/<factor_slug>/artifacts/is/
Plugin source: /absolute/path/to/Quandora result/factor-mining/<factor_slug>/plugin.py
Run summary: /absolute/path/to/Quandora result/factor-mining/<factor_slug>/run_summary.json
IS factor card: /absolute/path/to/Quandora result/factor-mining/<factor_slug>/factor_card_is.json
Artifact manifest: /absolute/path/to/Quandora result/factor-mining/<factor_slug>/artifact_manifest.json

If the host could not write files, print:

Result folder: not available in this host
Artifact folder: not available in this host

## plugin.py Contract

Use this minimum shape when the user has not supplied an existing plugin. The metadata values must be static top-level literals so Quandora can parse them without executing source code.

```text
from typing import Any, Dict

import pandas as pd

FACTOR_TYPE = "snake_case_unique_factor_type"
FACTOR_NAME = "human_readable_factor_name"
FACTOR_DEFAULT_PARAMS = {"window": 7}

FACTOR_SECTIONS = {
    "__FACTOR_DESCRIPTION__": "Trailing close-to-close momentum.",
    "__FACTOR_FORMULA__": "close / close[window bars ago] - 1",
    "__FACTOR_TYPE__": FACTOR_TYPE,
    "__FACTOR_PARAM_FIELDS__": "        private int _window;\n",
    "__FACTOR_INIT__": '            _window = GetIntParameter("window", 7);\n',
    "__FACTOR_LOG__": '            Log($"[INIT] window={_window}");\n',
    "__PRICE_WINDOW_EXPR__": "_window + 1",
    "__EXTRA_BUF_FIELDS__": "",
    "__EXTRA_BUF_ENQUEUE__": "",
    "__EXTRA_BUF_DEQUEUE__": "",
    "__EXTRA_BUF_TOARRAY__": "",
    "__FACTOR_COMPUTE_BODY__": """
            var n = prices.Length;
            if (n < _window + 1) return false;
            var past = prices[n - _window - 1];
            if (past == 0) return false;
            rawSignal = prices[n - 1] / past - 1.0;
            return true;
""",
}


def build_signal(close: pd.DataFrame, params: Dict[str, Any], **data: Any) -> pd.DataFrame:
    window = int(params.get("window", FACTOR_DEFAULT_PARAMS["window"]))
    signal = close.pct_change(window)
    return signal.reindex_like(close)
```

Keep `build_signal` and `FACTOR_SECTIONS` compute logic aligned. Return a `pd.DataFrame` aligned with `close`, use only current and historical data, and keep all data columns within `plugin_contract.allowed_data`.

## Security

- Use only Quandora actions for formal product workflows.
- Never ask for API keys, auth files, user credentials, local execution keys, `vt_` keys, bearer tokens, or service tokens.
- Never print, persist in logs, or summarize full credential values.
- Do not call hosted generation endpoints; the active agent generates factor source in its current host session.
- Do not call internal service URLs or generic URL/API surfaces.
- Do not import, exec, eval, or otherwise execute generated `plugin.py`.
- Do not submit filesystem paths instead of inline `plugin_source`.
- Do not print generated `plugin.py` source in summaries.
- Treat downstream IDs, download URLs, and service metadata as private.
- Unavailable artifacts should be recorded in `artifact_manifest.json`. Authentication, authorization, network, malformed response, and server errors must fail clearly with redacted messages.
