> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noonum.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Iterate a strategy

> Refine a Noonum strategy with Python. Enhance the objective, review holdings and evidence, exclude what you don't want, resubmit, and generate historical results.

This guide continues from [Build a strategy](/v2/guides/build-strategy) and covers the refinement loop:

1. Start with a rough objective.
2. Enhance the objective with `POST /enhance-objective`.
3. Create and submit a strategy.
4. Poll until processing is complete (`status == 100`).
5. Review holdings.
6. Look up a company id from a symbol with `POST /helper/companies/search`.
7. Fetch reasoning and evidence for a company.
8. Craft exclusion phrases grounded in the holdings, then `PATCH` them onto the strategy.
9. Resubmit and repeat. Optionally generate an inverse objective with `POST /reverse-objective` and run the same loop.

[Build a strategy](/v2/guides/build-strategy) shows this loop as curl one-liners. Here it is scripted in Python and extended with the refine, exclude, and resubmit cycle.

```mermaid theme={null}
flowchart TD
    A["Start with raw objective"] --> B["POST /enhance-objective"]
    B --> C["POST /strategies"]
    C --> D["POST /strategies/{strategyId}/submit"]
    D --> E{"GET /strategies/{strategyId}<br/>status hits 100?"}
    E -->|"No"| F["wait, then poll again"]
    F --> E
    E -->|"Yes"| G["GET /strategies/{strategyId}/companies<br/>review holdings"]
    G --> G2["POST /helper/companies/search<br/>look up a company id"]
    G2 --> H["GET /strategies/{strategyId}/companies/{companyId}<br/>review reasoning and evidence"]
    H --> I["POST /exclusion-phrases<br/>get suggestions, choose phrases"]
    I --> J["PATCH /strategies/{strategyId}<br/>apply exclusions"]
    J --> K{"Satisfied?"}
    K -->|"No, resubmit"| D
    K -->|"Yes"| L["Done"]
    L -.->|"optional"| M["POST /reverse-objective<br/>repeat loop for inverse strategy"]
```

