This guide continues from Build a strategy and covers the refinement loop:
- Start with a rough objective.
- Enhance the objective with
POST /enhance-objective.
- Create and submit a strategy.
- Poll until processing is complete (
status == 100).
- Review holdings.
- Look up a company id from a symbol with
POST /helper/companies/search.
- Fetch reasoning and evidence for a company.
- Craft exclusion phrases grounded in the holdings, then
PATCH them onto the strategy.
- 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.