Skip to main content
This guide continues from Build a 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 shows this loop as curl one-liners. Here it is scripted in Python and extended with the refine, exclude, and resubmit cycle. The examples use Python and the requests library. All requests target the REST API base URL https://api.noonum.ai/v1 and authenticate with an Authorization: Bearer <token> header, where the token is your Noonum API key. See Authentication for details.
Premade strategies are read-only and cannot be refined.

Setup

from time import sleep

import requests

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

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

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"]

print("Enhanced objective:", objective)
print("Improvements summary:", enhanced.get("improvementsSummary"))

2. Create a strategy from the enhanced objective

create_resp = requests.post(
    f"{BASE_URL}/strategies",
    json={"name": 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

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

4. Poll until processing is complete

See Build a strategy for how submit-and-poll works (status == 100 means processing finished).
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

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.
# 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.
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:
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 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)
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.
# 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

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"))
Resubmit and repeat the review loop.
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.
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

1. Request historical data for specific month-end dates

historical_request_resp = requests.post(
    f"{BASE_URL}/strategies/{strategy_id}/historical-data",
    json={"dates": ["2025-07-31", "2025-06-30"]},
    headers=headers,
)
historical_request_resp.raise_for_status()

print(historical_request_resp.json())

2. Get available dates for download

available_dates_resp = requests.get(
    f"{BASE_URL}/strategies/{strategy_id}/historical-data/dates",
    headers=headers,
)
available_dates_resp.raise_for_status()

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

3. Download historical results for one date

date = available_dates[0]

historical_results_resp = requests.get(
    f"{BASE_URL}/strategies/{strategy_id}/historical-data",
    headers=headers,
    params={"date": date},
)
historical_results_resp.raise_for_status()

historical_companies = historical_results_resp.json()
print("Historical companies returned:", len(historical_companies))
print("First historical row:", historical_companies[0])

Where to go next

After refinement, GET /strategies/{strategyId}/factsheet (MCP server tool get_strategy_factsheet) returns a presigned URL to a PDF summary of the updated strategy.