The examples use Python and the [`requests`](https://requests.readthedocs.io/) library.
All requests target the REST API base URL `https://api.noonum.ai/v2` and authenticate with an
`Authorization: Bearer <token>` header, where the token is your Noonum API key.
See [Authentication](/v2/getting-started/authentication) for details.

<Note>
  Public-catalog strategies are read-only. You can read them, but you can only refine strategies you own.
</Note>

<Note>
  Each time you submit a changed draft, Noonum freezes the new objective and exclusions into a fresh
  **active version** and reads default to that version. Watch `active_version_id` and `draft_version_id`
  to track which version is serving and whether your draft has unsaved edits.
  See [Objective versioning](/v2/guides/objective-versioning) for the full model.
</Note>

## Setup

```python theme={null}
from time import sleep

import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.noonum.ai/v2"

headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
```

## Iterate a strategy until you're happy

### 1. Start with an objective, then enhance it

```python theme={null}
name = "European defense sector"
raw_objective = "Invest in companies that would benefit from increased spend in the European defense sector."

enhance_resp = requests.post(
    f"{BASE_URL}/enhance-objective",
    json={"objective": raw_objective},
    headers=headers,
)
enhance_resp.raise_for_status()

enhanced = enhance_resp.json()
objective = enhanced["enhancedObjective"]
suggested_name = enhanced.get("suggestedName")

print("Enhanced objective:", objective)
print("Improvements summary:", enhanced.get("improvementsSummary"))
print("Suggested name:", suggested_name)
```

### 2. Create a strategy from the enhanced objective

```python theme={null}
create_resp = requests.post(
    f"{BASE_URL}/strategies",
    json={"name": suggested_name or name, "objective": objective},
    headers=headers,
)
create_resp.raise_for_status()

strategy = create_resp.json()
strategy_id = strategy["id"]
print("Strategy id:", strategy_id)
```

### 3. Submit the strategy for processing

```python theme={null}
submit_resp = requests.post(f"{BASE_URL}/strategies/{strategy_id}/submit", headers=headers)
submit_resp.raise_for_status()
```

<Note>
  Submitting does not generate a factsheet by default. While you're iterating quickly, that keeps
  each run fast and avoids blocking on factsheet generation. When you want one, pass
  `create_factsheet: true` in the submit body, or request it separately later (see
  [Where to go next](#where-to-go-next)).
</Note>

### 4. Poll until processing is complete

See [Build a strategy](/v2/guides/build-strategy) for how submit-and-poll works (`status == 100`
means processing finished).

```python theme={null}
while True:
    strategy_resp = requests.get(f"{BASE_URL}/strategies/{strategy_id}", headers=headers)
    strategy_resp.raise_for_status()

    status = strategy_resp.json()["status"]
    print("status:", status)

    if status == 100:
        break

    sleep(10)
```

### 5. Review companies in the strategy

```python theme={null}
companies_resp = requests.get(
    f"{BASE_URL}/strategies/{strategy_id}/companies",
    headers=headers,
)
companies_resp.raise_for_status()

companies = companies_resp.json()
print("Companies returned:", len(companies))
print("First company:", companies[0])
```

Each company entry includes an `id` for fetching its reasoning and evidence.

### 6. Look up a company id from a symbol

If you have a symbol in mind (e.g. `"ABC"`) but don't know the Noonum company id,
use `POST /helper/companies/search`.

```python theme={null}
# Use a known-in-strategy symbol to start (then replace with any symbol/ISIN/FIGI/name fragment).
symbol_query = companies[0].get("symbol") or "ABC"

search_resp = requests.post(
    f"{BASE_URL}/helper/companies/search",
    json={"query": symbol_query},
    headers=headers,
)
search_resp.raise_for_status()

search_results = search_resp.json()["results"]
company = search_results[0]
company_id = company["id"]
print("Matched company:", company["name"], company_id)
```

### 7. Fetch evidence for a company in your strategy

This endpoint explains why a company is or isn't in your strategy results:

* If the company fits the strategy, you'll see `isIncluded: true`, a human-readable
  `reasoning`, and `summaries` containing the evidence excerpts.
* If the company does not fit (or there isn't enough supporting evidence), you'll still
  get a response with `isIncluded: false`, a short "why not" message in `reasoning`, and
  typically an empty `summaries` list.

```python theme={null}
evidence_resp = requests.get(
    f"{BASE_URL}/strategies/{strategy_id}/companies/{company_id}",
    headers=headers,
)
evidence_resp.raise_for_status()

company_evidence = evidence_resp.json()
print("isIncluded:", company_evidence.get("isIncluded"))
print("reasoning:", company_evidence.get("reasoning"))
print("summaries (count):", len(company_evidence.get("summaries", [])))
```

To pull reasoning and evidence for all companies at once:

```python theme={null}
all_evidences_resp = requests.get(
    f"{BASE_URL}/strategies/{strategy_id}/evidences",
    headers=headers,
)
all_evidences_resp.raise_for_status()

all_evidences = all_evidences_resp.json()["evidences"]
print("Companies with evidences:", len(all_evidences))
```

### 8. Craft exclusion phrases grounded in the holdings

An exclusion is a short phrase describing a category of companies you want removed.
After you add exclusions and resubmit, the matching companies drop out and new companies
may surface to replace them. Add an exclusion to remove a specific company you don't want
even if it's otherwise relevant, or to remove a type of company such as a sector or business
activity.

Write each phrase as a short description of what a company is (2–6 words, no negation,
no "exclude" prefix, no company names). See [Build a strategy](/v2/guides/build-strategy) for
the full exclusion rules. Ground the phrase in the holdings: look at the inclusion reasoning
of the companies you want to remove, and describe the shared pattern.

`POST /exclusion-phrases` turns a company's evidence and reasoning into suggested phrases.
You can also write your own. Pass:

* your strategy `objective`
* the `companyId` you're reviewing
* the company's `inclusionReason` (use the `reasoning` field you just fetched)

```python theme={null}
inclusion_reason = company_evidence.get("reasoning") or ""

exclusion_resp = requests.post(
    f"{BASE_URL}/exclusion-phrases",
    json={
        "objective": objective,
        "companyId": company_id,
        "inclusionReason": inclusion_reason,
    },
    headers=headers,
)
exclusion_resp.raise_for_status()

suggestions = exclusion_resp.json()["phrases"]
print("Suggested phrases:")
for item in suggestions:
    print("-", item["phrase"], "=>", item["reason"])
```

Now choose which phrases to apply as exclusions. Start with 2–4; you can always add more.

```python theme={null}
# Example: pick the first 2 suggested phrases (or choose any subset)
exclusions = [suggestions[0]["phrase"], suggestions[1]["phrase"]]
print("Chosen exclusions:", exclusions)
```

### 9. Update the strategy with your exclusions, then resubmit

```python theme={null}
patch_resp = requests.patch(
    f"{BASE_URL}/strategies/{strategy_id}",
    json={"exclusions": exclusions},
    headers=headers,
)
patch_resp.raise_for_status()

updated_strategy = patch_resp.json()
print("Updated exclusions:", updated_strategy.get("exclusions"))
```

Editing `objective` or `exclusions` changes the draft, so `draft_version_id` becomes `null`
until you resubmit. The next submit freezes the new draft into a fresh active version.
See [Objective versioning](/v2/guides/objective-versioning) for details.

Resubmit and repeat the review loop.

```python theme={null}
requests.post(f"{BASE_URL}/strategies/{strategy_id}/submit", headers=headers).raise_for_status()

while True:
    strategy_resp = requests.get(f"{BASE_URL}/strategies/{strategy_id}", headers=headers)
    strategy_resp.raise_for_status()
    if strategy_resp.json()["status"] == 100:
        break
    sleep(10)

companies_resp = requests.get(f"{BASE_URL}/strategies/{strategy_id}/companies", headers=headers)
companies_resp.raise_for_status()
companies = companies_resp.json()
print("New companies returned:", len(companies))
```

After resubmission, check that your exclusions didn't remove companies you wanted to keep,
then keep iterating.

## Create an inverse strategy objective

Use `POST /reverse-objective` to generate an "inverse" strategy, then repeat the loop above.

```python theme={null}
reverse_resp = requests.post(
    f"{BASE_URL}/reverse-objective",
    json={"objective": objective},
    headers=headers,
)
reverse_resp.raise_for_status()

reverse = reverse_resp.json()
inverse_objective = reverse["reverseObjective"]
inverse_name = reverse.get("reverseName") or f"Inverse of {name}"

print("Inverse name:", inverse_name)
print("Inverse objective:", inverse_objective)
```

From here, repeat the same steps:

* `POST /strategies` with `inverse_name` and `inverse_objective`
* `POST /strategies/{strategyId}/submit`
* poll `GET /strategies/{strategyId}` until `status == 100`
* review companies and evidence
* craft exclusions and `PATCH /strategies/{strategyId}` with the updated `exclusions`
* resubmit and repeat

## Generate historical results

There is no `/historical-data` endpoint in v2. To produce results for a past date, submit a run
with a past `as_of_date`, poll that `(version, date)` cell to completion, then read its companies
with `asOfDate`. Historical runs are rate-limited; an already-in-flight run for the same date
returns `423`.

### 1. Submit historical runs for specific month-end dates

Submit once per date against the active version. You can omit `version_id` (it defaults to the
current draft's version), but pinning it to the active version is unambiguous.

```python theme={null}
active_version_id = requests.get(
    f"{BASE_URL}/strategies/{strategy_id}", headers=headers
).json()["active_version_id"]

dates = ["2025-07-31", "2025-06-30"]

for d in dates:
    r = requests.post(
        f"{BASE_URL}/strategies/{strategy_id}/submit",
        json={"as_of_date": d, "version_id": active_version_id},
        headers=headers,
    )
    if r.status_code == 201:
        print(f"{d}: accepted")
    elif r.status_code == 423:
        print(f"{d}: already in flight (treat as accepted)")
    elif r.status_code == 429:
        body = r.json()
        print(f"{d}: rate limited — {body['available']} credits left, "
              f"{body['requested']} requested")
    elif r.status_code == 400:
        print(f"{d}: rejected (future date)")
    else:
        r.raise_for_status()
```

### 2. Poll a historical run to completion

Poll the `(version, date)` cell via `GET /strategies/{strategyId}/versions/{versionId}` with `asOfDate`.

```python theme={null}
def wait_for_cell(strategy_id, version_id, as_of_date, timeout_s=1800):
    waited = 0
    while waited < timeout_s:
        r = requests.get(
            f"{BASE_URL}/strategies/{strategy_id}/versions/{version_id}",
            params={"asOfDate": as_of_date},
            headers=headers,
        )
        if r.status_code == 404:
            # No run yet for this cell — keep waiting (or it was never submitted).
            sleep(10); waited += 10; continue
        r.raise_for_status()
        status = r.json()["status"]
        if status == 100:
            return True
        if status in (-1, -2):
            return False
        sleep(10); waited += 10
    return False

wait_for_cell(strategy_id, active_version_id, "2025-06-30")
```

### 3. List the dates that are ready

```python theme={null}
available_dates_resp = requests.get(
    f"{BASE_URL}/strategies/{strategy_id}/available-dates",
    headers=headers,  # add params={"versionId": active_version_id} to pin a version
)
available_dates_resp.raise_for_status()

available_dates = available_dates_resp.json()["available_dates"]
print("Available dates:", available_dates)
```

### 4. Read historical results for one date

Use the **companies** endpoint with `asOfDate`. The response shape matches the live results.

```python theme={null}
date = available_dates[0]

historical_resp = requests.get(
    f"{BASE_URL}/strategies/{strategy_id}/companies",
    headers=headers,
    params={"asOfDate": date},  # add "versionId": ... to pin a version
)
historical_resp.raise_for_status()

historical_companies = historical_resp.json()
print(f"{date}: {len(historical_companies)} companies")
print("First historical row:", historical_companies[0])
```

## Where to go next

After refinement, request a factsheet with `POST /strategies/{strategyId}/factsheet`, then
`GET /strategies/{strategyId}/factsheet` — it returns a `302` redirect to the PDF when ready,
or a `200` status body (`generating` / `not_requested`) while it is still being produced. You
can also pass `create_factsheet: true` on the original submit. (The MCP server tool
`get_strategy_factsheet` drives the same flow.)

* [Analyze strategies](/v2/guides/analyze-strategies): filter, sort, and compare holdings.
* [REST integration](/v2/guides/rest-integration): wire the loop into your own application.
* [MCP server](/base/agentic/mcp-server): drive the same workflow from an agent.
