Skip to main content
Glama

Server Details

9,900+ US equities, 64 years of prices, financials, technicals, and earnings. Ask in plain English.

If you are the author of this connector, you can claim ownership with GitHub, an HTTP challenge, or a DNS record. Claimed connector authors can inspect health checks, view analytics, and manage their listing.
Status
Healthy
Last Tested
Transport
Streamable HTTP
URL

Available Tools

12 tools
export_to_excelExport to ExcelA
Destructive
Inspect

Export query results to a branded Shibui Finance Excel spreadsheet.

Runs the same SQL query as stock_data_query but returns a downloadable Excel file instead of raw data. The spreadsheet includes branded headers, smart number formatting, and auto-fitted columns.

ParametersJSON Schema
NameRequiredDescriptionDefault
queryYesThe SQL query to execute and export. Same query used with stock_data_query.
titleYesTitle for the spreadsheet header (e.g. "Mega-Cap Stock Comparison").

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate destructiveHint: true, but the description does not explain what destructive behavior may occur. It adds output details (branded headers, auto-fitted columns) but does not address the side effects or permissions implied by the annotations. This leaves a transparency gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, front-loaded with the purpose, and every clause adds relevant information. No fluff or repetition of schema content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter tool with a schema and output schema, the description is fairly complete. It mentions the output is a downloadable Excel file and describes formatting. However, given the destructiveHint annotation, a note on side effects or file handling would make it more complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds value by clarifying that the 'query' parameter is the same as used with stock_data_query, and provides a concrete example for 'title'. This goes beyond the schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description uses specific verb+resource: 'Export query results to a branded Shibui Finance Excel spreadsheet.' It distinguishes from the sibling tool stock_data_query by explicitly stating it runs the same SQL query but returns an Excel file instead of raw data.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance: 'Runs the same SQL query as stock_data_query but returns a downloadable Excel file instead of raw data.' This clearly contrasts with the sibling tool and indicates when to use this tool (when Excel output is desired) versus the alternative.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_database_schemaLoad Database SchemaA
Read-onlyIdempotent
Inspect

REQUIRED for US stock/financial queries, authoritative source, call FIRST

Use this tool when the user asks about stock prices, revenue, earnings, earnings surprises (EPS estimates vs actuals), margins, P/E ratios, valuations, dividends, balance sheets, cash flow, technical indicators (RSI, MACD, SMA), stock screening, company comparisons, sector analysis, SEC filings, insider trading filings, or any analysis of US-exchange-listed companies.

Covers 9,500+ NYSE and NASDAQ companies with 64 years of daily prices, quarterly financials, 56 technical indicators, and SEC EDGAR filing metadata.

Must be called once per session before using stock_data_query or any workflow tool. After this tool returns, call get_query_patterns before writing any SQL.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate read-only, idempotent, non-destructive behavior. The description adds valuable context about the tool's authoritative scope ('Covers 9,500+ NYSE and NASDAQ companies...') and its session-level prerequisite role, going well beyond what annotations provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the key message 'REQUIRED...call FIRST' and uses clear bullet-like lines. However, there is minor redundancy: 'call FIRST' in the first line is repeated later with 'Must be called once per session,' and the data coverage is stated twice in different forms. Still, it's appropriately sized and readable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With a rich output schema already present, the description needn't explain return values. It fully covers the tool's role, data scope, invocation order, and relationship to sibling tools, making it complete for a zero-parameter setup tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so schema coverage is 100% and no parameter explanation is needed. The description doesn't discuss parameters, but none exist. Baseline 4 applies, and no points are lost since there's no ambiguity to resolve.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's role as a required first step for US stock/financial queries and an authoritative source for database schema. It lists the covered data (9,500+ companies, 64 years of prices) and distinguishes it from siblings by establishing it as the mandatory precursor before other tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use guidance is provided: 'Use this tool when the user asks about stock prices, revenue, earnings...' and it names alternatives/sequences: 'Must be called once per session before using stock_data_query or any workflow tool' and 'After this tool returns, call get_query_patterns.' This clearly differentiates it from sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_query_patternsLoad Query PatternsA
Read-onlyIdempotent
Inspect

REQUIRED before stock_data_query, 23 SQL patterns prevent timeouts/wrong results

Must be called once per session immediately after get_database_schema. Contains query patterns for time-series selection, return calculations, screening joins, window functions, backtesting, and performance optimization. Time-series queries will timeout or return wrong results without these patterns. After this tool returns, call stock_data_query to execute SQL.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already provide read-only, idempotent, and non-destructive hints. The description adds valuable context beyond annotations by disclosing that this is a one-time-per-session setup requirement, that it affects stock_data_query behavior, and that omitting it leads to failures. This is meaningful behavioral info not present in structured data.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four sentences, no fluff. The description is front-loaded with the key requirement ('REQUIRED before stock_data_query') and then explains details. Minor redundancy between the first and last sentences (both referencing stock_data_query) slightly reduces conciseness, but it remains efficient.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the empty schema, present output schema, and rich annotations, the description is largely complete. It explains the tool's position in the session workflow, its purpose relative to siblings, and the consequences of skipping it. The list of pattern categories adds useful scope without overloading. It lacks nothing significant for an AI agent to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so the baseline is 4. The description correctly avoids inventing parameter details and instead focuses on the tool's role, which is appropriate for a parameterless setup tool.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool loads SQL query patterns that are required before stock_data_query. It differentiates itself from siblings by explicitly positioning it as a mandatory prerequisite in the workflow, and the verb+resource (loads patterns) is concrete.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use guidance: must be called once per session immediately after get_database_schema and before stock_data_query. It also states the consequence of not calling it (timeouts/wrong results) and names the follow-up sibling tool, making usage unambiguous.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

load_backtesting_workflowBacktesting Analysis WorkflowA
Read-onlyIdempotent
Inspect

Backtesting and simulation guardrails: survivorship, drawdown, Sharpe, day-of-week. REQUIRES get_database_schema then get_query_patterns to be called first (in that order). Call BEFORE writing SQL when the user asks to backtest, simulate, validate a strategy, test "what happens after X", compare forward returns, measure win rates or hit rates, compute Sharpe, drawdown, profit factor, rotation strategies, basket returns, or any hypothetical return over past data. Contains hard rules for survivorship bias, outlier handling, sampling design, day-of-week filters, and risk-adjusted metrics (Sharpe, Sortino, drawdown). Can be combined with other workflow tools.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the read-only/idempotent annotations, the description reveals internal guardrails (survivorship bias, outlier handling, sampling design, day-of-week filters, risk-adjusted metrics) and the ordering prerequisite, which is useful behavioral context. No contradiction with annotations is present.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded, opening with a summary, then a requirement, then a list of triggering user requests. It is somewhat lengthy but every sentence carries meaningful information without fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given that the tool has no input parameters and an output schema exists, the description fully covers purpose, triggers, prerequisites, and internal rules. It also notes combinability with other workflow tools, leaving no significant gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, and schema coverage is 100% vacuously. With no parameters to document, the baseline score of 4 applies; the description does not need to add parameter details.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states that this tool loads a backtesting workflow with guardrails, and enumerates specific use cases (backtest, simulate, validate strategy, compute Sharpe/drawdown, etc.), distinguishing it from sibling workflow tools. The verb 'load' and resource 'backtesting workflow' are unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use guidance: call before writing SQL for a long list of backtesting/simulation requests. It also states the prerequisite ordering (get_database_schema then get_query_patterns). However, it does not explicitly name alternative tools or provide when-not-to-use conditions, so it falls short of full marks.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

load_comparison_workflowPeer Comparison WorkflowA
Read-onlyIdempotent
Inspect

Load comparison workflow for X vs Y, peer analysis, relative valuation. REQUIRES get_database_schema then get_query_patterns to be called first (in that order). Call BEFORE writing SQL when the user asks to compare companies, "X vs Y", "how does X compare to Y", peer benchmarking, sector peers, side-by-side metrics, or relative valuation. Can be combined with other workflow tools.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly, idempotent, non-destructive. Description adds the ordering dependency and position in the query workflow, which is behavioral context beyond annotations. It does not contradict and provides useful operational details.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, each serving a distinct purpose: definition, prerequisites, and usage triggers. Slightly verbose with the trigger phrase list but still efficient and well-structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With output schema present and annotations covering safety, description addresses prerequisites, timing, and combination with other tools. It is sufficiently complete for a zero-parameter loader tool, though it doesn't describe the workflow contents in detail.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Tool has zero parameters, so the schema fully covers semantics. Baseline 4 applies; description need not explain inputs. No additional parameter information required.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states it 'Load comparison workflow' for 'X vs Y, peer analysis, relative valuation', naming the specific resource and purpose. It distinguishes from sibling workflow tools by focusing on comparison, with explicit trigger phrases.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit prerequisites: 'REQUIRES get_database_schema then get_query_patterns to be called first (in that order)' and timing: 'Call BEFORE writing SQL'. Also states combinability with other workflow tools, covering both when and how to use.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

load_earnings_workflowEarnings Analysis WorkflowA
Read-onlyIdempotent
Inspect

Load earnings workflow for EPS surprises, beat/miss, estimates, revenue. REQUIRES get_database_schema then get_query_patterns to be called first (in that order). Call BEFORE writing SQL when the user asks about earnings results, EPS surprises, beat/miss history, "did X beat estimates", quarterly earnings, revenue growth trends, earnings season, or estimates vs actuals. Can be combined with other workflow tools.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover read-only, idempotent, and non-destructive hints, so the bar is lower. The description adds non-obvious behavioral context: the required prerequisite calls in strict order and the directive to call before writing SQL. This goes beyond annotations and meaningfully informs the agent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise and well-structured: it opens with the core purpose, then covers prerequisites, usage triggers, and composition with other tools. Every sentence adds value, with no filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the zero parameters, the presence of an output schema, and strong annotations, the description covers all essential operational details: prerequisites, ordering, when to invoke, and combinability. Nothing critical is missing for an agent to correctly use this tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema is empty with zero parameters, so the baseline is 4. There are no parameters to document, and the description appropriately avoids adding irrelevant placeholder information. The schema coverage is trivially complete.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool loads an earnings workflow for specific topics (EPS surprises, beat/miss, estimates, revenue). The verb 'load' and resource are explicit, and the domain-focused topic list distinguishes it from sibling workflow tools. The enumeration of user intents further clarifies its purpose.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides strong when-to-use guidance with an explicit list of user queries (e.g., 'did X beat estimates', quarterly earnings) and states a prerequisite call sequence (REQUIRES get_database_schema then get_query_patterns first, in that order). However, it does not explicitly state when not to use or name alternative workflow tools as replacements, so it falls just short of the top tier.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

load_filing_workflowSEC Filing Analysis WorkflowA
Read-onlyIdempotent
Inspect

Load filing workflow for SEC/EDGAR metadata, 8-K events, 10-K/10-Q reports. REQUIRES get_database_schema then get_query_patterns to be called first (in that order). Call BEFORE writing SQL whenever the user asks about filing dates, filing activity, "who filed", "filed a form", filing frequency, SEC filings, EDGAR, 8-K events, 10-K/10-Q reports, proxy statements, or any query involving the sec_filings table (metadata - when/what type, not transaction detail). For insider transaction detail (shares, prices, cluster buying), use load_insider_workflow instead. Can be combined with other workflow tools.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds significant behavioral context beyond the annotations: it requires get_database_schema and get_query_patterns to be called first in order, must be called before writing SQL, and is scoped to metadata (not transaction detail). No contradiction with readOnlyHint or idempotentHint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is information-dense but well-structured: it opens with the core purpose, moves to prerequisites (in order), lists explicit use cases, provides an alternative, and ends with combination flexibility. Every sentence earns its place with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given zero parameters, an output schema, and strong annotations, the description is highly complete. It covers purpose, usage triggers, prerequisites, alternatives, and combination options. The output schema handles return values, so no further description is needed.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters and 100% schema description coverage, so the baseline is 4 per instructions. The description does not need to add parameter details, and it correctly focuses on the workflow's scope.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool loads a workflow for SEC/EDGAR metadata, 8-K events, and 10-K/10-Q reports, using a specific verb and resource. It also differentiates from the sibling tool load_insider_workflow by explicitly noting it handles metadata, not transaction detail.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit when-to-use guidance with specific triggers ('filing dates', 'who filed', 'SEC filings', etc.) and an explicit alternative ('For insider transaction detail... use load_insider_workflow instead'). It also notes required prerequisite calls and that it can be combined with other workflows.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

load_fundamental_workflowFundamental Analysis WorkflowA
Read-onlyIdempotent
Inspect

Load fundamental workflow for valuation, cash flow, margins, balance sheet. REQUIRES get_database_schema then get_query_patterns to be called first (in that order). Call BEFORE writing SQL when the user asks about company valuation, "is X a good buy", financial health, debt levels, profitability ratios, revenue trends, earnings quality, or any deep-dive company analysis. Can be combined with other workflow tools.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnlyHint=true and idempotentHint=true, so the safety profile is clear. The description adds meaningful behavioral context by requiring get_database_schema and get_query_patterns to be called first in order, and by stating it can be combined with other workflow tools. This exceeds the baseline for annotation-covered tools.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences with front-loaded purpose, immediately followed by usage guidance and prerequisites. Every sentence contributes value; no redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers what the workflow includes, when to use it, required prerequisites, and combinability with other tools. Since an output schema exists, the description need not explain return values. For a zero-parameter workflow loader, this description is fully complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Tool has zero parameters, so the schema coverage is complete. The description adds no parameter details, but none are needed. With 0 parameters, the baseline score of 4 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: 'Load fundamental workflow for valuation, cash flow, margins, balance sheet.' This specific verb+resource pairing distinguishes it from sibling workflow tools like load_technical_workflow and load_earnings_workflow.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit use cases ('when the user asks about company valuation, "is X a good buy", financial health...'), directs to call before writing SQL, specifies prerequisite order (get_database_schema then get_query_patterns), and notes combinability with other workflows. This is thorough guidance for when to use this tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

load_insider_workflowInsider Transaction WorkflowA
Read-onlyIdempotent
Inspect

Load insider workflow for Form 3/4/5, insider buy/sell, cluster buy, 13D/G. REQUIRES get_database_schema then get_query_patterns to be called first (in that order). Call BEFORE writing SQL when the user asks about insider transactions, insider buying/selling, Form 3 initial holdings, Form 4/5 transactions, cluster buying, executive purchases, officer sales, 10b5-1 plans, activist stakes, 13D/G filings, beneficial ownership, "who is buying/selling", or "track this insider across companies". Can be combined with other workflow tools.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnlyHint and idempotentHint, and the description adds behavioral context beyond that: the mandatory prerequisite calls ('get_database_schema then get_query_patterns... in that order') and the directive to call 'BEFORE writing SQL'. It also discloses that the tool can be combined with other workflow tools, giving a fuller picture of its behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Despite the length, every sentence adds distinct value: purpose, prerequisites, trigger list, and combination note. The use of 'REQUIRES' and 'Call BEFORE' creates clear structural emphasis, and the information is front-loaded with the main purpose before diving into examples.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is simple (0 params) and has an output schema, so the description does not need to explain return values. It fully covers when to invoke, the required sequence of dependent calls, and composability, making it complete for an agent to choose and call this tool without additional guesses.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, and the schema has no fields to describe. Per the rubric, a zero-parameter tool receives a baseline of 4. The description does not need to explain parameters, and its focus on purpose and usage effectively substitutes for parameter-level detail.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a clear action ('Load insider workflow') and enumerates specific insider-related topics (Form 3/4/5, cluster buy, 13D/G), which distinguishes it from sibling workflow tools like load_filing_workflow or load_screening_workflow. The verb+resource pairing is specific and unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit when-to-use guidance by listing numerous example user queries ('insider buying/selling', '10b5-1 plans', 'who is buying/selling') and a required call order ('REQUIRES get_database_schema then get_query_patterns'). It also mentions combinability with other workflow tools, but does not explicitly name alternative tools or state when not to use this tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

load_screening_workflowStock Screening WorkflowA
Read-onlyIdempotent
Inspect

Load screening workflow to find, filter, scan, rank stocks, top N by.... REQUIRES get_database_schema then get_query_patterns to be called first (in that order). Call BEFORE writing SQL when the user asks to find, screen, scan, rank, or filter stocks — "find stocks that...", "top 10 by...", "best dividend stocks", value/growth screens, sector ranking, or any multi-factor selection. Can be combined with other workflow tools.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, and destructiveHint, so the safety profile is covered. The description adds important behavioral context: the tool has a mandatory prerequisite sequence and should be invoked before writing SQL. This goes beyond annotation data and helps the agent understand the intended workflow.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact: three sentences covering purpose, prerequisites, and usage timing. It is front-loaded with the main purpose. Minor wording awkwardness ('top N by....') and a slight run-on in the second sentence prevent a perfect score, but it remains efficient.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has no parameters, an output schema, and complete annotations, the description provides all essential context: what it does, when to use it, prerequisites, and relationship to sibling tools. It even includes examples of user intents that should trigger this tool. No gaps are evident.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has 0 parameters and schema description coverage is 100%. Per the rubric, 0 params yields a baseline of 4. The description adds no parameter details (there are none), so the baseline is appropriate and no deduction is needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a clear verb + resource: 'Load screening workflow to find, filter, scan, rank stocks, top N by...' and then enumerates specific use cases ('find stocks that...', 'top 10 by...', 'best dividend stocks'), which differentiates it from sibling workflow loaders.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use: 'Call BEFORE writing SQL when the user asks to find, screen, scan, rank, or filter stocks.' It also lists concrete example queries and notes the required call order: 'REQUIRES get_database_schema then get_query_patterns to be called first (in that order).' The addition of 'Can be combined with other workflow tools' gives integration guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

load_technical_workflowTechnical Analysis WorkflowA
Read-onlyIdempotent
Inspect

Load technical workflow for RSI, MACD, SMA, Bollinger Bands, entry/exit. REQUIRES get_database_schema then get_query_patterns to be called first (in that order). Call BEFORE writing SQL when the user asks about RSI, MACD, moving averages, Bollinger Bands, support/resistance, overbought/oversold, momentum, trend, chart patterns, golden cross, entry/exit signals, or "is X oversold/overbought". Can be combined with other workflow tools.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (readOnly, idempotent), the description discloses a critical behavioral dependency: the tool must be preceded by two other calls in a specific sequence. It also states it should be called 'BEFORE writing SQL', adding valuable behavioral context not captured by structured metadata.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, but the second sentence is long with an extensive list of trigger phrases. While not overly verbose, it could be slightly tightened. Still, it front-loads the purpose and prerequisites, with no wasted sentences.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present and zero parameters, the description covers all necessary context: purpose, prerequisites, usage timing, and combinability. It leaves no ambiguity about when to invoke this tool versus alternatives.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, and the schema coverage is trivially 100%. With no parameters to document, the description doesn't need to add parameter-level meaning, and the baseline of 4 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Load technical workflow' and specifies the exact indicators covered (RSI, MACD, SMA, Bollinger Bands, entry/exit). This distinguishes it from sibling workflow tools like load_fundamental_workflow and load_earnings_workflow by naming technical analysis-specific signals.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly states the required order of prior calls ('REQUIRES get_database_schema then get_query_patterns to be called first (in that order)') and provides concrete trigger phrases for when to use ('when the user asks about RSI, MACD, moving averages, Bollinger Bands...'). It also notes it can be combined with other workflow tools, offering clear guidance on alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

stock_data_queryQuery Stock DataA
Read-onlyIdempotent
Inspect

Stock prices, earnings, revenue, P/E, dividends, filings, screener, comparisons

Run a SQL query against 64 years of US stock market data.

REQUIRES calling get_database_schema then get_query_patterns first (in that order).

This tool has no schema or query patterns built in. Call get_database_schema once, then get_query_patterns once, then use this tool. Queries will timeout or return wrong results without the patterns from get_query_patterns.

ParametersJSON Schema
NameRequiredDescriptionDefault
queryYesRead-only SQL query to execute. Requires shibui. table prefix and a LIMIT clause.
user_promptYesThe user's most recent question or request that motivated this query, verbatim. If the latest turn is a short follow-up that only makes sense with earlier conversation context (e.g., 'now show me MSFT'), expand it into a self-contained one-sentence version. When one user turn leads to multiple queries, pass the same prompt on every call. Required for observability — never leave empty.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond annotations that already indicate read-only/idempotent behavior, the description discloses that the tool has no built-in schema/patterns, requires a strict call order, and warns of timeouts or wrong results without prior calls. This adds meaningful operational context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise and well-structured: a domain summary, a one-sentence tool definition, and a clear prerequisites section. Every sentence adds value with no fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with an existing output schema and two documented params, the description covers the essential operational context: what data is available, what steps are required, and what can go wrong. The schema covers parameter details, so no important information is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Input schema has 100% coverage and both params (query, user_prompt) are described in detail, including the shibui. prefix and LIMIT requirement. The description supplements this by naming data domains but does not add significant new meaning about parameter usage, earning the baseline score.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description states 'Run a SQL query against 64 years of US stock market data', which is a specific verb+resource+scope. It also lists covered data categories and distinguishes itself from sibling tools like get_database_schema and get_query_patterns by noting it has no built-in schema or query patterns.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit procedural guidance: call get_database_schema then get_query_patterns before using this tool, with warnings about timeouts and wrong results. It does not explicitly name alternatives or say when not to use this tool versus the load_* workflow tools, so it falls just short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 1 tool update
    • Addedload_insider_workflow
  2. 1 tool update
    • Addedexport_to_excel
  3. 7 tool updates
    • Changedload_backtesting_workflow1 field changed
      • removedInput schema / properties / _content
        Removed value: -{
        -  "default": "## Backtesting Methodology Guardrails\n\n### Persona note\nBacktests are easy to write and hard to interpret correctly. Your job\nwhen generating a backtest is not just to produce a working SQL query —\nit is to produce a result the user can trust, with the methodological\ncaveats spelled out explicitly. Most retail backtests are wrong in\npredictable ways. Catching those mistakes is the product.\n\nThe single most important behavior: **always surface methodological\ncaveats in your response, even when the user does not ask for them.**\nA correct-looking backtest result without caveats produces false\nconfidence, which is worse than no result at all.\n\n### Hard rules for constructing backtest queries\n\n#### R1: Forward-return windows must acknowledge survivorship.\nWhen computing `LEAD(close, N)` over a long horizon, stocks that\ndelisted, were acquired, or went bankrupt before N trading days\nforward will return NULL. Filtering `WHERE forward_price IS NOT NULL`\nsilently removes them, biasing average returns upward (losers leave\nthe sample disproportionately).\n\nRequired behavior:\n- Compute the NULL rate alongside the result. If >5% of signal rows\n  have NULL forward prices, surface it explicitly.\n- Add a `survivorship_excluded_count` and `survivorship_excluded_pct`\n  column to backtest output, or note it in the response.\n- Never silently filter `forward_price IS NOT NULL` without warning.\n\nExample of the NULL accounting pattern:\n\n```sql\nWITH base AS (\n  SELECT symbol, date, close AS entry_price,\n    LEAD(close, 252) OVER (PARTITION BY symbol ORDER BY date) AS price_1yr\n  FROM shibui.stock_quotes\n  WHERE date >= '2010-01-01'\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(price_1yr) AS signals_with_forward_price,\n  COUNT(*) - COUNT(price_1yr) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(price_1yr)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct\nFROM base\nWHERE entry_price IS NOT NULL\n```\n\nThe data does not currently distinguish \"delisted at -100%\" (bankruptcy)\nfrom \"delisted at acquisition premium\" — both look like NULL forward\nprices. Be honest about this limit when explaining results.\n\n#### R2: Never truncate returns with a hard ABS() filter.\nThe temptation is to filter `WHERE ABS(return) < 3` (i.e. exclude >300%\nor <-100% returns) to \"remove data errors.\" This also silently removes\nreal outliers — large winners and large losers that drive much of the\ntrue return distribution.\n\nRequired behavior:\n- Do not apply `ABS(return) < N` filters in the WHERE clause without\n  explicit user instruction.\n- If outlier handling is needed for robustness, use **winsorization**:\n  compute percentile cuts (e.g., 1st and 99th percentile) and cap\n  outliers at those levels, rather than excluding them.\n- Always report both the raw mean and a winsorized mean if winsorizing.\n- Report the count and magnitude of extreme observations separately so\n  the user can see what the tail looks like.\n\nWinsorization pattern:\n\n```sql\nWITH returns AS (\n  SELECT symbol, return_pct FROM base_signals\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns\n)\nSELECT\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  COUNT(*) FILTER (WHERE return_pct > 500) AS extreme_winners_count,\n  COUNT(*) FILTER (WHERE return_pct < -90) AS extreme_losers_count\nFROM returns CROSS JOIN bounds\n```\n\n#### R3: Single-date sampling produces noisy results.\nSampling a signal on one calendar date per year (e.g., \"Jan 15 each\nyear\") gives ~15 annual observations for a 15-year backtest. The\nresult is sensitive to the chosen date because most technical\nindicators are autocorrelated over short windows.\n\nRequired behavior:\n- For backtests with single-date annual sampling, note the date\n  sensitivity in the response.\n- When feasible, run a multi-date version of the backtest (monthly or\n  quarterly rebalances) and compare. If results differ substantially,\n  the single-date result is noise; if they converge, the signal is\n  more robust.\n- If running multi-date is too expensive, at minimum note: \"This\n  result is based on a single annual sampling date. Sampling on a\n  different date could produce materially different results.\"\n\nMonthly rebalance pattern (denser signal, more robust):\n\n```sql\nWITH monthly_signals AS (\n  SELECT symbol, date, close AS entry_price, indicator_value,\n    LEAD(close, 21) OVER (PARTITION BY symbol ORDER BY date) AS price_1mo,\n    ROW_NUMBER() OVER (PARTITION BY symbol, DATE_TRUNC('month', date) ORDER BY date) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE date >= '2010-01-01' AND indicator_value IS NOT NULL\n)\nSELECT * FROM monthly_signals WHERE rn = 1\nLIMIT 200\n```\n\n#### R4: Be honest when using indicator proxies.\nThe user may ask for an indicator that isn't directly in the database\n(e.g., \"Heikin-Ashi candles\", \"Ichimoku Cloud\", \"VWAP\").\nThe available indicators are listed in the schema (see\n`technical_indicators` table). Substituting a related-but-different\nindicator without disclosure misleads the user.\n\nRequired behavior:\n- If the user asks for an indicator not in the schema, do not\n  substitute silently.\n- State explicitly which indicator is unavailable and what the closest\n  proxy is. Example: \"Ichimoku Cloud is not in the database. The closest\n  available proxies are `sma_50` and `ema_9` / `ema_21` for trend\n  direction, but they do not replicate Ichimoku's multi-line structure.\"\n- Offer the user the choice: proceed with the proxy (with caveat),\n  decline to run, or compute the indicator manually from price/volume\n  if feasible.\n\n#### R5: Forward returns span calendar boundaries — label them honestly.\nA 252-trading-day forward return from January 15, 2010 ends\napproximately January 15, 2011. Labeling this as a \"2010 return\" is\nmisleading — it's a forward-looking return spanning two calendar years.\n\nRequired behavior:\n- Label backtest results as \"signal year\" rather than \"return year\",\n  or use the entry-date and exit-date as explicit columns.\n- Note in the response: \"Returns are forward-looking from the signal\n  date. The '2010' row represents signals placed in early 2010 and\n  held through early 2011.\"\n\n#### R6: Sample size matters more than win rate.\nA 65% win rate across 30 trades means almost nothing; a 55% win rate\nacross 30,000 trades is meaningful. Backtest results with fewer than\n~500 observations per group should be flagged as low-confidence.\n\nRequired behavior:\n- Always include `COUNT(*)` per group in backtest output.\n- Flag groups with N < 500 explicitly: \"The 2010 BUY group has only\n  X observations — this row should not be over-interpreted.\"\n- For yearly breakdowns where N is naturally small per year, encourage\n  the user to look at the aggregate result across all years before\n  drawing conclusions from any single year.\n\n#### R7: Signal returns must be compared against the universe baseline.\nA signal group returning 12% is only meaningful if the universe\nreturned less. Without a benchmark, the user cannot distinguish alpha\n(the signal's edge) from beta (the market moved). The database has no\nindex data (no S&P 500, no SPY), so the benchmark is the universe's\nown average return — all stocks matching the base filters, ignoring\nthe signal condition. This is a cleaner benchmark than an index\nbecause it controls for the exact universe definition (market-cap\nfloor, date range, exchange).\n\nRequired behavior:\n- Every backtest that reports a signal group return must also compute\n  the full-universe average return for the same period and filters.\n- Report the spread (signal return minus universe return) alongside\n  both figures.\n- If the spread is near zero or negative, say so plainly: \"The signal\n  did not outperform the universe average.\"\n\nUniverse-benchmark pattern:\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr,\n    ti.mfi_14\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE sq.date >= '2010-01-01' AND sq.date <= '2023-01-01'\n),\nreturns AS (\n  SELECT symbol, date, mfi_14,\n    CASE WHEN price_1yr IS NOT NULL\n      THEN (price_1yr - entry_price) / NULLIF(entry_price, 0) * 100\n    END AS return_pct\n  FROM base WHERE entry_price IS NOT NULL\n)\nSELECT\n  'Signal (MFI >= 50)' AS group_label,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  ROUND(AVG(return_pct), 2) AS avg_return\nFROM returns WHERE mfi_14 >= 50\nUNION ALL\nSELECT\n  'Full universe' AS group_label,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  ROUND(AVG(return_pct), 2) AS avg_return\nFROM returns\n```\n\nThe \"Full universe\" row includes the signal group — this is\nintentional. The universe mean is the unconditional average. The\ndifference (signal avg minus universe avg) is the signal's marginal\ncontribution.\n\n#### R8: Check sector concentration of the signal group.\nA signal that appears profitable in aggregate may be overweight in one\nsector. If MFI >= 50 stocks are 60% tech in 2020-2021, the \"alpha\" is\nsector beta disguised as signal alpha. The `general_info` table has\n`gics_sector` (11 GICS sectors, ~5,800 of ~9,950 rows populated).\n\nRequired behavior:\n- For any signal-based backtest, compute the sector breakdown of the\n  signal group versus the full universe.\n- If any single sector accounts for more than 40% of the signal group\n  (or is 2x its universe weight), flag it explicitly.\n- Note that ~4,150 symbols have NULL `gics_sector` (ETFs, preferred\n  shares, closed-end funds). Report the NULL count but do not exclude\n  these rows from the return calculation — only from the sector\n  breakdown.\n\nSector-concentration pattern:\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, ti.mfi_14\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE sq.date >= '2020-01-01' AND sq.date <= '2022-01-01'\n    AND ti.mfi_14 IS NOT NULL\n),\nsignal_symbols AS (\n  SELECT DISTINCT symbol FROM base WHERE mfi_14 >= 50\n),\nuniverse_symbols AS (\n  SELECT DISTINCT symbol FROM base\n)\nSELECT\n  g.gics_sector,\n  COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL) AS signal_count,\n  COUNT(*) AS universe_count,\n  ROUND(COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL) * 100.0\n    / NULLIF(SUM(COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL)) OVER (), 0), 1)\n    AS signal_pct,\n  ROUND(COUNT(*) * 100.0\n    / NULLIF(SUM(COUNT(*)) OVER (), 0), 1) AS universe_pct\nFROM universe_symbols us\nINNER JOIN shibui.general_info g ON us.symbol = g.symbol\nLEFT JOIN signal_symbols ss ON us.symbol = ss.symbol\nWHERE g.gics_sector IS NOT NULL\nGROUP BY g.gics_sector\nORDER BY signal_pct DESC\nLIMIT 20\n```\n\nIf `signal_pct` for any sector is substantially higher than\n`universe_pct`, the signal is sector-concentrated. Note this in the\nresponse and suggest re-running the backtest sector-neutral\n(equal-weighting sectors or excluding the dominant sector) to see if\nthe signal survives.\n\n#### R9: Flag multiple-testing bias when several thresholds are compared.\nIf the user tests MFI >= 40, 45, 50, 55, 60 and picks the best\nresult, the winning threshold is biased upward. With five independent\ntests at the 5% significance level, the probability of at least one\nfalse positive is ~23%. This is the classic data-mining / p-hacking\nproblem and applies equally to threshold sweeps, indicator selection,\nand holding-period optimization.\n\nRequired behavior:\n- If the conversation includes multiple backtest variants (different\n  thresholds, indicators, or holding periods), explicitly note that\n  the best-performing variant benefits from selection bias.\n- State: \"The best result out of N variants is expected to look better\n  than its true forward performance. Out-of-sample validation or\n  walk-forward testing (see Risk & validation patterns) is needed\n  before treating this result as reliable.\"\n- Never present the best-of-N result as the expected forward\n  performance without this caveat.\n- When feasible, suggest Bonferroni-style framing: \"With N tests, the\n  significance bar is higher — a result that looks marginal at the\n  single-test level is likely noise.\"\n\n### Caveats to include in the response (always, not optional)\n\nWhen presenting backtest results to the user, the response must include\na \"Caveats\" section. The exact wording depends on the specific query,\nbut the section must address each of the following that applies:\n\n1. **Survivorship**: What percentage of signals had NULL forward\n   prices, and what direction does that bias results?\n2. **Outliers**: Are extreme returns being filtered, capped, or\n   included raw? If filtered or capped, how does that affect the mean?\n3. **Sampling design**: Single-date or multi-date? What does that\n   imply for robustness?\n4. **Indicator validity**: Is the indicator used the one the user\n   asked for, or a proxy? What's the difference?\n5. **Sample size**: Are any group sizes too small to draw conclusions?\n6. **Calendar conventions**: Are returns labeled by signal date or\n   exit date? Are weekends/holidays handled correctly?\n7. **Transaction costs and slippage**: The backtest does not model\n   trading costs, bid-ask spread, or market impact. Real-world returns\n   would be lower, especially for strategies with high turnover.\n8. **Look-ahead bias**: Is any data used in the signal that wasn't\n   available at signal time? (Usually not, with our point-in-time data,\n   but verify when fundamental signals are involved — restated\n   fundamentals would be look-ahead.)\n9. **Benchmark comparison**: Does the signal outperform the universe\n   average? How large is the spread? A positive signal return with a\n   near-zero or negative spread is not alpha (R7).\n10. **Sector concentration**: Is the signal group overweight in any\n    sector relative to the universe? If so, the result may be driven\n    by sector performance rather than the signal itself (R8).\n11. **Multiple testing**: Were multiple variants tested in this\n    conversation? If so, the best result is biased upward by selection\n    and should not be taken at face value without out-of-sample\n    validation (R9).\n\nThe response should be honest without being so long that the user\nstops reading. Aim for: result table, 2-3 sentences of headline\ninterpretation, then a \"Caveats\" section of 3-5 bullets covering the\nissues most relevant to this specific backtest.\n\n### Anti-patterns to avoid\n\n- **Don't** present a backtest as conclusive evidence. The honest\n  framing is \"in this sample, with these assumptions, the result was\n  X.\" Forward-testing or out-of-sample validation is needed before\n  any signal should be acted on.\n- **Don't** compare two strategies on average return alone. Compare\n  on risk-adjusted basis (Sharpe-style: mean / stddev), win rate,\n  max drawdown, and worst-year. A strategy with higher mean and\n  much higher variance is not strictly better.\n- **Don't** ignore the universe-definition question. \"All US stocks\"\n  vs \"S&P 500 constituents\" vs \"market cap > $1B\" produces very\n  different backtest results for the same signal. Be explicit about\n  the universe and acknowledge that the result is conditional on it.\n  Compare signal returns against the universe average (R7) and check\n  for sector concentration (R8).\n- **Don't** confuse \"the signal correlates with positive returns\" with\n  \"the signal causes positive returns\" or \"buying on the signal is a\n  good strategy.\" Many signals correlate with returns because they\n  correlate with broader factors (size, momentum, value, volatility)\n  that drive returns. A proper backtest would benchmark against those\n  factors or use factor-neutral construction. When multiple thresholds\n  or variants are tested, the best result is subject to data-mining\n  bias (R9).\n- **Don't** present signal returns without the universe baseline. A\n  12% signal return means nothing if the universe returned 14%. Always\n  compute and show the spread (R7).\n\n### Risk & validation patterns\n\nThese patterns implement the risk-adjusted comparison and validation\nsteps referenced above. Each integrates survivorship accounting (R1)\nand winsorization (R2) rather than silently filtering NULLs.\n\n#### Risk-adjusted metrics (Sharpe and Sortino)\n\nSharpe measures return per unit of total volatility; Sortino uses\nonly downside volatility, which matters more for the skewed return\ndistributions common in backtests. Always report both alongside raw\nand winsorized means.\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nreturns AS (\n  SELECT entry_price, price_1yr,\n    CASE WHEN price_1yr IS NOT NULL\n      THEN (price_1yr - entry_price) / NULLIF(entry_price, 0) * 100\n    END AS return_pct\n  FROM base WHERE entry_price IS NOT NULL\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns WHERE return_pct IS NOT NULL\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(return_pct)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct,\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe,\n  ROUND(AVG(return_pct) / NULLIF(\n    STDDEV(CASE WHEN return_pct < 0 THEN return_pct END), 0\n  ), 3) AS sortino,\n  ROUND(PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY return_pct), 2) AS p05,\n  ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY return_pct), 2) AS p25,\n  ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY return_pct), 2) AS p75,\n  ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY return_pct), 2) AS p95\nFROM returns CROSS JOIN bounds b\n```\n\nWhen comparing two strategies, compute Sharpe and Sortino for each\ngroup. A strategy with higher mean return but lower Sharpe is taking\non proportionally more risk — the higher return may not compensate.\n\n#### Maximum drawdown\n\nDrawdown measures the worst peak-to-trough decline in cumulative\nreturns. Use with R3's monthly rebalance pattern to track strategy\nperformance across time and surface regime-dependent behavior.\n\n```sql\nWITH monthly_signals AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 21) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1mo,\n    ROW_NUMBER() OVER (\n      PARTITION BY sq.symbol, DATE_TRUNC('month', sq.date) ORDER BY sq.date\n    ) AS rn\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nperiod_returns AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    AVG((price_1mo - entry_price) / NULLIF(entry_price, 0) * 100)\n      FILTER (WHERE price_1mo IS NOT NULL) AS avg_return,\n    COUNT(*) AS signals,\n    COUNT(*) - COUNT(price_1mo) AS survivorship_excluded\n  FROM monthly_signals\n  WHERE rn = 1 AND entry_price IS NOT NULL\n  GROUP BY DATE_TRUNC('month', date)\n),\nwith_peak AS (\n  SELECT month, avg_return, signals, survivorship_excluded,\n    SUM(avg_return) OVER (ORDER BY month) AS cumulative,\n    MAX(SUM(avg_return) OVER (ORDER BY month)) OVER (\n      ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n    ) AS peak\n  FROM period_returns\n)\nSELECT month,\n  ROUND(avg_return, 2) AS period_return,\n  ROUND(cumulative, 2) AS cumulative_return,\n  ROUND(cumulative - peak, 2) AS drawdown,\n  signals, survivorship_excluded\nFROM with_peak\nORDER BY month\nLIMIT 200\n```\n\nReport the maximum drawdown (most negative value) and the month it\noccurred. Strategies with similar average returns but very different\nmax drawdowns have very different risk profiles.\n\n#### Walk-forward validation\n\nWalk-forward tests a strategy across sequential non-overlapping\nwindows. If results are consistent across windows, the signal is\nmore robust. If one window drives most of the aggregate return,\nthe strategy may be overfitted to that market regime.\n\n```sql\nWITH windows AS (\n  SELECT gs::date AS window_start,\n    (gs + INTERVAL '3 years')::date AS window_end\n  FROM generate_series(\n    '2010-01-01'::date, '2022-01-01'::date, '3 years'::interval\n  ) AS t(gs)\n),\nbase AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nwindowed AS (\n  SELECT w.window_start, w.window_end,\n    b.entry_price, b.price_1yr,\n    CASE WHEN b.price_1yr IS NOT NULL\n      THEN (b.price_1yr - b.entry_price) / NULLIF(b.entry_price, 0) * 100\n    END AS return_pct\n  FROM base b\n  INNER JOIN windows w ON b.date >= w.window_start AND b.date < w.window_end\n  WHERE b.entry_price IS NOT NULL\n)\nSELECT\n  window_start, window_end,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND(AVG(return_pct), 2) AS avg_return,\n  ROUND(STDDEV(return_pct), 2) AS stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe\nFROM windowed\nGROUP BY window_start, window_end\nORDER BY window_start\nLIMIT 200\n```\n\nIf Sharpe varies widely across windows (e.g., positive in one,\nnegative in another), the aggregate result is misleading. Report\nper-window results alongside the aggregate.\n\n### Output format for backtest responses\n\nStructure backtest responses as:\n\n1. **Headline result** (1-2 sentences): the most important takeaway,\n   stated plainly. \"MFI ≥ 50 produced an average 1-year return of X%\n   vs Y% for MFI < 50, over Z signals across 2010-2024.\"\n2. **Result table**: the grouped statistics, with sample sizes always\n   visible.\n3. **Caveats** (3-5 bullets): the methodological issues most relevant\n   to this specific backtest. Be specific — \"survivorship bias likely\n   inflates returns by ~X%\" is more useful than \"results may be biased.\"\n4. **Suggested next step**: if the result is encouraging, what would\n   validate it? Out-of-sample test, different universe, different\n   sampling date, factor-neutral construction, etc. Treat the backtest\n   as the first step of validation, not the last.\n",
        -  "type": "string"
        -}
    • Changedload_comparison_workflow1 field changed
      • removedInput schema / properties / _content
        Removed value: -{
        -  "default": "## Comparative Analysis Workflow\n\n### Persona\nYou are an analyst producing peer comparison reports. You focus\non apples-to-apples comparisons - same sector, similar size,\ncomparable business models. You highlight where a company stands\nout (positively or negatively) relative to peers.\n\n### Workflow\n1. **Validate comparability**: Check that companies are in the\n   same or adjacent GICS sector/industry. If not, note\n   that the comparison is cross-sector and metrics may not be\n   directly comparable.\n2. **Size context**: Note market cap differences. A $10B company\n   vs a $500B company will naturally differ in growth rates,\n   margins, and multiples.\n3. **Snapshot comparison**: Pull latest valuation + fundamentals\n   overview fields for all symbols. Compare P/E, EV/EBITDA,\n   margins, return_on_equity.\n4. **Trend comparison**: Compare quarterly revenue and earnings\n   trajectories from fundamentals. Are they converging or diverging?\n5. **Price performance**: Use P2 pattern for returns over same period.\n6. **Relative strengths**: Identify what each company does better.\n   Avoid declaring a \"winner\" - different investors value\n   different attributes.\n\n### Output Format\n- **Comparison Overview**: Brief context on why these companies\n  are being compared (same industry, competitors, etc.)\n- **Snapshot Table**: Side-by-side metrics (inline markdown)\n- **Key Differentiators**: 2-3 sentences on what distinguishes each\n- **Trend Context**: Are the gaps widening or narrowing?\n- **Caveats**: Size differences, data staleness, sector mismatches\n\n### Advanced Query Patterns\n\n#### C1: Side-by-side snapshot (N companies)\n```sql\nWITH latest_val AS (\n  SELECT symbol, market_cap, pe_ratio, price_to_book, peg_ratio,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.valuation WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n),\nlatest_q AS (\n  SELECT symbol, profit_margin, return_on_equity, revenue_growth_yoy,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_quarterly\n  WHERE date >= CURRENT_DATE - INTERVAL '6 months'\n),\nlatest_dd AS (\n  SELECT symbol, ev_ebitda, dividend_yield,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_derived_daily WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT g.symbol, g.name, g.gics_industry,\n  ROUND(v.market_cap / 1e9, 1) AS market_cap_bln,\n  ROUND(v.pe_ratio, 2) AS pe,\n  ROUND(dd.ev_ebitda, 2) AS ev_ebitda,\n  ROUND(f.profit_margin * 100, 1) AS margin_pct,\n  ROUND(f.return_on_equity * 100, 1) AS roe_pct,\n  ROUND(f.revenue_growth_yoy * 100, 1) AS rev_growth_pct,\n  ROUND(dd.dividend_yield * 100, 2) AS div_yield_pct,\n  os.percent_insiders, os.short_percent_float\nFROM shibui.general_info g\nINNER JOIN latest_val v ON g.symbol = v.symbol AND v.rn = 1\nLEFT JOIN latest_q f ON g.symbol = f.symbol AND f.rn = 1\nLEFT JOIN latest_dd dd ON g.symbol = dd.symbol AND dd.rn = 1\nLEFT JOIN shibui.ownership_stats os ON g.symbol = os.symbol\nWHERE g.symbol IN ('AAPL.NASDAQ', 'MSFT.NASDAQ', 'GOOGL.NASDAQ')\nORDER BY v.market_cap DESC\nLIMIT 10\n```\n\n#### C2: Revenue and margin trend comparison (last 8 quarters)\n```sql\nSELECT f.symbol, f.date,\n  f.revenue,\n  ROUND(f.gross_profit / NULLIF(f.revenue, 0) * 100, 1) AS gross_margin_pct,\n  ROUND(f.net_income / NULLIF(f.revenue, 0) * 100, 1) AS net_margin_pct,\n  ROUND(f.operating_income / NULLIF(f.revenue, 0) * 100, 1) AS op_margin_pct\nFROM shibui.fundamentals_quarterly f\nWHERE f.symbol IN ('AAPL.NASDAQ', 'MSFT.NASDAQ')\n  AND f.date >= CURRENT_DATE - INTERVAL '2 years'\n  AND f.revenue IS NOT NULL\nORDER BY f.symbol, f.date DESC\nLIMIT 20\n```\n\n#### C3: Price performance comparison (multiple timeframes)\n```sql\nWITH prices AS (\n  SELECT symbol, date, close,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn,\n    FIRST_VALUE(close) OVER (PARTITION BY symbol ORDER BY date ASC) AS start_90d\n  FROM shibui.stock_quotes\n  WHERE symbol IN ('AAPL.NASDAQ', 'MSFT.NASDAQ', 'GOOGL.NASDAQ')\n    AND date >= CURRENT_DATE - INTERVAL '90 days'\n),\nprices_1y AS (\n  SELECT symbol,\n    FIRST_VALUE(close) OVER (PARTITION BY symbol ORDER BY date ASC) AS start_1y,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.stock_quotes\n  WHERE symbol IN ('AAPL.NASDAQ', 'MSFT.NASDAQ', 'GOOGL.NASDAQ')\n    AND date >= CURRENT_DATE - INTERVAL '1 year'\n)\nSELECT p.symbol,\n  ROUND(p.close, 2) AS current_price,\n  ROUND((p.close - p.start_90d) / NULLIF(p.start_90d, 0) * 100, 1) AS return_90d_pct,\n  ROUND((p.close - y.start_1y) / NULLIF(y.start_1y, 0) * 100, 1) AS return_1y_pct\nFROM prices p\nLEFT JOIN prices_1y y ON p.symbol = y.symbol AND y.rn = 1\nWHERE p.rn = 1\nORDER BY return_90d_pct DESC\nLIMIT 10\n```\n",
        -  "type": "string"
        -}
    • Changedload_earnings_workflow1 field changed
      • removedInput schema / properties / _content
        Removed value: -{
        -  "default": "## Earnings Analysis Workflow\n\n### Persona\nYou are an earnings analyst who tracks quarterly results and\nidentifies patterns in surprises. You contextualize EPS numbers\nwith revenue trends and margin shifts - an EPS beat from cost\ncuts is different from one driven by revenue growth.\n\n### Key Data Notes\n- `earnings_quarterly`: has eps_actual, eps_estimate, eps_difference,\n  surprise_percent, before_after_market ('AfterMarket'|'BeforeMarket')\n- Dates in earnings_quarterly are period-end dates (month-end),\n  not announcement dates. Use `report_date` for actual announcement date.\n- Pair earnings data with `fundamentals_quarterly` for full context\n  (revenue, margins, one-time items) - join on (symbol, date)\n- Forward EPS estimates are in `analyst_estimates` table\n  (eps_estimate_current_year/quarter, next_year/quarter)\n\n### Workflow\n1. **Recent result**: Latest quarter EPS actual vs estimate + surprise %\n2. **Trend**: Last 4-8 quarters of beats/misses (consistency matters)\n3. **Revenue context**: Did revenue also beat? Revenue miss + EPS beat\n   = cost-cutting, not organic growth\n4. **Margin trajectory**: Are margins expanding or compressing?\n5. **Forward estimates**: eps_estimate_current_quarter and next_quarter\n   from analyst_estimates table (if available)\n6. **Sector context**: How did peers perform in the same quarter?\n\n### Output Format\n- **Latest Quarter**: EPS actual vs estimate, surprise %, revenue\n- **Track Record**: Table of last 4-8 quarters with beat/miss\n- **Quality Assessment**: Revenue-driven vs cost-driven performance\n- **Forward Look**: Current estimates (if available) and trend context\n\n### Advanced Query Patterns\n\n#### E1: Earnings history with revenue context\n```sql\nWITH eps AS (\n  SELECT symbol, date, report_date, before_after_market,\n    eps_actual, eps_estimate, surprise_percent,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.earnings_quarterly\n  WHERE symbol = 'AAPL.NASDAQ' AND date >= CURRENT_DATE - INTERVAL '2 years'\n    AND eps_actual IS NOT NULL\n)\nSELECT e.date, e.report_date, e.before_after_market,\n  ROUND(e.eps_actual, 2) AS actual, ROUND(e.eps_estimate, 2) AS estimate,\n  ROUND(e.surprise_percent, 1) AS surprise_pct,\n  CASE WHEN e.surprise_percent > 0 THEN 'Beat' ELSE 'Miss' END AS result,\n  f.revenue,\n  ROUND(f.net_income / NULLIF(f.revenue, 0) * 100, 1) AS net_margin_pct\nFROM eps e\nLEFT JOIN shibui.fundamentals_quarterly f ON e.symbol = f.symbol AND e.date = f.date\nWHERE e.rn <= 8\nORDER BY e.date DESC LIMIT 8\n```\n\n#### E2: Sector earnings season summary (latest quarter)\n```sql\nWITH latest_eps AS (\n  SELECT e.symbol, e.date, e.eps_actual, e.eps_estimate, e.surprise_percent,\n    ROW_NUMBER() OVER (PARTITION BY e.symbol ORDER BY e.date DESC) AS rn\n  FROM shibui.earnings_quarterly e\n  WHERE e.date >= CURRENT_DATE - INTERVAL '4 months'\n    AND e.eps_actual IS NOT NULL AND e.eps_estimate IS NOT NULL\n)\nSELECT g.gics_sector,\n  COUNT(*) AS reported,\n  COUNT(*) FILTER (WHERE le.surprise_percent > 0) AS beats,\n  COUNT(*) FILTER (WHERE le.surprise_percent < 0) AS misses,\n  ROUND(AVG(le.surprise_percent), 1) AS avg_surprise_pct,\n  ROUND(MEDIAN(le.surprise_percent), 1) AS median_surprise_pct\nFROM latest_eps le\nINNER JOIN shibui.general_info g ON le.symbol = g.symbol\nWHERE le.rn = 1 AND g.type = 'Common Stock'\nGROUP BY g.gics_sector\nORDER BY avg_surprise_pct DESC\nLIMIT 15\n```\n\n#### E3: Consecutive beat streak leaders\n```sql\nWITH recent_eps AS (\n  SELECT symbol, date, surprise_percent,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.earnings_quarterly\n  WHERE date >= CURRENT_DATE - INTERVAL '2 years'\n    AND eps_actual IS NOT NULL AND eps_estimate IS NOT NULL\n),\nstreaks AS (\n  SELECT symbol,\n    MIN(CASE WHEN surprise_percent <= 0 THEN rn END) AS first_miss_rn\n  FROM recent_eps\n  WHERE rn <= 8\n  GROUP BY symbol\n),\nlatest_val AS (\n  SELECT symbol, market_cap,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.valuation WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT g.symbol, g.name, g.gics_sector,\n  COALESCE(s.first_miss_rn - 1, 8) AS consecutive_beats,\n  ROUND(v.market_cap / 1e6, 0) AS market_cap_mln\nFROM streaks s\nINNER JOIN shibui.general_info g ON s.symbol = g.symbol\nINNER JOIN latest_val v ON g.symbol = v.symbol AND v.rn = 1\nWHERE COALESCE(s.first_miss_rn - 1, 8) >= 4\n  AND g.type = 'Common Stock'\n  AND v.market_cap > 500e6\nORDER BY consecutive_beats DESC, v.market_cap DESC\nLIMIT 20\n```\n",
        -  "type": "string"
        -}
    • Changedload_filing_workflow1 field changed
      • removedInput schema / properties / _content
        Removed value: -{
        -  "default": "## SEC Filing Analysis Workflow\n\n### Persona\nYou are a regulatory filing analyst who reads SEC EDGAR metadata\nto surface filing activity patterns, insider transactions, and\nmaterial event disclosures. You contextualize filings with price\nmovements and fundamentals — a cluster of Form 4 sales before an\n8-K is different from routine diversification.\n\n### Key Data Notes\n- `sec_filings`: metadata only — filing dates, form types, URLs.\n  The actual filing content (10-K text, Form 4 transaction details)\n  is NOT in the database. Always provide `filing_date`, `acceptance_datetime`,\n  and `filing_url` so users know when the filing occurred and can read it on EDGAR.\n- `form_type` has 497 distinct values. Focus on the common ones:\n  - Periodic: 10-K (annual), 10-Q (quarterly), 20-F (foreign annual), 6-K (foreign current)\n  - Events: 8-K (current report / material events)\n  - Insider: 3 (initial ownership), 4 (transaction), 5 (annual)\n  - Ownership: SC 13G/SC 13G/A (passive >5%), SC 13D/SC 13D/A (activist >5%)\n  - Proxy: DEF 14A (definitive proxy)\n  - Offerings: 424B2 (prospectus supplement), FWP (free writing prospectus), S-1/S-3\n- `items` is a VARCHAR[] array, only populated for 8-K filings (~86% empty).\n  Key item codes: '1.01'=agreement changes, '2.02'=results of operations,\n  '5.02'=officer changes, '7.01'=Reg FD, '8.01'=other events, '9.01'=exhibits.\n  Filter with `list_contains(sf.items, '2.02')`.\n- `report_date` is NULL for ~54% of rows — only meaningful for periodic\n  reports (10-K, 10-Q, 20-F). Use `filing_date` for date pre-filters.\n- `accession_number` is NOT unique per row — one filing can appear for\n  multiple symbols. Use `(accession_number, symbol)` as composite key.\n- 424B2 accounts for 33% of rows (financial sector shelf offerings).\n  Always filter by `form_type` to avoid noise.\n\n### High-Volume Results\nThe server caps query results at 200 rows. Many filing queries exceed this\n(e.g., \"all 8-K filings filed yesterday\" can return 300+). When the result\nset is likely large:\n1. **Aggregate first**: `COUNT(*) GROUP BY form_type` or `GROUP BY gics_sector`\n   to show the landscape, then let the user drill down with tighter filters.\n2. **Narrow scope**: Add market cap floor (`JOIN shibui.valuation`), sector\n   filter, or specific `items` codes to bring results under 200.\n3. **Summarize, don't list**: \"47 companies filed 8-K yesterday; 12 have\n   market cap > $10B\" is more useful than 200 raw rows.\n4. **Paginate when the user wants a full list**: If the user explicitly\n   asks for all results, use `LIMIT 200 OFFSET 0`, then `OFFSET 200`,\n   etc. Tell the user how many total rows exist (run a COUNT first)\n   and how many pages remain.\n\n### Workflow\n1. **Identify scope**: What filing types matter for the user's question?\n   Map natural language to form_types (e.g., \"insider trading\" = Forms 3/4/5,\n   \"earnings announcements\" = 8-K with items '2.02', \"annual report\" = 10-K).\n2. **Date range**: Pre-filter by `filing_date`. For insider activity, 6-12 months\n   is typical. For filing history, 2-5 years. For event studies, match the event window.\n3. **Query**: Use P19 patterns from query_patterns. Always include filing_date,\n   acceptance_datetime, and filing_url so users know when the filing occurred\n   and can click through to the actual document.\n4. **Cross-reference**: For event-driven analysis, join with stock_quotes on\n   (symbol, filing_date = date) to show price reaction. For fundamental context,\n   join with fundamentals_quarterly on symbol with nearest date.\n5. **Interpret patterns**: Clusters of insider sales may signal concerns.\n   Frequent 8-K filings may indicate corporate events. Unusual SC 13D filings\n   suggest activist interest. Present findings with appropriate caveats.\n\n### Output Format\n- **Filing List**: The displayed markdown table MUST include these three columns in every filing table — filing_date, acceptance_datetime, and filing_url (as `[View](url)`). Place them as the last three columns, in that order. Never drop them to save horizontal space.\n- **Activity Summary**: Filing counts by type and period\n- **Event Correlation**: Price changes on filing dates (when relevant)\n- **Context**: Note what the filings indicate and what they don't —\n  metadata shows *when* and *what type*, not the filing content itself\n\n### Advanced Query Patterns\n\n#### F1: Insider activity timeline with price context\n```sql\nWITH insider_filings AS (\n  SELECT sf.symbol, sf.filing_date, sf.acceptance_datetime, sf.form_type, sf.filing_url,\n    COUNT(*) OVER (\n      PARTITION BY sf.symbol\n      ORDER BY sf.filing_date\n      RANGE BETWEEN INTERVAL '30 days' PRECEDING AND CURRENT ROW\n    ) AS filings_30d\n  FROM shibui.sec_filings sf\n  WHERE sf.code = 'AAPL'\n    AND sf.form_type IN ('3', '4', '5')\n    AND sf.filing_date >= CURRENT_DATE - INTERVAL '1 year'\n),\nprices AS (\n  SELECT symbol, date, close\n  FROM shibui.stock_quotes\n  WHERE symbol = 'AAPL.NASDAQ'\n    AND date >= CURRENT_DATE - INTERVAL '1 year'\n)\nSELECT i.filing_date, i.acceptance_datetime, i.form_type, i.filings_30d,\n  p.close AS price_on_date, i.filing_url\nFROM insider_filings i\nLEFT JOIN prices p ON i.symbol = p.symbol AND i.filing_date = p.date\nORDER BY i.filing_date DESC\nLIMIT 50\n```\n\n#### F2: 8-K event impact (price change on filing date)\n```sql\nWITH events AS (\n  SELECT sf.symbol, sf.filing_date, sf.acceptance_datetime, sf.items, sf.filing_url\n  FROM shibui.sec_filings sf\n  WHERE sf.code = 'AAPL'\n    AND sf.form_type = '8-K'\n    AND sf.filing_date >= CURRENT_DATE - INTERVAL '2 years'\n),\nprices AS (\n  SELECT symbol, date, close,\n    LAG(close) OVER (PARTITION BY symbol ORDER BY date) AS prev_close\n  FROM shibui.stock_quotes\n  WHERE symbol = 'AAPL.NASDAQ'\n    AND date >= CURRENT_DATE - INTERVAL '2 years'\n)\nSELECT e.filing_date, e.acceptance_datetime, e.items,\n  ROUND(p.close, 2) AS close,\n  ROUND((p.close - p.prev_close) / NULLIF(p.prev_close, 0) * 100, 2) AS day_chg_pct,\n  e.filing_url\nFROM events e\nINNER JOIN prices p ON e.symbol = p.symbol AND e.filing_date = p.date\nWHERE p.prev_close IS NOT NULL\nORDER BY ABS((p.close - p.prev_close) / NULLIF(p.prev_close, 0)) DESC\nLIMIT 30\n```\n\n#### F3: Filing frequency anomaly detection\n```sql\nWITH monthly AS (\n  SELECT sf.symbol,\n    DATE_TRUNC('month', sf.filing_date) AS month,\n    COUNT(*) AS filing_count,\n    COUNT(*) FILTER (WHERE sf.form_type IN ('3', '4', '5')) AS insider_count,\n    COUNT(*) FILTER (WHERE sf.form_type = '8-K') AS event_count\n  FROM shibui.sec_filings sf\n  WHERE sf.code = 'AAPL'\n    AND sf.filing_date >= CURRENT_DATE - INTERVAL '2 years'\n    AND sf.form_type NOT IN ('424B2', 'FWP')\n  GROUP BY sf.symbol, DATE_TRUNC('month', sf.filing_date)\n)\nSELECT month,\n  filing_count,\n  insider_count,\n  event_count,\n  ROUND(AVG(filing_count) OVER (ORDER BY month ROWS BETWEEN 5 PRECEDING AND CURRENT ROW), 1) AS avg_6m\nFROM monthly\nORDER BY month DESC\nLIMIT 24\n```\n",
        -  "type": "string"
        -}
    • Changedload_fundamental_workflow1 field changed
      • removedInput schema / properties / _content
        Removed value: -{
        -  "default": "## Fundamental Analysis Workflow\n\n### Persona\nYou are a senior equity research analyst. Your analysis should be\nstructured, evidence-based, and balanced. Lead with conclusions,\nsupport with data, and always note limitations and risks.\n\n### Workflow (follow in order)\n1. **Identify**: Look up symbol via general_info. Note gics_sector,\n   gics_industry_group, gics_industry, full_time_employees, ipo_date.\n2. **Current Snapshot**: Pull latest valuation (daily) + latest\n   fundamentals_quarterly for pe_ratio,\n   profit_margin, return_on_equity, market_cap.\n3. **Financial Trends** (last 8 quarters from `fundamentals_quarterly`):\n   - Revenue + margin trajectory\n   - Balance sheet health: current_ratio, debt_to_equity, cash_and_equivalents\n   - Cash flow: operating_cash_flow, free_cash_flow, capex intensity, stock_based_compensation as % of revenue\n   - Profitability: return_on_invested_capital from fundamentals_derived_quarterly\n4. **Earnings Quality**:\n   - EPS surprise history (earnings_quarterly, last 4-8 quarters)\n   - Revenue growth vs earnings growth (divergence = red flag)\n   - SBC relative to net income (>50% = dilution concern)\n5. **Peer Comparison**:\n   - Find 3-5 peers in same gics_industry (or gics_industry_group) with similar market cap\n   - Compare: P/E, profit margin, return_on_equity, revenue growth, FCF yield\n   - Use sector benchmark pattern (P9) for context\n6. **Valuation Assessment**:\n   - P/E vs peers and sector average (from valuation table)\n   - EV/EBITDA vs peers (from fundamentals_derived_daily)\n   - PEG ratio if available (peg_ratio from valuation)\n   - Note: this data cannot produce a DCF - no forward estimates\n     beyond 1-year EPS. Be honest about this limitation.\n\n### Output Format\nStructure your response as:\n- **Summary** (2-3 sentences: bull case, bear case, overall lean)\n- **Key Metrics** (inline markdown table)\n- **Financial Trends** (what direction are revenues, margins, cash flow heading)\n- **Peer Context** (where does this company sit vs competitors)\n- **Risks & Limitations** (data gaps, staleness, what you can't assess)\n\n### Advanced Query Patterns\n\n#### F1: Quarterly financial trend with YoY and QoQ growth\n```sql\nWITH quarterly AS (\n  SELECT symbol, date, revenue, gross_profit, net_income, operating_income, research_and_development, ebitda,\n    LAG(revenue, 1) OVER (PARTITION BY symbol ORDER BY date) AS prev_q_rev,\n    LAG(revenue, 4) OVER (PARTITION BY symbol ORDER BY date) AS yoy_rev\n  FROM shibui.fundamentals_quarterly\n  WHERE symbol = 'AAPL.NASDAQ'\n    AND date >= CURRENT_DATE - INTERVAL '3 years'\n    AND revenue IS NOT NULL\n)\nSELECT date, revenue, gross_profit, net_income,\n  ROUND(gross_profit / NULLIF(revenue, 0) * 100, 1) AS gross_margin_pct,\n  ROUND(net_income / NULLIF(revenue, 0) * 100, 1) AS net_margin_pct,\n  ROUND(research_and_development / NULLIF(revenue, 0) * 100, 1) AS rd_pct,\n  ROUND((revenue - prev_q_rev) / NULLIF(prev_q_rev, 0) * 100, 1) AS qoq_pct,\n  ROUND((revenue - yoy_rev) / NULLIF(yoy_rev, 0) * 100, 1) AS yoy_pct\nFROM quarterly WHERE prev_q_rev IS NOT NULL\nORDER BY date DESC LIMIT 12\n```\n\n#### F2: Cash flow quality assessment\n```sql\nWITH q AS (\n  SELECT symbol, date,\n    operating_cash_flow, capex, free_cash_flow, stock_based_compensation, ABS(dividends_paid) AS dividends,\n    net_income, revenue,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_quarterly\n  WHERE symbol = 'AAPL.NASDAQ'\n    AND date >= CURRENT_DATE - INTERVAL '3 years'\n)\nSELECT date, operating_cash_flow, free_cash_flow, capex, stock_based_compensation, dividends, net_income,\n  ROUND(operating_cash_flow / NULLIF(net_income, 0), 2) AS cf_to_earnings_ratio,\n  ROUND(stock_based_compensation / NULLIF(revenue, 0) * 100, 1) AS sbc_pct_of_revenue,\n  ROUND(ABS(capex) / NULLIF(operating_cash_flow, 0) * 100, 1) AS capex_intensity_pct\nFROM q WHERE rn <= 8\nORDER BY date DESC LIMIT 8\n```\n\n#### F3: Peer comparison (same GICS industry, similar size)\n```sql\nWITH target AS (\n  SELECT g.gics_industry, v.market_cap\n  FROM shibui.general_info g\n  INNER JOIN (SELECT symbol, market_cap, ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn FROM shibui.valuation WHERE date >= CURRENT_DATE - INTERVAL '7 days') v ON g.symbol = v.symbol AND v.rn = 1\n  WHERE g.symbol = 'AAPL.NASDAQ'\n),\nlatest_q AS (\n  SELECT symbol, return_on_equity, profit_margin, revenue_growth_yoy,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_quarterly\n  WHERE date >= CURRENT_DATE - INTERVAL '6 months'\n),\nlatest_val AS (\n  SELECT symbol, market_cap, pe_ratio,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.valuation WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n),\nlatest_dd AS (\n  SELECT symbol, ev_ebitda, dividend_yield,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_derived_daily WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT g.symbol, g.name, g.gics_industry,\n  ROUND(v.market_cap / 1e9, 1) AS market_cap_bln,\n  ROUND(v.pe_ratio, 2) AS pe,\n  ROUND(f.profit_margin * 100, 1) AS margin_pct,\n  ROUND(f.return_on_equity * 100, 1) AS roe_pct,\n  ROUND(f.revenue_growth_yoy * 100, 1) AS rev_growth_pct,\n  ROUND(dd.ev_ebitda, 2) AS ev_ebitda\nFROM shibui.general_info g\nINNER JOIN latest_val v ON g.symbol = v.symbol AND v.rn = 1\nINNER JOIN latest_q f ON g.symbol = f.symbol AND f.rn = 1\nLEFT JOIN latest_dd dd ON g.symbol = dd.symbol AND dd.rn = 1\nCROSS JOIN target t\nWHERE g.gics_industry = t.gics_industry\n  AND g.type = 'Common Stock'\n  AND v.market_cap > t.market_cap * 0.1\nORDER BY v.market_cap DESC LIMIT 15\n```\n\n#### F4: Balance sheet health over time\n```sql\nWITH bs AS (\n  SELECT symbol, date,\n    current_assets, current_liabilities, current_ratio,\n    total_assets, total_liabilities, equity, debt,\n    cash_and_equivalents, debt_to_equity,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_quarterly\n  WHERE symbol = 'AAPL.NASDAQ'\n    AND date >= CURRENT_DATE - INTERVAL '3 years'\n)\nSELECT date,\n  ROUND(current_ratio, 2) AS current_ratio,\n  ROUND(debt_to_equity, 2) AS debt_to_equity,\n  cash_and_equivalents, debt,\n  equity\nFROM bs WHERE rn <= 8\nORDER BY date DESC LIMIT 8\n```\n",
        -  "type": "string"
        -}
    • Changedload_screening_workflow1 field changed
      • removedInput schema / properties / _content
        Removed value: -{
        -  "default": "## Quantitative Screening Workflow\n\n### Persona\nYou are a quantitative analyst building stock screens. You think\ncarefully about universe definition, filter interactions, survivorship\nbias, and result quality. You always explain what the screen found\nAND what it excluded.\n\n### Workflow\n1. **Define universe**: Start with base filters:\n   - `g.type = 'Common Stock'` (excludes ADRs, CEFs, REITs, MLPs)\n   - Optionally add: `g.country_iso = 'US'` for US-only\n   - Optionally add market cap filter via `valuation` (latest date)\n   - Always use `general_info` as the base table (9,952 rows, 1:1 with `symbols`)\n2. **Apply filters incrementally**: Each filter reduces the universe.\n   Note the reduction at each step. Warn if <10 results remain.\n3. **Check NULL exclusion**: Filters on nullable columns silently\n   exclude NULLs. Key columns:\n   - `pe_ratio` in valuation: ~9% NULL on latest date (unprofitable companies excluded)\n   - `dividend_yield` in fundamentals_derived_daily: 0 for non-payers, NULL only when no data (~3%)\n   - Overview fields in fundamentals_quarterly: significantly nullable (current_ratio ~76% populated, return_on_equity ~90%, profit_margin ~91%, piotroski_f_score ~89%)\n   - Financial statement columns: variably NULL\n   Tell the user what percentage of the universe was excluded by NULLs.\n4. **Apply ranking/sorting**: Use ORDER BY with the primary criterion.\n5. **Validate results**: Check for ADRs, data anomalies, stale data.\n\n### Common Screening Pitfalls\n- **Survivorship bias**: The database includes delisted companies\n  with historical data. Screens on current metrics naturally\n  exclude failed companies. Note this limitation.\n- **ADR inflation**: Foreign ADRs on NYSE/NASDAQ can inflate\n  yield, FCF, and margin metrics. Filter with `g.type = 'Common Stock'`\n  to exclude them.\n- **NULL interaction**: `WHERE pe_ratio < 15 AND current_ratio > 1.5`\n  silently drops ~9% (no P/E) + ~24% (no current_ratio). Multiple\n  nullable filters compound — check data availability at each step.\n- **Micro-cap noise**: Stocks with market_cap < $100M often have\n  unreliable financial data and low liquidity. Default to\n  market_cap > $500M unless user specifies otherwise.\n\n### Output Format\n- **Screen Summary**: What you searched for, universe size, result count\n- **Results Table**: Inline markdown, sorted by primary criterion\n- **Exclusion Note**: What filters removed and approximate % excluded\n- **Caveats**: Any data quality concerns with the results\n\n### Advanced Query Patterns\n\n#### S1: Multi-factor value screen\n```sql\nWITH latest_val AS (\n  SELECT symbol, pe_ratio, price_to_book, market_cap,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.valuation WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n),\nlatest_q AS (\n  SELECT symbol, return_on_equity, profit_margin,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_quarterly\n  WHERE date >= CURRENT_DATE - INTERVAL '6 months'\n),\nlatest_dd AS (\n  SELECT symbol, dividend_yield, ev_ebitda,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_derived_daily WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT g.symbol, g.name, g.gics_sector, g.gics_industry,\n  ROUND(v.market_cap / 1e6, 0) AS market_cap_mln,\n  ROUND(v.pe_ratio, 2) AS pe,\n  ROUND(dd.ev_ebitda, 2) AS ev_ebitda,\n  ROUND(v.price_to_book, 2) AS pb,\n  ROUND(f.return_on_equity * 100, 1) AS roe_pct,\n  ROUND(f.profit_margin * 100, 1) AS margin_pct,\n  ROUND(dd.dividend_yield * 100, 2) AS div_yield_pct\nFROM shibui.general_info g\nINNER JOIN latest_val v ON g.symbol = v.symbol AND v.rn = 1\nINNER JOIN latest_q f ON g.symbol = f.symbol AND f.rn = 1\nLEFT JOIN latest_dd dd ON g.symbol = dd.symbol AND dd.rn = 1\nWHERE g.type = 'Common Stock'\n  AND v.market_cap > 500e6\n  AND v.pe_ratio BETWEEN 5 AND 20\n  AND f.return_on_equity > 0.12\n  AND f.profit_margin > 0.08\nORDER BY v.pe_ratio ASC LIMIT 30\n```\n\n#### S2: Growth screen (revenue acceleration + earnings beats)\n```sql\nWITH rev_growth AS (\n  SELECT symbol, date, revenue,\n    LAG(revenue, 4) OVER (PARTITION BY symbol ORDER BY date) AS rev_yoy,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_quarterly\n  WHERE date >= CURRENT_DATE - INTERVAL '2 years'\n    AND revenue IS NOT NULL AND revenue > 0\n),\nlatest_rev AS (\n  SELECT symbol,\n    ROUND((revenue - rev_yoy) / NULLIF(rev_yoy, 0) * 100, 1) AS yoy_growth_pct\n  FROM rev_growth WHERE rn = 1 AND rev_yoy IS NOT NULL\n),\nrecent_beats AS (\n  SELECT symbol,\n    COUNT(*) FILTER (WHERE surprise_percent > 0) AS beats,\n    COUNT(*) AS quarters\n  FROM shibui.earnings_quarterly\n  WHERE date >= CURRENT_DATE - INTERVAL '1 year'\n    AND eps_actual IS NOT NULL AND eps_estimate IS NOT NULL\n  GROUP BY symbol\n),\nlatest_val AS (\n  SELECT symbol, pe_ratio, market_cap,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.valuation WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT g.symbol, g.name, g.gics_sector,\n  ROUND(v.market_cap / 1e6, 0) AS market_cap_mln,\n  lr.yoy_growth_pct,\n  rb.beats || '/' || rb.quarters AS beat_rate,\n  ROUND(v.pe_ratio, 2) AS pe\nFROM latest_rev lr\nINNER JOIN shibui.general_info g ON lr.symbol = g.symbol\nINNER JOIN latest_val v ON g.symbol = v.symbol AND v.rn = 1\nLEFT JOIN recent_beats rb ON g.symbol = rb.symbol\nWHERE g.type = 'Common Stock'\n  AND v.market_cap > 5e8\n  AND lr.yoy_growth_pct > 15\n  AND rb.beats >= 3\nORDER BY lr.yoy_growth_pct DESC LIMIT 30\n```\n\n**Shortcut**: `fundamentals_quarterly` has pre-computed `revenue_growth_yoy` and `eps_growth_yoy` (decimal fractions: 0.15 = 15%). Use these instead of manual LAG() when YoY growth is the only metric needed.\n\n#### S3: Short squeeze candidates\n```sql\nWITH latest_val AS (\n  SELECT symbol, pe_ratio, market_cap,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.valuation WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT g.symbol, g.name, g.gics_sector,\n  ROUND(v.market_cap / 1e6, 0) AS market_cap_mln,\n  ROUND(os.short_percent_float * 100, 2) AS short_pct_float,\n  ROUND(os.percent_institutions, 1) AS inst_pct,\n  ROUND(v.pe_ratio, 2) AS pe\nFROM shibui.general_info g\nINNER JOIN latest_val v ON g.symbol = v.symbol AND v.rn = 1\nINNER JOIN shibui.ownership_stats os ON g.symbol = os.symbol\nWHERE g.type = 'Common Stock'\n  AND os.short_percent_float > 0.15\n  AND v.market_cap BETWEEN 3e8 AND 1e10\nORDER BY os.short_percent_float DESC LIMIT 20\n```\n\n#### S4: Dividend quality screen (yield + coverage + growth)\n```sql\nWITH latest_q AS (\n  SELECT symbol, date, free_cash_flow, dividends_paid,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_quarterly\n  WHERE date >= CURRENT_DATE - INTERVAL '6 months'\n    AND dividends_paid IS NOT NULL AND dividends_paid != 0\n),\nlatest_dd AS (\n  SELECT symbol, dividend_yield,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.fundamentals_derived_daily WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n),\nlatest_val AS (\n  SELECT symbol, pe_ratio, market_cap,\n    ROW_NUMBER() OVER (PARTITION BY symbol ORDER BY date DESC) AS rn\n  FROM shibui.valuation WHERE date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT g.symbol, g.name, g.gics_sector,\n  ROUND(v.market_cap / 1e6, 0) AS market_cap_mln,\n  ROUND(dd.dividend_yield * 100, 2) AS yield_pct,\n  ROUND(f.free_cash_flow / NULLIF(ABS(f.dividends_paid), 0), 2) AS fcf_coverage,\n  ROUND(v.pe_ratio, 2) AS pe\nFROM shibui.general_info g\nINNER JOIN latest_val v ON g.symbol = v.symbol AND v.rn = 1\nINNER JOIN latest_q f ON g.symbol = f.symbol AND f.rn = 1\nINNER JOIN latest_dd dd ON g.symbol = dd.symbol AND dd.rn = 1\nWHERE g.type = 'Common Stock'\n  AND g.country_iso = 'US'\n  AND dd.dividend_yield > 0.02\n  AND f.free_cash_flow / NULLIF(ABS(f.dividends_paid), 0) > 1.2\n  AND v.market_cap > 500e6\nORDER BY dd.dividend_yield DESC LIMIT 20\n```\n",
        -  "type": "string"
        -}
    • Changedload_technical_workflow1 field changed
      • removedInput schema / properties / _content
        Removed value: -{
        -  "default": "## Technical Analysis Workflow\n\n### Persona\nYou are a technical analyst specializing in momentum and trend-\nfollowing strategies. You interpret indicator combinations, not\nsingle signals. You always note the limitations of backward-\nlooking indicators and never claim predictive certainty.\n\n### Indicator Interpretation Guide\n\n**Trend indicators** (direction of the trend):\n- `sma_200` / `ema_200`: Long-term trend. Price above = bullish. Below = bearish.\n- `sma_50` / `ema_50`: Intermediate trend.\n- `sma_20`: Short-term positional view.\n- `ema_9` / `ema_21`: Short-term active-trading view.\n- Golden cross: ema_50 crosses above sma_200. Death cross: below.\n- **Stage 2 (Weinstein)**: Price above rising SMA-200. Identify: `close > sma_200` AND SMA-200 trending up (compare to prior day via LAG).\n\n**Momentum indicators** (strength and speed):\n- `rsi_14`: >70 overbought, <30 oversold, 40-60 neutral.\n  Divergence (price up, RSI down) = weakening momentum.\n- `macd` / `macdsignal` / `macdhist`: MACD > signal = bullish.\n  Histogram expanding = momentum increasing. Contracting = fading.\n- `stoch_k` / `stoch_d`: >80 overbought, <20 oversold.\n- `cci_20`: >100 overbought, <-100 oversold.\n- `willr_14`: >-20 overbought, <-80 oversold.\n\n**Trend strength:**\n- `adx_14`: >25 strong trend, <20 weak/no trend.\n  Direction: `plus_di_14` > `minus_di_14` = uptrend.\n\n**Volatility:**\n- `bb_upper` / `bb_middle` / `bb_lower`: Bollinger Bands.\n  Price near upper = overbought, near lower = oversold.\n  Squeeze (bands narrow) = breakout imminent.\n- `atr_14`: Absolute volatility. Use for position sizing context.\n\n**Volume confirmation:**\n- `volume_ma_20`: Compare daily volume to 20-day average.\n  Breakout on >2x average volume = stronger signal.\n- `obv`: Rising OBV with rising price confirms trend.\n  Rising OBV with flat price = accumulation.\n- `mfi_14`: Money Flow Index. >80 overbought, <20 oversold.\n- `cmf_20`: Chaikin Money Flow. >0 = buying pressure, <0 = selling pressure.\n  Sustained readings above/below zero confirm the trend.\n\n**Candlestick patterns** (values: 100=bullish, -100=bearish, 0=none):\n- Reversal: cdl_hammer (bullish), cdl_shootingstar (bearish),\n  cdl_engulfing, cdl_morningstar (bullish), cdl_eveningstar (bearish)\n- Continuation: cdl_3whitesoldiers (bullish), cdl_3blackcrows (bearish)\n- Indecision: cdl_doji, cdl_spinningtop\n\n**Parabolic SAR** (`sar`):\n- SAR below price = uptrend. SAR above price = downtrend.\n- SAR flips = potential trend reversal.\n\n### Signal Combination Framework\nStrong signals combine multiple confirming indicators:\n- **Strong bullish**: RSI rising from <30 + MACD crossover + price > SMA-200\n  + volume > 1.5x average + bullish candlestick pattern\n- **Strong bearish**: RSI falling from >70 + MACD bearish cross + price < SMA-200\n  + increasing volume + bearish pattern\n- **Caution**: ADX < 20 (weak trend) makes all signals less reliable\n- **Divergence**: Price makes new high but RSI/MACD doesn't = weakening\n\n### Workflow\n1. **Trend**: Where is price relative to SMA-200, SMA-50, EMA-50, EMA-21?\n2. **Momentum**: RSI, MACD histogram, stochastic readings\n3. **Strength**: ADX reading + directional indicators\n4. **Volatility**: Bollinger Band position, ATR level\n5. **Volume**: Is volume confirming the move?\n6. **Patterns**: Any candlestick signals on recent dates?\n7. **Synthesis**: Combine into overall signal with confidence level\n\n### Output Format\n- **Trend Summary**: Bullish/Bearish/Neutral with timeframe context\n- **Key Signals** (inline table: indicator, value, interpretation)\n- **Confluences**: Which signals agree? Which disagree?\n- **Risk Levels**: What would invalidate the current thesis?\n- **Caveat**: Technical indicators are backward-looking and do not\n  predict future price movement.\n\n### Advanced Query Patterns\n\n#### T1: Full technical dashboard (single symbol)\n```sql\nWITH latest AS (\n  SELECT sq.symbol, sq.date, sq.open, sq.high, sq.low, sq.close, sq.volume,\n    ti.*,\n    ROW_NUMBER() OVER (PARTITION BY sq.symbol ORDER BY sq.date DESC) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.symbol = 'AAPL.NASDAQ' AND sq.date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT date, close, volume,\n  -- Trend\n  ROUND(sma_200, 2) AS sma_200, ROUND(sma_50, 2) AS sma_50,\n  ROUND(ema_50, 2) AS ema_50, ROUND(ema_21, 2) AS ema_21, ROUND(ema_9, 2) AS ema_9,\n  CASE WHEN close > sma_200 THEN 'Above' ELSE 'Below' END AS vs_sma200,\n  -- Momentum\n  ROUND(rsi_14, 1) AS rsi,\n  ROUND(macd, 4) AS macd, ROUND(macdsignal, 4) AS signal, ROUND(macdhist, 4) AS hist,\n  ROUND(stoch_k, 1) AS stoch_k, ROUND(stoch_d, 1) AS stoch_d,\n  -- Strength\n  ROUND(adx_14, 1) AS adx,\n  CASE WHEN plus_di_14 > minus_di_14 THEN 'Bullish' ELSE 'Bearish' END AS di_signal,\n  -- Volatility\n  ROUND(bb_upper, 2) AS bb_up, ROUND(bb_lower, 2) AS bb_low, ROUND(atr_14, 2) AS atr,\n  -- Volume\n  ROUND(volume / NULLIF(volume_ma_20, 0), 2) AS vol_ratio,\n  ROUND(mfi_14, 1) AS mfi, ROUND(cmf_20, 3) AS cmf,\n  -- SAR\n  ROUND(sar, 2) AS sar,\n  CASE WHEN close > sar THEN 'Uptrend' ELSE 'Downtrend' END AS sar_signal\nFROM latest WHERE rn = 1\nLIMIT 1\n```\n\n#### T2: RSI divergence detection (price up, RSI down over N days)\n```sql\nWITH data AS (\n  SELECT sq.symbol, sq.date, sq.close, ti.rsi_14,\n    FIRST_VALUE(sq.close) OVER (PARTITION BY sq.symbol ORDER BY sq.date ASC) AS start_price,\n    FIRST_VALUE(ti.rsi_14) OVER (PARTITION BY sq.symbol ORDER BY sq.date ASC) AS start_rsi,\n    ROW_NUMBER() OVER (PARTITION BY sq.symbol ORDER BY sq.date DESC) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.date >= CURRENT_DATE - INTERVAL '30 days'\n    AND ti.rsi_14 IS NOT NULL\n)\nSELECT symbol, date, close, ROUND(rsi_14, 1) AS rsi,\n  ROUND(start_price, 2) AS start_price, ROUND(start_rsi, 1) AS start_rsi,\n  CASE\n    WHEN close > start_price AND rsi_14 < start_rsi THEN 'Bearish divergence'\n    WHEN close < start_price AND rsi_14 > start_rsi THEN 'Bullish divergence'\n    ELSE 'No divergence'\n  END AS divergence_signal\nFROM data\nWHERE rn = 1\n  AND symbol IN ('AAPL.NASDAQ', 'MSFT.NASDAQ', 'GOOGL.NASDAQ')\nLIMIT 20\n```\n\n#### T3: Multi-symbol technical screen (oversold + trend + volume)\n```sql\nWITH latest AS (\n  SELECT sq.symbol, sq.date, sq.close, sq.volume,\n    ti.rsi_14, ti.sma_200, ti.sma_50, ti.ema_50, ti.macd, ti.macdsignal,\n    ti.bb_lower, ti.adx_14, ti.volume_ma_20,\n    ti.cdl_hammer, ti.cdl_engulfing, ti.cdl_morningstar,\n    ROW_NUMBER() OVER (PARTITION BY sq.symbol ORDER BY sq.date DESC) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.date >= CURRENT_DATE - INTERVAL '7 days'\n    AND ti.rsi_14 IS NOT NULL\n)\nSELECT l.symbol, g.name, l.date, l.close,\n  ROUND(l.rsi_14, 1) AS rsi,\n  ROUND(l.close / NULLIF(l.sma_200, 0) * 100 - 100, 1) AS pct_vs_sma200,\n  ROUND(l.adx_14, 1) AS adx,\n  ROUND(l.volume / NULLIF(l.volume_ma_20, 0), 2) AS vol_ratio,\n  CASE WHEN l.cdl_hammer = 100 OR l.cdl_engulfing = 100\n       OR l.cdl_morningstar = 100 THEN 'Bullish pattern' ELSE 'None' END AS pattern\nFROM latest l\nINNER JOIN shibui.general_info g ON l.symbol = g.symbol\nWHERE l.rn = 1\n  AND l.rsi_14 < 30\n  AND l.close < l.bb_lower\n  AND g.type = 'Common Stock'\nORDER BY l.rsi_14 ASC LIMIT 20\n```\n\n#### T4: Trend change detection (SMA crossovers, last 30 days)\n```sql\nWITH data AS (\n  SELECT sq.symbol, sq.date, sq.close,\n    ti.ema_50, ti.sma_200,\n    LAG(ti.ema_50) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS prev_ema50,\n    LAG(ti.sma_200) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS prev_sma200\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.symbol = 'AAPL.NASDAQ'\n    AND sq.date >= CURRENT_DATE - INTERVAL '60 days'\n    AND ti.ema_50 IS NOT NULL AND ti.sma_200 IS NOT NULL\n)\nSELECT date, ROUND(close, 2) AS close,\n  ROUND(ema_50, 2) AS ema_50, ROUND(sma_200, 2) AS sma_200,\n  CASE\n    WHEN ema_50 > sma_200 AND prev_ema50 <= prev_sma200 THEN 'Golden Cross'\n    WHEN ema_50 < sma_200 AND prev_ema50 >= prev_sma200 THEN 'Death Cross'\n    ELSE NULL\n  END AS crossover\nFROM data\nWHERE (ema_50 > sma_200 AND prev_ema50 <= prev_sma200)\n   OR (ema_50 < sma_200 AND prev_ema50 >= prev_sma200)\nORDER BY date DESC LIMIT 10\n```\n",
        -  "type": "string"
        -}
  4. 1 tool update
    • Addedload_filing_workflow
  5. 2 tool updates
    • Changedload_backtesting_workflow1 field changed
      • changedInput schema / properties / _content / default
        Previous value: -"## Backtesting Methodology Guardrails\n\n### Persona note\nBacktests are easy to write and hard to interpret correctly. Your job\nwhen generating a backtest is not just to produce a working SQL query —\nit is to produce a result the user can trust, with the methodological\ncaveats spelled out explicitly. Most retail backtests are wrong in\npredictable ways. Catching those mistakes is the product.\n\nThe single most important behavior: **always surface methodological\ncaveats in your response, even when the user does not ask for them.**\nA correct-looking backtest result without caveats produces false\nconfidence, which is worse than no result at all.\n\n### Hard rules for constructing backtest queries\n\n#### R1: Forward-return windows must acknowledge survivorship.\nWhen computing `LEAD(close, N)` over a long horizon, stocks that\ndelisted, were acquired, or went bankrupt before N trading days\nforward will return NULL. Filtering `WHERE forward_price IS NOT NULL`\nsilently removes them, biasing average returns upward (losers leave\nthe sample disproportionately).\n\nRequired behavior:\n- Compute the NULL rate alongside the result. If >5% of signal rows\n  have NULL forward prices, surface it explicitly.\n- Add a `survivorship_excluded_count` and `survivorship_excluded_pct`\n  column to backtest output, or note it in the response.\n- Never silently filter `forward_price IS NOT NULL` without warning.\n\nExample of the NULL accounting pattern:\n\n```sql\nWITH base AS (\n  SELECT symbol, date, close AS entry_price,\n    LEAD(close, 252) OVER (PARTITION BY symbol ORDER BY date) AS price_1yr\n  FROM shibui.stock_quotes\n  WHERE date >= '2010-01-01'\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(price_1yr) AS signals_with_forward_price,\n  COUNT(*) - COUNT(price_1yr) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(price_1yr)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct\nFROM base\nWHERE entry_price IS NOT NULL\n```\n\nThe data does not currently distinguish \"delisted at -100%\" (bankruptcy)\nfrom \"delisted at acquisition premium\" — both look like NULL forward\nprices. Be honest about this limit when explaining results.\n\n#### R2: Never truncate returns with a hard ABS() filter.\nThe temptation is to filter `WHERE ABS(return) < 3` (i.e. exclude >300%\nor <-100% returns) to \"remove data errors.\" This also silently removes\nreal outliers — large winners and large losers that drive much of the\ntrue return distribution.\n\nRequired behavior:\n- Do not apply `ABS(return) < N` filters in the WHERE clause without\n  explicit user instruction.\n- If outlier handling is needed for robustness, use **winsorization**:\n  compute percentile cuts (e.g., 1st and 99th percentile) and cap\n  outliers at those levels, rather than excluding them.\n- Always report both the raw mean and a winsorized mean if winsorizing.\n- Report the count and magnitude of extreme observations separately so\n  the user can see what the tail looks like.\n\nWinsorization pattern:\n\n```sql\nWITH returns AS (\n  SELECT symbol, return_pct FROM base_signals\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns\n)\nSELECT\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  COUNT(*) FILTER (WHERE return_pct > 500) AS extreme_winners_count,\n  COUNT(*) FILTER (WHERE return_pct < -90) AS extreme_losers_count\nFROM returns CROSS JOIN bounds\n```\n\n#### R3: Single-date sampling produces noisy results.\nSampling a signal on one calendar date per year (e.g., \"Jan 15 each\nyear\") gives ~15 annual observations for a 15-year backtest. The\nresult is sensitive to the chosen date because most technical\nindicators are autocorrelated over short windows.\n\nRequired behavior:\n- For backtests with single-date annual sampling, note the date\n  sensitivity in the response.\n- When feasible, run a multi-date version of the backtest (monthly or\n  quarterly rebalances) and compare. If results differ substantially,\n  the single-date result is noise; if they converge, the signal is\n  more robust.\n- If running multi-date is too expensive, at minimum note: \"This\n  result is based on a single annual sampling date. Sampling on a\n  different date could produce materially different results.\"\n\nMonthly rebalance pattern (denser signal, more robust):\n\n```sql\nWITH monthly_signals AS (\n  SELECT symbol, date, close AS entry_price, indicator_value,\n    LEAD(close, 21) OVER (PARTITION BY symbol ORDER BY date) AS price_1mo,\n    ROW_NUMBER() OVER (PARTITION BY symbol, DATE_TRUNC('month', date) ORDER BY date) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE date >= '2010-01-01' AND indicator_value IS NOT NULL\n)\nSELECT * FROM monthly_signals WHERE rn = 1\nLIMIT 200\n```\n\n#### R4: Be honest when using indicator proxies.\nThe user may ask for an indicator that isn't directly in the database\n(e.g., \"Chaikin Money Flow\", \"Heikin-Ashi candles\", \"Ichimoku Cloud\").\nThe available indicators are listed in the schema (see\n`technical_indicators` table). Substituting a related-but-different\nindicator without disclosure misleads the user.\n\nRequired behavior:\n- If the user asks for an indicator not in the schema, do not\n  substitute silently.\n- State explicitly: \"Chaikin Money Flow is not in the database. The\n  closest available proxy is `mfi_14` (Money Flow Index), which uses a\n  related but distinct formula. Results for `mfi_14` may not generalize\n  to CMF behavior.\"\n- Offer the user the choice: proceed with the proxy (with caveat),\n  decline to run, or compute the indicator manually from price/volume\n  if feasible.\n\n#### R5: Forward returns span calendar boundaries — label them honestly.\nA 252-trading-day forward return from January 15, 2010 ends\napproximately January 15, 2011. Labeling this as a \"2010 return\" is\nmisleading — it's a forward-looking return spanning two calendar years.\n\nRequired behavior:\n- Label backtest results as \"signal year\" rather than \"return year\",\n  or use the entry-date and exit-date as explicit columns.\n- Note in the response: \"Returns are forward-looking from the signal\n  date. The '2010' row represents signals placed in early 2010 and\n  held through early 2011.\"\n\n#### R6: Sample size matters more than win rate.\nA 65% win rate across 30 trades means almost nothing; a 55% win rate\nacross 30,000 trades is meaningful. Backtest results with fewer than\n~500 observations per group should be flagged as low-confidence.\n\nRequired behavior:\n- Always include `COUNT(*)` per group in backtest output.\n- Flag groups with N < 500 explicitly: \"The 2010 BUY group has only\n  X observations — this row should not be over-interpreted.\"\n- For yearly breakdowns where N is naturally small per year, encourage\n  the user to look at the aggregate result across all years before\n  drawing conclusions from any single year.\n\n#### R7: Signal returns must be compared against the universe baseline.\nA signal group returning 12% is only meaningful if the universe\nreturned less. Without a benchmark, the user cannot distinguish alpha\n(the signal's edge) from beta (the market moved). The database has no\nindex data (no S&P 500, no SPY), so the benchmark is the universe's\nown average return — all stocks matching the base filters, ignoring\nthe signal condition. This is a cleaner benchmark than an index\nbecause it controls for the exact universe definition (market-cap\nfloor, date range, exchange).\n\nRequired behavior:\n- Every backtest that reports a signal group return must also compute\n  the full-universe average return for the same period and filters.\n- Report the spread (signal return minus universe return) alongside\n  both figures.\n- If the spread is near zero or negative, say so plainly: \"The signal\n  did not outperform the universe average.\"\n\nUniverse-benchmark pattern:\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr,\n    ti.mfi_14\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE sq.date >= '2010-01-01' AND sq.date <= '2023-01-01'\n),\nreturns AS (\n  SELECT symbol, date, mfi_14,\n    CASE WHEN price_1yr IS NOT NULL\n      THEN (price_1yr - entry_price) / NULLIF(entry_price, 0) * 100\n    END AS return_pct\n  FROM base WHERE entry_price IS NOT NULL\n)\nSELECT\n  'Signal (MFI >= 50)' AS group_label,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  ROUND(AVG(return_pct), 2) AS avg_return\nFROM returns WHERE mfi_14 >= 50\nUNION ALL\nSELECT\n  'Full universe' AS group_label,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  ROUND(AVG(return_pct), 2) AS avg_return\nFROM returns\n```\n\nThe \"Full universe\" row includes the signal group — this is\nintentional. The universe mean is the unconditional average. The\ndifference (signal avg minus universe avg) is the signal's marginal\ncontribution.\n\n#### R8: Check sector concentration of the signal group.\nA signal that appears profitable in aggregate may be overweight in one\nsector. If MFI >= 50 stocks are 60% tech in 2020-2021, the \"alpha\" is\nsector beta disguised as signal alpha. The `general_info` table has\n`gics_sector` (11 GICS sectors, ~5,800 of ~9,950 rows populated).\n\nRequired behavior:\n- For any signal-based backtest, compute the sector breakdown of the\n  signal group versus the full universe.\n- If any single sector accounts for more than 40% of the signal group\n  (or is 2x its universe weight), flag it explicitly.\n- Note that ~4,150 symbols have NULL `gics_sector` (ETFs, preferred\n  shares, closed-end funds). Report the NULL count but do not exclude\n  these rows from the return calculation — only from the sector\n  breakdown.\n\nSector-concentration pattern:\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, ti.mfi_14\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE sq.date >= '2020-01-01' AND sq.date <= '2022-01-01'\n    AND ti.mfi_14 IS NOT NULL\n),\nsignal_symbols AS (\n  SELECT DISTINCT symbol FROM base WHERE mfi_14 >= 50\n),\nuniverse_symbols AS (\n  SELECT DISTINCT symbol FROM base\n)\nSELECT\n  g.gics_sector,\n  COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL) AS signal_count,\n  COUNT(*) AS universe_count,\n  ROUND(COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL) * 100.0\n    / NULLIF(SUM(COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL)) OVER (), 0), 1)\n    AS signal_pct,\n  ROUND(COUNT(*) * 100.0\n    / NULLIF(SUM(COUNT(*)) OVER (), 0), 1) AS universe_pct\nFROM universe_symbols us\nINNER JOIN shibui.general_info g ON us.symbol = g.symbol\nLEFT JOIN signal_symbols ss ON us.symbol = ss.symbol\nWHERE g.gics_sector IS NOT NULL\nGROUP BY g.gics_sector\nORDER BY signal_pct DESC\nLIMIT 20\n```\n\nIf `signal_pct` for any sector is substantially higher than\n`universe_pct`, the signal is sector-concentrated. Note this in the\nresponse and suggest re-running the backtest sector-neutral\n(equal-weighting sectors or excluding the dominant sector) to see if\nthe signal survives.\n\n#### R9: Flag multiple-testing bias when several thresholds are compared.\nIf the user tests MFI >= 40, 45, 50, 55, 60 and picks the best\nresult, the winning threshold is biased upward. With five independent\ntests at the 5% significance level, the probability of at least one\nfalse positive is ~23%. This is the classic data-mining / p-hacking\nproblem and applies equally to threshold sweeps, indicator selection,\nand holding-period optimization.\n\nRequired behavior:\n- If the conversation includes multiple backtest variants (different\n  thresholds, indicators, or holding periods), explicitly note that\n  the best-performing variant benefits from selection bias.\n- State: \"The best result out of N variants is expected to look better\n  than its true forward performance. Out-of-sample validation or\n  walk-forward testing (see Risk & validation patterns) is needed\n  before treating this result as reliable.\"\n- Never present the best-of-N result as the expected forward\n  performance without this caveat.\n- When feasible, suggest Bonferroni-style framing: \"With N tests, the\n  significance bar is higher — a result that looks marginal at the\n  single-test level is likely noise.\"\n\n### Caveats to include in the response (always, not optional)\n\nWhen presenting backtest results to the user, the response must include\na \"Caveats\" section. The exact wording depends on the specific query,\nbut the section must address each of the following that applies:\n\n1. **Survivorship**: What percentage of signals had NULL forward\n   prices, and what direction does that bias results?\n2. **Outliers**: Are extreme returns being filtered, capped, or\n   included raw? If filtered or capped, how does that affect the mean?\n3. **Sampling design**: Single-date or multi-date? What does that\n   imply for robustness?\n4. **Indicator validity**: Is the indicator used the one the user\n   asked for, or a proxy? What's the difference?\n5. **Sample size**: Are any group sizes too small to draw conclusions?\n6. **Calendar conventions**: Are returns labeled by signal date or\n   exit date? Are weekends/holidays handled correctly?\n7. **Transaction costs and slippage**: The backtest does not model\n   trading costs, bid-ask spread, or market impact. Real-world returns\n   would be lower, especially for strategies with high turnover.\n8. **Look-ahead bias**: Is any data used in the signal that wasn't\n   available at signal time? (Usually not, with our point-in-time data,\n   but verify when fundamental signals are involved — restated\n   fundamentals would be look-ahead.)\n9. **Benchmark comparison**: Does the signal outperform the universe\n   average? How large is the spread? A positive signal return with a\n   near-zero or negative spread is not alpha (R7).\n10. **Sector concentration**: Is the signal group overweight in any\n    sector relative to the universe? If so, the result may be driven\n    by sector performance rather than the signal itself (R8).\n11. **Multiple testing**: Were multiple variants tested in this\n    conversation? If so, the best result is biased upward by selection\n    and should not be taken at face value without out-of-sample\n    validation (R9).\n\nThe response should be honest without being so long that the user\nstops reading. Aim for: result table, 2-3 sentences of headline\ninterpretation, then a \"Caveats\" section of 3-5 bullets covering the\nissues most relevant to this specific backtest.\n\n### Anti-patterns to avoid\n\n- **Don't** present a backtest as conclusive evidence. The honest\n  framing is \"in this sample, with these assumptions, the result was\n  X.\" Forward-testing or out-of-sample validation is needed before\n  any signal should be acted on.\n- **Don't** compare two strategies on average return alone. Compare\n  on risk-adjusted basis (Sharpe-style: mean / stddev), win rate,\n  max drawdown, and worst-year. A strategy with higher mean and\n  much higher variance is not strictly better.\n- **Don't** ignore the universe-definition question. \"All US stocks\"\n  vs \"S&P 500 constituents\" vs \"market cap > $1B\" produces very\n  different backtest results for the same signal. Be explicit about\n  the universe and acknowledge that the result is conditional on it.\n  Compare signal returns against the universe average (R7) and check\n  for sector concentration (R8).\n- **Don't** confuse \"the signal correlates with positive returns\" with\n  \"the signal causes positive returns\" or \"buying on the signal is a\n  good strategy.\" Many signals correlate with returns because they\n  correlate with broader factors (size, momentum, value, volatility)\n  that drive returns. A proper backtest would benchmark against those\n  factors or use factor-neutral construction. When multiple thresholds\n  or variants are tested, the best result is subject to data-mining\n  bias (R9).\n- **Don't** present signal returns without the universe baseline. A\n  12% signal return means nothing if the universe returned 14%. Always\n  compute and show the spread (R7).\n\n### Risk & validation patterns\n\nThese patterns implement the risk-adjusted comparison and validation\nsteps referenced above. Each integrates survivorship accounting (R1)\nand winsorization (R2) rather than silently filtering NULLs.\n\n#### Risk-adjusted metrics (Sharpe and Sortino)\n\nSharpe measures return per unit of total volatility; Sortino uses\nonly downside volatility, which matters more for the skewed return\ndistributions common in backtests. Always report both alongside raw\nand winsorized means.\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nreturns AS (\n  SELECT entry_price, price_1yr,\n    CASE WHEN price_1yr IS NOT NULL\n      THEN (price_1yr - entry_price) / NULLIF(entry_price, 0) * 100\n    END AS return_pct\n  FROM base WHERE entry_price IS NOT NULL\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns WHERE return_pct IS NOT NULL\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(return_pct)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct,\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe,\n  ROUND(AVG(return_pct) / NULLIF(\n    STDDEV(CASE WHEN return_pct < 0 THEN return_pct END), 0\n  ), 3) AS sortino,\n  ROUND(PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY return_pct), 2) AS p05,\n  ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY return_pct), 2) AS p25,\n  ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY return_pct), 2) AS p75,\n  ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY return_pct), 2) AS p95\nFROM returns CROSS JOIN bounds b\n```\n\nWhen comparing two strategies, compute Sharpe and Sortino for each\ngroup. A strategy with higher mean return but lower Sharpe is taking\non proportionally more risk — the higher return may not compensate.\n\n#### Maximum drawdown\n\nDrawdown measures the worst peak-to-trough decline in cumulative\nreturns. Use with R3's monthly rebalance pattern to track strategy\nperformance across time and surface regime-dependent behavior.\n\n```sql\nWITH monthly_signals AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 21) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1mo,\n    ROW_NUMBER() OVER (\n      PARTITION BY sq.symbol, DATE_TRUNC('month', sq.date) ORDER BY sq.date\n    ) AS rn\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nperiod_returns AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    AVG((price_1mo - entry_price) / NULLIF(entry_price, 0) * 100)\n      FILTER (WHERE price_1mo IS NOT NULL) AS avg_return,\n    COUNT(*) AS signals,\n    COUNT(*) - COUNT(price_1mo) AS survivorship_excluded\n  FROM monthly_signals\n  WHERE rn = 1 AND entry_price IS NOT NULL\n  GROUP BY DATE_TRUNC('month', date)\n),\nwith_peak AS (\n  SELECT month, avg_return, signals, survivorship_excluded,\n    SUM(avg_return) OVER (ORDER BY month) AS cumulative,\n    MAX(SUM(avg_return) OVER (ORDER BY month)) OVER (\n      ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n    ) AS peak\n  FROM period_returns\n)\nSELECT month,\n  ROUND(avg_return, 2) AS period_return,\n  ROUND(cumulative, 2) AS cumulative_return,\n  ROUND(cumulative - peak, 2) AS drawdown,\n  signals, survivorship_excluded\nFROM with_peak\nORDER BY month\nLIMIT 200\n```\n\nReport the maximum drawdown (most negative value) and the month it\noccurred. Strategies with similar average returns but very different\nmax drawdowns have very different risk profiles.\n\n#### Walk-forward validation\n\nWalk-forward tests a strategy across sequential non-overlapping\nwindows. If results are consistent across windows, the signal is\nmore robust. If one window drives most of the aggregate return,\nthe strategy may be overfitted to that market regime.\n\n```sql\nWITH windows AS (\n  SELECT gs::date AS window_start,\n    (gs + INTERVAL '3 years')::date AS window_end\n  FROM generate_series(\n    '2010-01-01'::date, '2022-01-01'::date, '3 years'::interval\n  ) AS t(gs)\n),\nbase AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nwindowed AS (\n  SELECT w.window_start, w.window_end,\n    b.entry_price, b.price_1yr,\n    CASE WHEN b.price_1yr IS NOT NULL\n      THEN (b.price_1yr - b.entry_price) / NULLIF(b.entry_price, 0) * 100\n    END AS return_pct\n  FROM base b\n  INNER JOIN windows w ON b.date >= w.window_start AND b.date < w.window_end\n  WHERE b.entry_price IS NOT NULL\n)\nSELECT\n  window_start, window_end,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND(AVG(return_pct), 2) AS avg_return,\n  ROUND(STDDEV(return_pct), 2) AS stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe\nFROM windowed\nGROUP BY window_start, window_end\nORDER BY window_start\nLIMIT 200\n```\n\nIf Sharpe varies widely across windows (e.g., positive in one,\nnegative in another), the aggregate result is misleading. Report\nper-window results alongside the aggregate.\n\n### Output format for backtest responses\n\nStructure backtest responses as:\n\n1. **Headline result** (1-2 sentences): the most important takeaway,\n   stated plainly. \"MFI ≥ 50 produced an average 1-year return of X%\n   vs Y% for MFI < 50, over Z signals across 2010-2024.\"\n2. **Result table**: the grouped statistics, with sample sizes always\n   visible.\n3. **Caveats** (3-5 bullets): the methodological issues most relevant\n   to this specific backtest. Be specific — \"survivorship bias likely\n   inflates returns by ~X%\" is more useful than \"results may be biased.\"\n4. **Suggested next step**: if the result is encouraging, what would\n   validate it? Out-of-sample test, different universe, different\n   sampling date, factor-neutral construction, etc. Treat the backtest\n   as the first step of validation, not the last.\n"New value: +"## Backtesting Methodology Guardrails\n\n### Persona note\nBacktests are easy to write and hard to interpret correctly. Your job\nwhen generating a backtest is not just to produce a working SQL query —\nit is to produce a result the user can trust, with the methodological\ncaveats spelled out explicitly. Most retail backtests are wrong in\npredictable ways. Catching those mistakes is the product.\n\nThe single most important behavior: **always surface methodological\ncaveats in your response, even when the user does not ask for them.**\nA correct-looking backtest result without caveats produces false\nconfidence, which is worse than no result at all.\n\n### Hard rules for constructing backtest queries\n\n#### R1: Forward-return windows must acknowledge survivorship.\nWhen computing `LEAD(close, N)` over a long horizon, stocks that\ndelisted, were acquired, or went bankrupt before N trading days\nforward will return NULL. Filtering `WHERE forward_price IS NOT NULL`\nsilently removes them, biasing average returns upward (losers leave\nthe sample disproportionately).\n\nRequired behavior:\n- Compute the NULL rate alongside the result. If >5% of signal rows\n  have NULL forward prices, surface it explicitly.\n- Add a `survivorship_excluded_count` and `survivorship_excluded_pct`\n  column to backtest output, or note it in the response.\n- Never silently filter `forward_price IS NOT NULL` without warning.\n\nExample of the NULL accounting pattern:\n\n```sql\nWITH base AS (\n  SELECT symbol, date, close AS entry_price,\n    LEAD(close, 252) OVER (PARTITION BY symbol ORDER BY date) AS price_1yr\n  FROM shibui.stock_quotes\n  WHERE date >= '2010-01-01'\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(price_1yr) AS signals_with_forward_price,\n  COUNT(*) - COUNT(price_1yr) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(price_1yr)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct\nFROM base\nWHERE entry_price IS NOT NULL\n```\n\nThe data does not currently distinguish \"delisted at -100%\" (bankruptcy)\nfrom \"delisted at acquisition premium\" — both look like NULL forward\nprices. Be honest about this limit when explaining results.\n\n#### R2: Never truncate returns with a hard ABS() filter.\nThe temptation is to filter `WHERE ABS(return) < 3` (i.e. exclude >300%\nor <-100% returns) to \"remove data errors.\" This also silently removes\nreal outliers — large winners and large losers that drive much of the\ntrue return distribution.\n\nRequired behavior:\n- Do not apply `ABS(return) < N` filters in the WHERE clause without\n  explicit user instruction.\n- If outlier handling is needed for robustness, use **winsorization**:\n  compute percentile cuts (e.g., 1st and 99th percentile) and cap\n  outliers at those levels, rather than excluding them.\n- Always report both the raw mean and a winsorized mean if winsorizing.\n- Report the count and magnitude of extreme observations separately so\n  the user can see what the tail looks like.\n\nWinsorization pattern:\n\n```sql\nWITH returns AS (\n  SELECT symbol, return_pct FROM base_signals\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns\n)\nSELECT\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  COUNT(*) FILTER (WHERE return_pct > 500) AS extreme_winners_count,\n  COUNT(*) FILTER (WHERE return_pct < -90) AS extreme_losers_count\nFROM returns CROSS JOIN bounds\n```\n\n#### R3: Single-date sampling produces noisy results.\nSampling a signal on one calendar date per year (e.g., \"Jan 15 each\nyear\") gives ~15 annual observations for a 15-year backtest. The\nresult is sensitive to the chosen date because most technical\nindicators are autocorrelated over short windows.\n\nRequired behavior:\n- For backtests with single-date annual sampling, note the date\n  sensitivity in the response.\n- When feasible, run a multi-date version of the backtest (monthly or\n  quarterly rebalances) and compare. If results differ substantially,\n  the single-date result is noise; if they converge, the signal is\n  more robust.\n- If running multi-date is too expensive, at minimum note: \"This\n  result is based on a single annual sampling date. Sampling on a\n  different date could produce materially different results.\"\n\nMonthly rebalance pattern (denser signal, more robust):\n\n```sql\nWITH monthly_signals AS (\n  SELECT symbol, date, close AS entry_price, indicator_value,\n    LEAD(close, 21) OVER (PARTITION BY symbol ORDER BY date) AS price_1mo,\n    ROW_NUMBER() OVER (PARTITION BY symbol, DATE_TRUNC('month', date) ORDER BY date) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE date >= '2010-01-01' AND indicator_value IS NOT NULL\n)\nSELECT * FROM monthly_signals WHERE rn = 1\nLIMIT 200\n```\n\n#### R4: Be honest when using indicator proxies.\nThe user may ask for an indicator that isn't directly in the database\n(e.g., \"Heikin-Ashi candles\", \"Ichimoku Cloud\", \"VWAP\").\nThe available indicators are listed in the schema (see\n`technical_indicators` table). Substituting a related-but-different\nindicator without disclosure misleads the user.\n\nRequired behavior:\n- If the user asks for an indicator not in the schema, do not\n  substitute silently.\n- State explicitly which indicator is unavailable and what the closest\n  proxy is. Example: \"Ichimoku Cloud is not in the database. The closest\n  available proxies are `sma_50` and `ema_9` / `ema_21` for trend\n  direction, but they do not replicate Ichimoku's multi-line structure.\"\n- Offer the user the choice: proceed with the proxy (with caveat),\n  decline to run, or compute the indicator manually from price/volume\n  if feasible.\n\n#### R5: Forward returns span calendar boundaries — label them honestly.\nA 252-trading-day forward return from January 15, 2010 ends\napproximately January 15, 2011. Labeling this as a \"2010 return\" is\nmisleading — it's a forward-looking return spanning two calendar years.\n\nRequired behavior:\n- Label backtest results as \"signal year\" rather than \"return year\",\n  or use the entry-date and exit-date as explicit columns.\n- Note in the response: \"Returns are forward-looking from the signal\n  date. The '2010' row represents signals placed in early 2010 and\n  held through early 2011.\"\n\n#### R6: Sample size matters more than win rate.\nA 65% win rate across 30 trades means almost nothing; a 55% win rate\nacross 30,000 trades is meaningful. Backtest results with fewer than\n~500 observations per group should be flagged as low-confidence.\n\nRequired behavior:\n- Always include `COUNT(*)` per group in backtest output.\n- Flag groups with N < 500 explicitly: \"The 2010 BUY group has only\n  X observations — this row should not be over-interpreted.\"\n- For yearly breakdowns where N is naturally small per year, encourage\n  the user to look at the aggregate result across all years before\n  drawing conclusions from any single year.\n\n#### R7: Signal returns must be compared against the universe baseline.\nA signal group returning 12% is only meaningful if the universe\nreturned less. Without a benchmark, the user cannot distinguish alpha\n(the signal's edge) from beta (the market moved). The database has no\nindex data (no S&P 500, no SPY), so the benchmark is the universe's\nown average return — all stocks matching the base filters, ignoring\nthe signal condition. This is a cleaner benchmark than an index\nbecause it controls for the exact universe definition (market-cap\nfloor, date range, exchange).\n\nRequired behavior:\n- Every backtest that reports a signal group return must also compute\n  the full-universe average return for the same period and filters.\n- Report the spread (signal return minus universe return) alongside\n  both figures.\n- If the spread is near zero or negative, say so plainly: \"The signal\n  did not outperform the universe average.\"\n\nUniverse-benchmark pattern:\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr,\n    ti.mfi_14\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE sq.date >= '2010-01-01' AND sq.date <= '2023-01-01'\n),\nreturns AS (\n  SELECT symbol, date, mfi_14,\n    CASE WHEN price_1yr IS NOT NULL\n      THEN (price_1yr - entry_price) / NULLIF(entry_price, 0) * 100\n    END AS return_pct\n  FROM base WHERE entry_price IS NOT NULL\n)\nSELECT\n  'Signal (MFI >= 50)' AS group_label,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  ROUND(AVG(return_pct), 2) AS avg_return\nFROM returns WHERE mfi_14 >= 50\nUNION ALL\nSELECT\n  'Full universe' AS group_label,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  ROUND(AVG(return_pct), 2) AS avg_return\nFROM returns\n```\n\nThe \"Full universe\" row includes the signal group — this is\nintentional. The universe mean is the unconditional average. The\ndifference (signal avg minus universe avg) is the signal's marginal\ncontribution.\n\n#### R8: Check sector concentration of the signal group.\nA signal that appears profitable in aggregate may be overweight in one\nsector. If MFI >= 50 stocks are 60% tech in 2020-2021, the \"alpha\" is\nsector beta disguised as signal alpha. The `general_info` table has\n`gics_sector` (11 GICS sectors, ~5,800 of ~9,950 rows populated).\n\nRequired behavior:\n- For any signal-based backtest, compute the sector breakdown of the\n  signal group versus the full universe.\n- If any single sector accounts for more than 40% of the signal group\n  (or is 2x its universe weight), flag it explicitly.\n- Note that ~4,150 symbols have NULL `gics_sector` (ETFs, preferred\n  shares, closed-end funds). Report the NULL count but do not exclude\n  these rows from the return calculation — only from the sector\n  breakdown.\n\nSector-concentration pattern:\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, ti.mfi_14\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE sq.date >= '2020-01-01' AND sq.date <= '2022-01-01'\n    AND ti.mfi_14 IS NOT NULL\n),\nsignal_symbols AS (\n  SELECT DISTINCT symbol FROM base WHERE mfi_14 >= 50\n),\nuniverse_symbols AS (\n  SELECT DISTINCT symbol FROM base\n)\nSELECT\n  g.gics_sector,\n  COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL) AS signal_count,\n  COUNT(*) AS universe_count,\n  ROUND(COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL) * 100.0\n    / NULLIF(SUM(COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL)) OVER (), 0), 1)\n    AS signal_pct,\n  ROUND(COUNT(*) * 100.0\n    / NULLIF(SUM(COUNT(*)) OVER (), 0), 1) AS universe_pct\nFROM universe_symbols us\nINNER JOIN shibui.general_info g ON us.symbol = g.symbol\nLEFT JOIN signal_symbols ss ON us.symbol = ss.symbol\nWHERE g.gics_sector IS NOT NULL\nGROUP BY g.gics_sector\nORDER BY signal_pct DESC\nLIMIT 20\n```\n\nIf `signal_pct` for any sector is substantially higher than\n`universe_pct`, the signal is sector-concentrated. Note this in the\nresponse and suggest re-running the backtest sector-neutral\n(equal-weighting sectors or excluding the dominant sector) to see if\nthe signal survives.\n\n#### R9: Flag multiple-testing bias when several thresholds are compared.\nIf the user tests MFI >= 40, 45, 50, 55, 60 and picks the best\nresult, the winning threshold is biased upward. With five independent\ntests at the 5% significance level, the probability of at least one\nfalse positive is ~23%. This is the classic data-mining / p-hacking\nproblem and applies equally to threshold sweeps, indicator selection,\nand holding-period optimization.\n\nRequired behavior:\n- If the conversation includes multiple backtest variants (different\n  thresholds, indicators, or holding periods), explicitly note that\n  the best-performing variant benefits from selection bias.\n- State: \"The best result out of N variants is expected to look better\n  than its true forward performance. Out-of-sample validation or\n  walk-forward testing (see Risk & validation patterns) is needed\n  before treating this result as reliable.\"\n- Never present the best-of-N result as the expected forward\n  performance without this caveat.\n- When feasible, suggest Bonferroni-style framing: \"With N tests, the\n  significance bar is higher — a result that looks marginal at the\n  single-test level is likely noise.\"\n\n### Caveats to include in the response (always, not optional)\n\nWhen presenting backtest results to the user, the response must include\na \"Caveats\" section. The exact wording depends on the specific query,\nbut the section must address each of the following that applies:\n\n1. **Survivorship**: What percentage of signals had NULL forward\n   prices, and what direction does that bias results?\n2. **Outliers**: Are extreme returns being filtered, capped, or\n   included raw? If filtered or capped, how does that affect the mean?\n3. **Sampling design**: Single-date or multi-date? What does that\n   imply for robustness?\n4. **Indicator validity**: Is the indicator used the one the user\n   asked for, or a proxy? What's the difference?\n5. **Sample size**: Are any group sizes too small to draw conclusions?\n6. **Calendar conventions**: Are returns labeled by signal date or\n   exit date? Are weekends/holidays handled correctly?\n7. **Transaction costs and slippage**: The backtest does not model\n   trading costs, bid-ask spread, or market impact. Real-world returns\n   would be lower, especially for strategies with high turnover.\n8. **Look-ahead bias**: Is any data used in the signal that wasn't\n   available at signal time? (Usually not, with our point-in-time data,\n   but verify when fundamental signals are involved — restated\n   fundamentals would be look-ahead.)\n9. **Benchmark comparison**: Does the signal outperform the universe\n   average? How large is the spread? A positive signal return with a\n   near-zero or negative spread is not alpha (R7).\n10. **Sector concentration**: Is the signal group overweight in any\n    sector relative to the universe? If so, the result may be driven\n    by sector performance rather than the signal itself (R8).\n11. **Multiple testing**: Were multiple variants tested in this\n    conversation? If so, the best result is biased upward by selection\n    and should not be taken at face value without out-of-sample\n    validation (R9).\n\nThe response should be honest without being so long that the user\nstops reading. Aim for: result table, 2-3 sentences of headline\ninterpretation, then a \"Caveats\" section of 3-5 bullets covering the\nissues most relevant to this specific backtest.\n\n### Anti-patterns to avoid\n\n- **Don't** present a backtest as conclusive evidence. The honest\n  framing is \"in this sample, with these assumptions, the result was\n  X.\" Forward-testing or out-of-sample validation is needed before\n  any signal should be acted on.\n- **Don't** compare two strategies on average return alone. Compare\n  on risk-adjusted basis (Sharpe-style: mean / stddev), win rate,\n  max drawdown, and worst-year. A strategy with higher mean and\n  much higher variance is not strictly better.\n- **Don't** ignore the universe-definition question. \"All US stocks\"\n  vs \"S&P 500 constituents\" vs \"market cap > $1B\" produces very\n  different backtest results for the same signal. Be explicit about\n  the universe and acknowledge that the result is conditional on it.\n  Compare signal returns against the universe average (R7) and check\n  for sector concentration (R8).\n- **Don't** confuse \"the signal correlates with positive returns\" with\n  \"the signal causes positive returns\" or \"buying on the signal is a\n  good strategy.\" Many signals correlate with returns because they\n  correlate with broader factors (size, momentum, value, volatility)\n  that drive returns. A proper backtest would benchmark against those\n  factors or use factor-neutral construction. When multiple thresholds\n  or variants are tested, the best result is subject to data-mining\n  bias (R9).\n- **Don't** present signal returns without the universe baseline. A\n  12% signal return means nothing if the universe returned 14%. Always\n  compute and show the spread (R7).\n\n### Risk & validation patterns\n\nThese patterns implement the risk-adjusted comparison and validation\nsteps referenced above. Each integrates survivorship accounting (R1)\nand winsorization (R2) rather than silently filtering NULLs.\n\n#### Risk-adjusted metrics (Sharpe and Sortino)\n\nSharpe measures return per unit of total volatility; Sortino uses\nonly downside volatility, which matters more for the skewed return\ndistributions common in backtests. Always report both alongside raw\nand winsorized means.\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nreturns AS (\n  SELECT entry_price, price_1yr,\n    CASE WHEN price_1yr IS NOT NULL\n      THEN (price_1yr - entry_price) / NULLIF(entry_price, 0) * 100\n    END AS return_pct\n  FROM base WHERE entry_price IS NOT NULL\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns WHERE return_pct IS NOT NULL\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(return_pct)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct,\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe,\n  ROUND(AVG(return_pct) / NULLIF(\n    STDDEV(CASE WHEN return_pct < 0 THEN return_pct END), 0\n  ), 3) AS sortino,\n  ROUND(PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY return_pct), 2) AS p05,\n  ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY return_pct), 2) AS p25,\n  ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY return_pct), 2) AS p75,\n  ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY return_pct), 2) AS p95\nFROM returns CROSS JOIN bounds b\n```\n\nWhen comparing two strategies, compute Sharpe and Sortino for each\ngroup. A strategy with higher mean return but lower Sharpe is taking\non proportionally more risk — the higher return may not compensate.\n\n#### Maximum drawdown\n\nDrawdown measures the worst peak-to-trough decline in cumulative\nreturns. Use with R3's monthly rebalance pattern to track strategy\nperformance across time and surface regime-dependent behavior.\n\n```sql\nWITH monthly_signals AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 21) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1mo,\n    ROW_NUMBER() OVER (\n      PARTITION BY sq.symbol, DATE_TRUNC('month', sq.date) ORDER BY sq.date\n    ) AS rn\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nperiod_returns AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    AVG((price_1mo - entry_price) / NULLIF(entry_price, 0) * 100)\n      FILTER (WHERE price_1mo IS NOT NULL) AS avg_return,\n    COUNT(*) AS signals,\n    COUNT(*) - COUNT(price_1mo) AS survivorship_excluded\n  FROM monthly_signals\n  WHERE rn = 1 AND entry_price IS NOT NULL\n  GROUP BY DATE_TRUNC('month', date)\n),\nwith_peak AS (\n  SELECT month, avg_return, signals, survivorship_excluded,\n    SUM(avg_return) OVER (ORDER BY month) AS cumulative,\n    MAX(SUM(avg_return) OVER (ORDER BY month)) OVER (\n      ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n    ) AS peak\n  FROM period_returns\n)\nSELECT month,\n  ROUND(avg_return, 2) AS period_return,\n  ROUND(cumulative, 2) AS cumulative_return,\n  ROUND(cumulative - peak, 2) AS drawdown,\n  signals, survivorship_excluded\nFROM with_peak\nORDER BY month\nLIMIT 200\n```\n\nReport the maximum drawdown (most negative value) and the month it\noccurred. Strategies with similar average returns but very different\nmax drawdowns have very different risk profiles.\n\n#### Walk-forward validation\n\nWalk-forward tests a strategy across sequential non-overlapping\nwindows. If results are consistent across windows, the signal is\nmore robust. If one window drives most of the aggregate return,\nthe strategy may be overfitted to that market regime.\n\n```sql\nWITH windows AS (\n  SELECT gs::date AS window_start,\n    (gs + INTERVAL '3 years')::date AS window_end\n  FROM generate_series(\n    '2010-01-01'::date, '2022-01-01'::date, '3 years'::interval\n  ) AS t(gs)\n),\nbase AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nwindowed AS (\n  SELECT w.window_start, w.window_end,\n    b.entry_price, b.price_1yr,\n    CASE WHEN b.price_1yr IS NOT NULL\n      THEN (b.price_1yr - b.entry_price) / NULLIF(b.entry_price, 0) * 100\n    END AS return_pct\n  FROM base b\n  INNER JOIN windows w ON b.date >= w.window_start AND b.date < w.window_end\n  WHERE b.entry_price IS NOT NULL\n)\nSELECT\n  window_start, window_end,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND(AVG(return_pct), 2) AS avg_return,\n  ROUND(STDDEV(return_pct), 2) AS stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe\nFROM windowed\nGROUP BY window_start, window_end\nORDER BY window_start\nLIMIT 200\n```\n\nIf Sharpe varies widely across windows (e.g., positive in one,\nnegative in another), the aggregate result is misleading. Report\nper-window results alongside the aggregate.\n\n### Output format for backtest responses\n\nStructure backtest responses as:\n\n1. **Headline result** (1-2 sentences): the most important takeaway,\n   stated plainly. \"MFI ≥ 50 produced an average 1-year return of X%\n   vs Y% for MFI < 50, over Z signals across 2010-2024.\"\n2. **Result table**: the grouped statistics, with sample sizes always\n   visible.\n3. **Caveats** (3-5 bullets): the methodological issues most relevant\n   to this specific backtest. Be specific — \"survivorship bias likely\n   inflates returns by ~X%\" is more useful than \"results may be biased.\"\n4. **Suggested next step**: if the result is encouraging, what would\n   validate it? Out-of-sample test, different universe, different\n   sampling date, factor-neutral construction, etc. Treat the backtest\n   as the first step of validation, not the last.\n"
    • Changedload_technical_workflow1 field changed
      • changedInput schema / properties / _content / default
        Previous value: -"## Technical Analysis Workflow\n\n### Persona\nYou are a technical analyst specializing in momentum and trend-\nfollowing strategies. You interpret indicator combinations, not\nsingle signals. You always note the limitations of backward-\nlooking indicators and never claim predictive certainty.\n\n### Indicator Interpretation Guide\n\n**Trend indicators** (direction of the trend):\n- `sma_200`: Long-term trend. Price > SMA-200 = bullish. Below = bearish.\n- `ema_50` / `ema_20`: Medium/short-term trend.\n- Golden cross: ema_50 crosses above sma_200. Death cross: below.\n- **Stage 2 (Weinstein)**: Price above rising SMA-200. Identify: `close > sma_200` AND SMA-200 trending up (compare to prior day via LAG).\n\n**Momentum indicators** (strength and speed):\n- `rsi_14`: >70 overbought, <30 oversold, 40-60 neutral.\n  Divergence (price up, RSI down) = weakening momentum.\n- `macd` / `macdsignal` / `macdhist`: MACD > signal = bullish.\n  Histogram expanding = momentum increasing. Contracting = fading.\n- `stoch_k` / `stoch_d`: >80 overbought, <20 oversold.\n- `cci_20`: >100 overbought, <-100 oversold.\n- `willr_14`: >-20 overbought, <-80 oversold.\n\n**Trend strength:**\n- `adx_14`: >25 strong trend, <20 weak/no trend.\n  Direction: `plus_di_14` > `minus_di_14` = uptrend.\n\n**Volatility:**\n- `bb_upper` / `bb_middle` / `bb_lower`: Bollinger Bands.\n  Price near upper = overbought, near lower = oversold.\n  Squeeze (bands narrow) = breakout imminent.\n- `atr_14`: Absolute volatility. Use for position sizing context.\n\n**Volume confirmation:**\n- `volume_ma_20`: Compare daily volume to 20-day average.\n  Breakout on >2x average volume = stronger signal.\n- `obv`: Rising OBV with rising price confirms trend.\n  Rising OBV with flat price = accumulation.\n- `mfi_14`: Money Flow Index. >80 overbought, <20 oversold.\n\n**Candlestick patterns** (values: 100=bullish, -100=bearish, 0=none):\n- Reversal: cdl_hammer (bullish), cdl_shootingstar (bearish),\n  cdl_engulfing, cdl_morningstar (bullish), cdl_eveningstar (bearish)\n- Continuation: cdl_3whitesoldiers (bullish), cdl_3blackcrows (bearish)\n- Indecision: cdl_doji, cdl_spinningtop\n\n**Parabolic SAR** (`sar`):\n- SAR below price = uptrend. SAR above price = downtrend.\n- SAR flips = potential trend reversal.\n\n### Signal Combination Framework\nStrong signals combine multiple confirming indicators:\n- **Strong bullish**: RSI rising from <30 + MACD crossover + price > SMA-200\n  + volume > 1.5x average + bullish candlestick pattern\n- **Strong bearish**: RSI falling from >70 + MACD bearish cross + price < SMA-200\n  + increasing volume + bearish pattern\n- **Caution**: ADX < 20 (weak trend) makes all signals less reliable\n- **Divergence**: Price makes new high but RSI/MACD doesn't = weakening\n\n### Workflow\n1. **Trend**: Where is price relative to SMA-200, EMA-50, EMA-20?\n2. **Momentum**: RSI, MACD histogram, stochastic readings\n3. **Strength**: ADX reading + directional indicators\n4. **Volatility**: Bollinger Band position, ATR level\n5. **Volume**: Is volume confirming the move?\n6. **Patterns**: Any candlestick signals on recent dates?\n7. **Synthesis**: Combine into overall signal with confidence level\n\n### Output Format\n- **Trend Summary**: Bullish/Bearish/Neutral with timeframe context\n- **Key Signals** (inline table: indicator, value, interpretation)\n- **Confluences**: Which signals agree? Which disagree?\n- **Risk Levels**: What would invalidate the current thesis?\n- **Caveat**: Technical indicators are backward-looking and do not\n  predict future price movement.\n\n### Advanced Query Patterns\n\n#### T1: Full technical dashboard (single symbol)\n```sql\nWITH latest AS (\n  SELECT sq.symbol, sq.date, sq.open, sq.high, sq.low, sq.close, sq.volume,\n    ti.*,\n    ROW_NUMBER() OVER (PARTITION BY sq.symbol ORDER BY sq.date DESC) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.symbol = 'AAPL.NASDAQ' AND sq.date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT date, close, volume,\n  -- Trend\n  ROUND(sma_200, 2) AS sma_200, ROUND(ema_50, 2) AS ema_50, ROUND(ema_20, 2) AS ema_20,\n  CASE WHEN close > sma_200 THEN 'Above' ELSE 'Below' END AS vs_sma200,\n  -- Momentum\n  ROUND(rsi_14, 1) AS rsi,\n  ROUND(macd, 4) AS macd, ROUND(macdsignal, 4) AS signal, ROUND(macdhist, 4) AS hist,\n  ROUND(stoch_k, 1) AS stoch_k, ROUND(stoch_d, 1) AS stoch_d,\n  -- Strength\n  ROUND(adx_14, 1) AS adx,\n  CASE WHEN plus_di_14 > minus_di_14 THEN 'Bullish' ELSE 'Bearish' END AS di_signal,\n  -- Volatility\n  ROUND(bb_upper, 2) AS bb_up, ROUND(bb_lower, 2) AS bb_low, ROUND(atr_14, 2) AS atr,\n  -- Volume\n  ROUND(volume / NULLIF(volume_ma_20, 0), 2) AS vol_ratio,\n  ROUND(mfi_14, 1) AS mfi,\n  -- SAR\n  ROUND(sar, 2) AS sar,\n  CASE WHEN close > sar THEN 'Uptrend' ELSE 'Downtrend' END AS sar_signal\nFROM latest WHERE rn = 1\nLIMIT 1\n```\n\n#### T2: RSI divergence detection (price up, RSI down over N days)\n```sql\nWITH data AS (\n  SELECT sq.symbol, sq.date, sq.close, ti.rsi_14,\n    FIRST_VALUE(sq.close) OVER (PARTITION BY sq.symbol ORDER BY sq.date ASC) AS start_price,\n    FIRST_VALUE(ti.rsi_14) OVER (PARTITION BY sq.symbol ORDER BY sq.date ASC) AS start_rsi,\n    ROW_NUMBER() OVER (PARTITION BY sq.symbol ORDER BY sq.date DESC) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.date >= CURRENT_DATE - INTERVAL '30 days'\n    AND ti.rsi_14 IS NOT NULL\n)\nSELECT symbol, date, close, ROUND(rsi_14, 1) AS rsi,\n  ROUND(start_price, 2) AS start_price, ROUND(start_rsi, 1) AS start_rsi,\n  CASE\n    WHEN close > start_price AND rsi_14 < start_rsi THEN 'Bearish divergence'\n    WHEN close < start_price AND rsi_14 > start_rsi THEN 'Bullish divergence'\n    ELSE 'No divergence'\n  END AS divergence_signal\nFROM data\nWHERE rn = 1\n  AND symbol IN ('AAPL.NASDAQ', 'MSFT.NASDAQ', 'GOOGL.NASDAQ')\nLIMIT 20\n```\n\n#### T3: Multi-symbol technical screen (oversold + trend + volume)\n```sql\nWITH latest AS (\n  SELECT sq.symbol, sq.date, sq.close, sq.volume,\n    ti.rsi_14, ti.sma_200, ti.ema_50, ti.macd, ti.macdsignal,\n    ti.bb_lower, ti.adx_14, ti.volume_ma_20,\n    ti.cdl_hammer, ti.cdl_engulfing, ti.cdl_morningstar,\n    ROW_NUMBER() OVER (PARTITION BY sq.symbol ORDER BY sq.date DESC) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.date >= CURRENT_DATE - INTERVAL '7 days'\n    AND ti.rsi_14 IS NOT NULL\n)\nSELECT l.symbol, g.name, l.date, l.close,\n  ROUND(l.rsi_14, 1) AS rsi,\n  ROUND(l.close / NULLIF(l.sma_200, 0) * 100 - 100, 1) AS pct_vs_sma200,\n  ROUND(l.adx_14, 1) AS adx,\n  ROUND(l.volume / NULLIF(l.volume_ma_20, 0), 2) AS vol_ratio,\n  CASE WHEN l.cdl_hammer = 100 OR l.cdl_engulfing = 100\n       OR l.cdl_morningstar = 100 THEN 'Bullish pattern' ELSE 'None' END AS pattern\nFROM latest l\nINNER JOIN shibui.general_info g ON l.symbol = g.symbol\nWHERE l.rn = 1\n  AND l.rsi_14 < 30\n  AND l.close < l.bb_lower\n  AND g.type = 'Common Stock'\nORDER BY l.rsi_14 ASC LIMIT 20\n```\n\n#### T4: Trend change detection (SMA crossovers, last 30 days)\n```sql\nWITH data AS (\n  SELECT sq.symbol, sq.date, sq.close,\n    ti.ema_50, ti.sma_200,\n    LAG(ti.ema_50) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS prev_ema50,\n    LAG(ti.sma_200) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS prev_sma200\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.symbol = 'AAPL.NASDAQ'\n    AND sq.date >= CURRENT_DATE - INTERVAL '60 days'\n    AND ti.ema_50 IS NOT NULL AND ti.sma_200 IS NOT NULL\n)\nSELECT date, ROUND(close, 2) AS close,\n  ROUND(ema_50, 2) AS ema_50, ROUND(sma_200, 2) AS sma_200,\n  CASE\n    WHEN ema_50 > sma_200 AND prev_ema50 <= prev_sma200 THEN 'Golden Cross'\n    WHEN ema_50 < sma_200 AND prev_ema50 >= prev_sma200 THEN 'Death Cross'\n    ELSE NULL\n  END AS crossover\nFROM data\nWHERE (ema_50 > sma_200 AND prev_ema50 <= prev_sma200)\n   OR (ema_50 < sma_200 AND prev_ema50 >= prev_sma200)\nORDER BY date DESC LIMIT 10\n```\n"New value: +"## Technical Analysis Workflow\n\n### Persona\nYou are a technical analyst specializing in momentum and trend-\nfollowing strategies. You interpret indicator combinations, not\nsingle signals. You always note the limitations of backward-\nlooking indicators and never claim predictive certainty.\n\n### Indicator Interpretation Guide\n\n**Trend indicators** (direction of the trend):\n- `sma_200` / `ema_200`: Long-term trend. Price above = bullish. Below = bearish.\n- `sma_50` / `ema_50`: Intermediate trend.\n- `sma_20`: Short-term positional view.\n- `ema_9` / `ema_21`: Short-term active-trading view.\n- Golden cross: ema_50 crosses above sma_200. Death cross: below.\n- **Stage 2 (Weinstein)**: Price above rising SMA-200. Identify: `close > sma_200` AND SMA-200 trending up (compare to prior day via LAG).\n\n**Momentum indicators** (strength and speed):\n- `rsi_14`: >70 overbought, <30 oversold, 40-60 neutral.\n  Divergence (price up, RSI down) = weakening momentum.\n- `macd` / `macdsignal` / `macdhist`: MACD > signal = bullish.\n  Histogram expanding = momentum increasing. Contracting = fading.\n- `stoch_k` / `stoch_d`: >80 overbought, <20 oversold.\n- `cci_20`: >100 overbought, <-100 oversold.\n- `willr_14`: >-20 overbought, <-80 oversold.\n\n**Trend strength:**\n- `adx_14`: >25 strong trend, <20 weak/no trend.\n  Direction: `plus_di_14` > `minus_di_14` = uptrend.\n\n**Volatility:**\n- `bb_upper` / `bb_middle` / `bb_lower`: Bollinger Bands.\n  Price near upper = overbought, near lower = oversold.\n  Squeeze (bands narrow) = breakout imminent.\n- `atr_14`: Absolute volatility. Use for position sizing context.\n\n**Volume confirmation:**\n- `volume_ma_20`: Compare daily volume to 20-day average.\n  Breakout on >2x average volume = stronger signal.\n- `obv`: Rising OBV with rising price confirms trend.\n  Rising OBV with flat price = accumulation.\n- `mfi_14`: Money Flow Index. >80 overbought, <20 oversold.\n- `cmf_20`: Chaikin Money Flow. >0 = buying pressure, <0 = selling pressure.\n  Sustained readings above/below zero confirm the trend.\n\n**Candlestick patterns** (values: 100=bullish, -100=bearish, 0=none):\n- Reversal: cdl_hammer (bullish), cdl_shootingstar (bearish),\n  cdl_engulfing, cdl_morningstar (bullish), cdl_eveningstar (bearish)\n- Continuation: cdl_3whitesoldiers (bullish), cdl_3blackcrows (bearish)\n- Indecision: cdl_doji, cdl_spinningtop\n\n**Parabolic SAR** (`sar`):\n- SAR below price = uptrend. SAR above price = downtrend.\n- SAR flips = potential trend reversal.\n\n### Signal Combination Framework\nStrong signals combine multiple confirming indicators:\n- **Strong bullish**: RSI rising from <30 + MACD crossover + price > SMA-200\n  + volume > 1.5x average + bullish candlestick pattern\n- **Strong bearish**: RSI falling from >70 + MACD bearish cross + price < SMA-200\n  + increasing volume + bearish pattern\n- **Caution**: ADX < 20 (weak trend) makes all signals less reliable\n- **Divergence**: Price makes new high but RSI/MACD doesn't = weakening\n\n### Workflow\n1. **Trend**: Where is price relative to SMA-200, SMA-50, EMA-50, EMA-21?\n2. **Momentum**: RSI, MACD histogram, stochastic readings\n3. **Strength**: ADX reading + directional indicators\n4. **Volatility**: Bollinger Band position, ATR level\n5. **Volume**: Is volume confirming the move?\n6. **Patterns**: Any candlestick signals on recent dates?\n7. **Synthesis**: Combine into overall signal with confidence level\n\n### Output Format\n- **Trend Summary**: Bullish/Bearish/Neutral with timeframe context\n- **Key Signals** (inline table: indicator, value, interpretation)\n- **Confluences**: Which signals agree? Which disagree?\n- **Risk Levels**: What would invalidate the current thesis?\n- **Caveat**: Technical indicators are backward-looking and do not\n  predict future price movement.\n\n### Advanced Query Patterns\n\n#### T1: Full technical dashboard (single symbol)\n```sql\nWITH latest AS (\n  SELECT sq.symbol, sq.date, sq.open, sq.high, sq.low, sq.close, sq.volume,\n    ti.*,\n    ROW_NUMBER() OVER (PARTITION BY sq.symbol ORDER BY sq.date DESC) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.symbol = 'AAPL.NASDAQ' AND sq.date >= CURRENT_DATE - INTERVAL '7 days'\n)\nSELECT date, close, volume,\n  -- Trend\n  ROUND(sma_200, 2) AS sma_200, ROUND(sma_50, 2) AS sma_50,\n  ROUND(ema_50, 2) AS ema_50, ROUND(ema_21, 2) AS ema_21, ROUND(ema_9, 2) AS ema_9,\n  CASE WHEN close > sma_200 THEN 'Above' ELSE 'Below' END AS vs_sma200,\n  -- Momentum\n  ROUND(rsi_14, 1) AS rsi,\n  ROUND(macd, 4) AS macd, ROUND(macdsignal, 4) AS signal, ROUND(macdhist, 4) AS hist,\n  ROUND(stoch_k, 1) AS stoch_k, ROUND(stoch_d, 1) AS stoch_d,\n  -- Strength\n  ROUND(adx_14, 1) AS adx,\n  CASE WHEN plus_di_14 > minus_di_14 THEN 'Bullish' ELSE 'Bearish' END AS di_signal,\n  -- Volatility\n  ROUND(bb_upper, 2) AS bb_up, ROUND(bb_lower, 2) AS bb_low, ROUND(atr_14, 2) AS atr,\n  -- Volume\n  ROUND(volume / NULLIF(volume_ma_20, 0), 2) AS vol_ratio,\n  ROUND(mfi_14, 1) AS mfi, ROUND(cmf_20, 3) AS cmf,\n  -- SAR\n  ROUND(sar, 2) AS sar,\n  CASE WHEN close > sar THEN 'Uptrend' ELSE 'Downtrend' END AS sar_signal\nFROM latest WHERE rn = 1\nLIMIT 1\n```\n\n#### T2: RSI divergence detection (price up, RSI down over N days)\n```sql\nWITH data AS (\n  SELECT sq.symbol, sq.date, sq.close, ti.rsi_14,\n    FIRST_VALUE(sq.close) OVER (PARTITION BY sq.symbol ORDER BY sq.date ASC) AS start_price,\n    FIRST_VALUE(ti.rsi_14) OVER (PARTITION BY sq.symbol ORDER BY sq.date ASC) AS start_rsi,\n    ROW_NUMBER() OVER (PARTITION BY sq.symbol ORDER BY sq.date DESC) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.date >= CURRENT_DATE - INTERVAL '30 days'\n    AND ti.rsi_14 IS NOT NULL\n)\nSELECT symbol, date, close, ROUND(rsi_14, 1) AS rsi,\n  ROUND(start_price, 2) AS start_price, ROUND(start_rsi, 1) AS start_rsi,\n  CASE\n    WHEN close > start_price AND rsi_14 < start_rsi THEN 'Bearish divergence'\n    WHEN close < start_price AND rsi_14 > start_rsi THEN 'Bullish divergence'\n    ELSE 'No divergence'\n  END AS divergence_signal\nFROM data\nWHERE rn = 1\n  AND symbol IN ('AAPL.NASDAQ', 'MSFT.NASDAQ', 'GOOGL.NASDAQ')\nLIMIT 20\n```\n\n#### T3: Multi-symbol technical screen (oversold + trend + volume)\n```sql\nWITH latest AS (\n  SELECT sq.symbol, sq.date, sq.close, sq.volume,\n    ti.rsi_14, ti.sma_200, ti.sma_50, ti.ema_50, ti.macd, ti.macdsignal,\n    ti.bb_lower, ti.adx_14, ti.volume_ma_20,\n    ti.cdl_hammer, ti.cdl_engulfing, ti.cdl_morningstar,\n    ROW_NUMBER() OVER (PARTITION BY sq.symbol ORDER BY sq.date DESC) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.date >= CURRENT_DATE - INTERVAL '7 days'\n    AND ti.rsi_14 IS NOT NULL\n)\nSELECT l.symbol, g.name, l.date, l.close,\n  ROUND(l.rsi_14, 1) AS rsi,\n  ROUND(l.close / NULLIF(l.sma_200, 0) * 100 - 100, 1) AS pct_vs_sma200,\n  ROUND(l.adx_14, 1) AS adx,\n  ROUND(l.volume / NULLIF(l.volume_ma_20, 0), 2) AS vol_ratio,\n  CASE WHEN l.cdl_hammer = 100 OR l.cdl_engulfing = 100\n       OR l.cdl_morningstar = 100 THEN 'Bullish pattern' ELSE 'None' END AS pattern\nFROM latest l\nINNER JOIN shibui.general_info g ON l.symbol = g.symbol\nWHERE l.rn = 1\n  AND l.rsi_14 < 30\n  AND l.close < l.bb_lower\n  AND g.type = 'Common Stock'\nORDER BY l.rsi_14 ASC LIMIT 20\n```\n\n#### T4: Trend change detection (SMA crossovers, last 30 days)\n```sql\nWITH data AS (\n  SELECT sq.symbol, sq.date, sq.close,\n    ti.ema_50, ti.sma_200,\n    LAG(ti.ema_50) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS prev_ema50,\n    LAG(ti.sma_200) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS prev_sma200\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti ON sq.symbol = ti.symbol AND sq.date = ti.date\n  WHERE sq.symbol = 'AAPL.NASDAQ'\n    AND sq.date >= CURRENT_DATE - INTERVAL '60 days'\n    AND ti.ema_50 IS NOT NULL AND ti.sma_200 IS NOT NULL\n)\nSELECT date, ROUND(close, 2) AS close,\n  ROUND(ema_50, 2) AS ema_50, ROUND(sma_200, 2) AS sma_200,\n  CASE\n    WHEN ema_50 > sma_200 AND prev_ema50 <= prev_sma200 THEN 'Golden Cross'\n    WHEN ema_50 < sma_200 AND prev_ema50 >= prev_sma200 THEN 'Death Cross'\n    ELSE NULL\n  END AS crossover\nFROM data\nWHERE (ema_50 > sma_200 AND prev_ema50 <= prev_sma200)\n   OR (ema_50 < sma_200 AND prev_ema50 >= prev_sma200)\nORDER BY date DESC LIMIT 10\n```\n"
  6. 1 tool update
    • Changedload_backtesting_workflow1 field changed
      • changedInput schema / properties / _content / default
        Previous value: -"## Backtesting Methodology Guardrails\n\n### Persona note\nBacktests are easy to write and hard to interpret correctly. Your job\nwhen generating a backtest is not just to produce a working SQL query —\nit is to produce a result the user can trust, with the methodological\ncaveats spelled out explicitly. Most retail backtests are wrong in\npredictable ways. Catching those mistakes is the product.\n\nThe single most important behavior: **always surface methodological\ncaveats in your response, even when the user does not ask for them.**\nA correct-looking backtest result without caveats produces false\nconfidence, which is worse than no result at all.\n\n### Hard rules for constructing backtest queries\n\n#### R1: Forward-return windows must acknowledge survivorship.\nWhen computing `LEAD(close, N)` over a long horizon, stocks that\ndelisted, were acquired, or went bankrupt before N trading days\nforward will return NULL. Filtering `WHERE forward_price IS NOT NULL`\nsilently removes them, biasing average returns upward (losers leave\nthe sample disproportionately).\n\nRequired behavior:\n- Compute the NULL rate alongside the result. If >5% of signal rows\n  have NULL forward prices, surface it explicitly.\n- Add a `survivorship_excluded_count` and `survivorship_excluded_pct`\n  column to backtest output, or note it in the response.\n- Never silently filter `forward_price IS NOT NULL` without warning.\n\nExample of the NULL accounting pattern:\n\n```sql\nWITH base AS (\n  SELECT symbol, date, close AS entry_price,\n    LEAD(close, 252) OVER (PARTITION BY symbol ORDER BY date) AS price_1yr\n  FROM shibui.stock_quotes\n  WHERE date >= '2010-01-01'\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(price_1yr) AS signals_with_forward_price,\n  COUNT(*) - COUNT(price_1yr) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(price_1yr)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct\nFROM base\nWHERE entry_price IS NOT NULL\n```\n\nThe data does not currently distinguish \"delisted at -100%\" (bankruptcy)\nfrom \"delisted at acquisition premium\" — both look like NULL forward\nprices. Be honest about this limit when explaining results.\n\n#### R2: Never truncate returns with a hard ABS() filter.\nThe temptation is to filter `WHERE ABS(return) < 3` (i.e. exclude >300%\nor <-100% returns) to \"remove data errors.\" This also silently removes\nreal outliers — large winners and large losers that drive much of the\ntrue return distribution.\n\nRequired behavior:\n- Do not apply `ABS(return) < N` filters in the WHERE clause without\n  explicit user instruction.\n- If outlier handling is needed for robustness, use **winsorization**:\n  compute percentile cuts (e.g., 1st and 99th percentile) and cap\n  outliers at those levels, rather than excluding them.\n- Always report both the raw mean and a winsorized mean if winsorizing.\n- Report the count and magnitude of extreme observations separately so\n  the user can see what the tail looks like.\n\nWinsorization pattern:\n\n```sql\nWITH returns AS (\n  SELECT symbol, return_pct FROM base_signals\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns\n)\nSELECT\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  COUNT(*) FILTER (WHERE return_pct > 500) AS extreme_winners_count,\n  COUNT(*) FILTER (WHERE return_pct < -90) AS extreme_losers_count\nFROM returns CROSS JOIN bounds\n```\n\n#### R3: Single-date sampling produces noisy results.\nSampling a signal on one calendar date per year (e.g., \"Jan 15 each\nyear\") gives ~15 annual observations for a 15-year backtest. The\nresult is sensitive to the chosen date because most technical\nindicators are autocorrelated over short windows.\n\nRequired behavior:\n- For backtests with single-date annual sampling, note the date\n  sensitivity in the response.\n- When feasible, run a multi-date version of the backtest (monthly or\n  quarterly rebalances) and compare. If results differ substantially,\n  the single-date result is noise; if they converge, the signal is\n  more robust.\n- If running multi-date is too expensive, at minimum note: \"This\n  result is based on a single annual sampling date. Sampling on a\n  different date could produce materially different results.\"\n\nMonthly rebalance pattern (denser signal, more robust):\n\n```sql\nWITH monthly_signals AS (\n  SELECT symbol, date, close AS entry_price, indicator_value,\n    LEAD(close, 21) OVER (PARTITION BY symbol ORDER BY date) AS price_1mo,\n    ROW_NUMBER() OVER (PARTITION BY symbol, DATE_TRUNC('month', date) ORDER BY date) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE date >= '2010-01-01' AND indicator_value IS NOT NULL\n)\nSELECT * FROM monthly_signals WHERE rn = 1\nLIMIT 200\n```\n\n#### R4: Be honest when using indicator proxies.\nThe user may ask for an indicator that isn't directly in the database\n(e.g., \"Chaikin Money Flow\", \"Heikin-Ashi candles\", \"Ichimoku Cloud\").\nThe available indicators are listed in the schema (see\n`technical_indicators` table). Substituting a related-but-different\nindicator without disclosure misleads the user.\n\nRequired behavior:\n- If the user asks for an indicator not in the schema, do not\n  substitute silently.\n- State explicitly: \"Chaikin Money Flow is not in the database. The\n  closest available proxy is `mfi_14` (Money Flow Index), which uses a\n  related but distinct formula. Results for `mfi_14` may not generalize\n  to CMF behavior.\"\n- Offer the user the choice: proceed with the proxy (with caveat),\n  decline to run, or compute the indicator manually from price/volume\n  if feasible.\n\n#### R5: Forward returns span calendar boundaries — label them honestly.\nA 252-trading-day forward return from January 15, 2010 ends\napproximately January 15, 2011. Labeling this as a \"2010 return\" is\nmisleading — it's a forward-looking return spanning two calendar years.\n\nRequired behavior:\n- Label backtest results as \"signal year\" rather than \"return year\",\n  or use the entry-date and exit-date as explicit columns.\n- Note in the response: \"Returns are forward-looking from the signal\n  date. The '2010' row represents signals placed in early 2010 and\n  held through early 2011.\"\n\n#### R6: Sample size matters more than win rate.\nA 65% win rate across 30 trades means almost nothing; a 55% win rate\nacross 30,000 trades is meaningful. Backtest results with fewer than\n~500 observations per group should be flagged as low-confidence.\n\nRequired behavior:\n- Always include `COUNT(*)` per group in backtest output.\n- Flag groups with N < 500 explicitly: \"The 2010 BUY group has only\n  X observations — this row should not be over-interpreted.\"\n- For yearly breakdowns where N is naturally small per year, encourage\n  the user to look at the aggregate result across all years before\n  drawing conclusions from any single year.\n\n### Caveats to include in the response (always, not optional)\n\nWhen presenting backtest results to the user, the response must include\na \"Caveats\" section. The exact wording depends on the specific query,\nbut the section must address each of the following that applies:\n\n1. **Survivorship**: What percentage of signals had NULL forward\n   prices, and what direction does that bias results?\n2. **Outliers**: Are extreme returns being filtered, capped, or\n   included raw? If filtered or capped, how does that affect the mean?\n3. **Sampling design**: Single-date or multi-date? What does that\n   imply for robustness?\n4. **Indicator validity**: Is the indicator used the one the user\n   asked for, or a proxy? What's the difference?\n5. **Sample size**: Are any group sizes too small to draw conclusions?\n6. **Calendar conventions**: Are returns labeled by signal date or\n   exit date? Are weekends/holidays handled correctly?\n7. **Transaction costs and slippage**: The backtest does not model\n   trading costs, bid-ask spread, or market impact. Real-world returns\n   would be lower, especially for strategies with high turnover.\n8. **Look-ahead bias**: Is any data used in the signal that wasn't\n   available at signal time? (Usually not, with our point-in-time data,\n   but verify when fundamental signals are involved — restated\n   fundamentals would be look-ahead.)\n\nThe response should be honest without being so long that the user\nstops reading. Aim for: result table, 2-3 sentences of headline\ninterpretation, then a \"Caveats\" section of 3-5 bullets covering the\nissues most relevant to this specific backtest.\n\n### Anti-patterns to avoid\n\n- **Don't** present a backtest as conclusive evidence. The honest\n  framing is \"in this sample, with these assumptions, the result was\n  X.\" Forward-testing or out-of-sample validation is needed before\n  any signal should be acted on.\n- **Don't** compare two strategies on average return alone. Compare\n  on risk-adjusted basis (Sharpe-style: mean / stddev), win rate,\n  max drawdown, and worst-year. A strategy with higher mean and\n  much higher variance is not strictly better.\n- **Don't** ignore the universe-definition question. \"All US stocks\"\n  vs \"S&P 500 constituents\" vs \"market cap > $1B\" produces very\n  different backtest results for the same signal. Be explicit about\n  the universe and acknowledge that the result is conditional on it.\n- **Don't** confuse \"the signal correlates with positive returns\" with\n  \"the signal causes positive returns\" or \"buying on the signal is a\n  good strategy.\" Many signals correlate with returns because they\n  correlate with broader factors (size, momentum, value, volatility)\n  that drive returns. A proper backtest would benchmark against those\n  factors or use factor-neutral construction.\n\n### Risk & validation patterns\n\nThese patterns implement the risk-adjusted comparison and validation\nsteps referenced above. Each integrates survivorship accounting (R1)\nand winsorization (R2) rather than silently filtering NULLs.\n\n#### Risk-adjusted metrics (Sharpe and Sortino)\n\nSharpe measures return per unit of total volatility; Sortino uses\nonly downside volatility, which matters more for the skewed return\ndistributions common in backtests. Always report both alongside raw\nand winsorized means.\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nreturns AS (\n  SELECT entry_price, price_1yr,\n    CASE WHEN price_1yr IS NOT NULL\n      THEN (price_1yr - entry_price) / NULLIF(entry_price, 0) * 100\n    END AS return_pct\n  FROM base WHERE entry_price IS NOT NULL\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns WHERE return_pct IS NOT NULL\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(return_pct)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct,\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe,\n  ROUND(AVG(return_pct) / NULLIF(\n    STDDEV(CASE WHEN return_pct < 0 THEN return_pct END), 0\n  ), 3) AS sortino,\n  ROUND(PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY return_pct), 2) AS p05,\n  ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY return_pct), 2) AS p25,\n  ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY return_pct), 2) AS p75,\n  ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY return_pct), 2) AS p95\nFROM returns CROSS JOIN bounds b\n```\n\nWhen comparing two strategies, compute Sharpe and Sortino for each\ngroup. A strategy with higher mean return but lower Sharpe is taking\non proportionally more risk — the higher return may not compensate.\n\n#### Maximum drawdown\n\nDrawdown measures the worst peak-to-trough decline in cumulative\nreturns. Use with R3's monthly rebalance pattern to track strategy\nperformance across time and surface regime-dependent behavior.\n\n```sql\nWITH monthly_signals AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 21) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1mo,\n    ROW_NUMBER() OVER (\n      PARTITION BY sq.symbol, DATE_TRUNC('month', sq.date) ORDER BY sq.date\n    ) AS rn\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nperiod_returns AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    AVG((price_1mo - entry_price) / NULLIF(entry_price, 0) * 100)\n      FILTER (WHERE price_1mo IS NOT NULL) AS avg_return,\n    COUNT(*) AS signals,\n    COUNT(*) - COUNT(price_1mo) AS survivorship_excluded\n  FROM monthly_signals\n  WHERE rn = 1 AND entry_price IS NOT NULL\n  GROUP BY DATE_TRUNC('month', date)\n),\nwith_peak AS (\n  SELECT month, avg_return, signals, survivorship_excluded,\n    SUM(avg_return) OVER (ORDER BY month) AS cumulative,\n    MAX(SUM(avg_return) OVER (ORDER BY month)) OVER (\n      ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n    ) AS peak\n  FROM period_returns\n)\nSELECT month,\n  ROUND(avg_return, 2) AS period_return,\n  ROUND(cumulative, 2) AS cumulative_return,\n  ROUND(cumulative - peak, 2) AS drawdown,\n  signals, survivorship_excluded\nFROM with_peak\nORDER BY month\nLIMIT 200\n```\n\nReport the maximum drawdown (most negative value) and the month it\noccurred. Strategies with similar average returns but very different\nmax drawdowns have very different risk profiles.\n\n#### Walk-forward validation\n\nWalk-forward tests a strategy across sequential non-overlapping\nwindows. If results are consistent across windows, the signal is\nmore robust. If one window drives most of the aggregate return,\nthe strategy may be overfitted to that market regime.\n\n```sql\nWITH windows AS (\n  SELECT gs::date AS window_start,\n    (gs + INTERVAL '3 years')::date AS window_end\n  FROM generate_series(\n    '2010-01-01'::date, '2022-01-01'::date, '3 years'::interval\n  ) AS t(gs)\n),\nbase AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nwindowed AS (\n  SELECT w.window_start, w.window_end,\n    b.entry_price, b.price_1yr,\n    CASE WHEN b.price_1yr IS NOT NULL\n      THEN (b.price_1yr - b.entry_price) / NULLIF(b.entry_price, 0) * 100\n    END AS return_pct\n  FROM base b\n  INNER JOIN windows w ON b.date >= w.window_start AND b.date < w.window_end\n  WHERE b.entry_price IS NOT NULL\n)\nSELECT\n  window_start, window_end,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND(AVG(return_pct), 2) AS avg_return,\n  ROUND(STDDEV(return_pct), 2) AS stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe\nFROM windowed\nGROUP BY window_start, window_end\nORDER BY window_start\nLIMIT 200\n```\n\nIf Sharpe varies widely across windows (e.g., positive in one,\nnegative in another), the aggregate result is misleading. Report\nper-window results alongside the aggregate.\n\n### Output format for backtest responses\n\nStructure backtest responses as:\n\n1. **Headline result** (1-2 sentences): the most important takeaway,\n   stated plainly. \"MFI ≥ 50 produced an average 1-year return of X%\n   vs Y% for MFI < 50, over Z signals across 2010-2024.\"\n2. **Result table**: the grouped statistics, with sample sizes always\n   visible.\n3. **Caveats** (3-5 bullets): the methodological issues most relevant\n   to this specific backtest. Be specific — \"survivorship bias likely\n   inflates returns by ~X%\" is more useful than \"results may be biased.\"\n4. **Suggested next step**: if the result is encouraging, what would\n   validate it? Out-of-sample test, different universe, different\n   sampling date, factor-neutral construction, etc. Treat the backtest\n   as the first step of validation, not the last.\n"New value: +"## Backtesting Methodology Guardrails\n\n### Persona note\nBacktests are easy to write and hard to interpret correctly. Your job\nwhen generating a backtest is not just to produce a working SQL query —\nit is to produce a result the user can trust, with the methodological\ncaveats spelled out explicitly. Most retail backtests are wrong in\npredictable ways. Catching those mistakes is the product.\n\nThe single most important behavior: **always surface methodological\ncaveats in your response, even when the user does not ask for them.**\nA correct-looking backtest result without caveats produces false\nconfidence, which is worse than no result at all.\n\n### Hard rules for constructing backtest queries\n\n#### R1: Forward-return windows must acknowledge survivorship.\nWhen computing `LEAD(close, N)` over a long horizon, stocks that\ndelisted, were acquired, or went bankrupt before N trading days\nforward will return NULL. Filtering `WHERE forward_price IS NOT NULL`\nsilently removes them, biasing average returns upward (losers leave\nthe sample disproportionately).\n\nRequired behavior:\n- Compute the NULL rate alongside the result. If >5% of signal rows\n  have NULL forward prices, surface it explicitly.\n- Add a `survivorship_excluded_count` and `survivorship_excluded_pct`\n  column to backtest output, or note it in the response.\n- Never silently filter `forward_price IS NOT NULL` without warning.\n\nExample of the NULL accounting pattern:\n\n```sql\nWITH base AS (\n  SELECT symbol, date, close AS entry_price,\n    LEAD(close, 252) OVER (PARTITION BY symbol ORDER BY date) AS price_1yr\n  FROM shibui.stock_quotes\n  WHERE date >= '2010-01-01'\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(price_1yr) AS signals_with_forward_price,\n  COUNT(*) - COUNT(price_1yr) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(price_1yr)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct\nFROM base\nWHERE entry_price IS NOT NULL\n```\n\nThe data does not currently distinguish \"delisted at -100%\" (bankruptcy)\nfrom \"delisted at acquisition premium\" — both look like NULL forward\nprices. Be honest about this limit when explaining results.\n\n#### R2: Never truncate returns with a hard ABS() filter.\nThe temptation is to filter `WHERE ABS(return) < 3` (i.e. exclude >300%\nor <-100% returns) to \"remove data errors.\" This also silently removes\nreal outliers — large winners and large losers that drive much of the\ntrue return distribution.\n\nRequired behavior:\n- Do not apply `ABS(return) < N` filters in the WHERE clause without\n  explicit user instruction.\n- If outlier handling is needed for robustness, use **winsorization**:\n  compute percentile cuts (e.g., 1st and 99th percentile) and cap\n  outliers at those levels, rather than excluding them.\n- Always report both the raw mean and a winsorized mean if winsorizing.\n- Report the count and magnitude of extreme observations separately so\n  the user can see what the tail looks like.\n\nWinsorization pattern:\n\n```sql\nWITH returns AS (\n  SELECT symbol, return_pct FROM base_signals\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns\n)\nSELECT\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  COUNT(*) FILTER (WHERE return_pct > 500) AS extreme_winners_count,\n  COUNT(*) FILTER (WHERE return_pct < -90) AS extreme_losers_count\nFROM returns CROSS JOIN bounds\n```\n\n#### R3: Single-date sampling produces noisy results.\nSampling a signal on one calendar date per year (e.g., \"Jan 15 each\nyear\") gives ~15 annual observations for a 15-year backtest. The\nresult is sensitive to the chosen date because most technical\nindicators are autocorrelated over short windows.\n\nRequired behavior:\n- For backtests with single-date annual sampling, note the date\n  sensitivity in the response.\n- When feasible, run a multi-date version of the backtest (monthly or\n  quarterly rebalances) and compare. If results differ substantially,\n  the single-date result is noise; if they converge, the signal is\n  more robust.\n- If running multi-date is too expensive, at minimum note: \"This\n  result is based on a single annual sampling date. Sampling on a\n  different date could produce materially different results.\"\n\nMonthly rebalance pattern (denser signal, more robust):\n\n```sql\nWITH monthly_signals AS (\n  SELECT symbol, date, close AS entry_price, indicator_value,\n    LEAD(close, 21) OVER (PARTITION BY symbol ORDER BY date) AS price_1mo,\n    ROW_NUMBER() OVER (PARTITION BY symbol, DATE_TRUNC('month', date) ORDER BY date) AS rn\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE date >= '2010-01-01' AND indicator_value IS NOT NULL\n)\nSELECT * FROM monthly_signals WHERE rn = 1\nLIMIT 200\n```\n\n#### R4: Be honest when using indicator proxies.\nThe user may ask for an indicator that isn't directly in the database\n(e.g., \"Chaikin Money Flow\", \"Heikin-Ashi candles\", \"Ichimoku Cloud\").\nThe available indicators are listed in the schema (see\n`technical_indicators` table). Substituting a related-but-different\nindicator without disclosure misleads the user.\n\nRequired behavior:\n- If the user asks for an indicator not in the schema, do not\n  substitute silently.\n- State explicitly: \"Chaikin Money Flow is not in the database. The\n  closest available proxy is `mfi_14` (Money Flow Index), which uses a\n  related but distinct formula. Results for `mfi_14` may not generalize\n  to CMF behavior.\"\n- Offer the user the choice: proceed with the proxy (with caveat),\n  decline to run, or compute the indicator manually from price/volume\n  if feasible.\n\n#### R5: Forward returns span calendar boundaries — label them honestly.\nA 252-trading-day forward return from January 15, 2010 ends\napproximately January 15, 2011. Labeling this as a \"2010 return\" is\nmisleading — it's a forward-looking return spanning two calendar years.\n\nRequired behavior:\n- Label backtest results as \"signal year\" rather than \"return year\",\n  or use the entry-date and exit-date as explicit columns.\n- Note in the response: \"Returns are forward-looking from the signal\n  date. The '2010' row represents signals placed in early 2010 and\n  held through early 2011.\"\n\n#### R6: Sample size matters more than win rate.\nA 65% win rate across 30 trades means almost nothing; a 55% win rate\nacross 30,000 trades is meaningful. Backtest results with fewer than\n~500 observations per group should be flagged as low-confidence.\n\nRequired behavior:\n- Always include `COUNT(*)` per group in backtest output.\n- Flag groups with N < 500 explicitly: \"The 2010 BUY group has only\n  X observations — this row should not be over-interpreted.\"\n- For yearly breakdowns where N is naturally small per year, encourage\n  the user to look at the aggregate result across all years before\n  drawing conclusions from any single year.\n\n#### R7: Signal returns must be compared against the universe baseline.\nA signal group returning 12% is only meaningful if the universe\nreturned less. Without a benchmark, the user cannot distinguish alpha\n(the signal's edge) from beta (the market moved). The database has no\nindex data (no S&P 500, no SPY), so the benchmark is the universe's\nown average return — all stocks matching the base filters, ignoring\nthe signal condition. This is a cleaner benchmark than an index\nbecause it controls for the exact universe definition (market-cap\nfloor, date range, exchange).\n\nRequired behavior:\n- Every backtest that reports a signal group return must also compute\n  the full-universe average return for the same period and filters.\n- Report the spread (signal return minus universe return) alongside\n  both figures.\n- If the spread is near zero or negative, say so plainly: \"The signal\n  did not outperform the universe average.\"\n\nUniverse-benchmark pattern:\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr,\n    ti.mfi_14\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE sq.date >= '2010-01-01' AND sq.date <= '2023-01-01'\n),\nreturns AS (\n  SELECT symbol, date, mfi_14,\n    CASE WHEN price_1yr IS NOT NULL\n      THEN (price_1yr - entry_price) / NULLIF(entry_price, 0) * 100\n    END AS return_pct\n  FROM base WHERE entry_price IS NOT NULL\n)\nSELECT\n  'Signal (MFI >= 50)' AS group_label,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  ROUND(AVG(return_pct), 2) AS avg_return\nFROM returns WHERE mfi_14 >= 50\nUNION ALL\nSELECT\n  'Full universe' AS group_label,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  ROUND(AVG(return_pct), 2) AS avg_return\nFROM returns\n```\n\nThe \"Full universe\" row includes the signal group — this is\nintentional. The universe mean is the unconditional average. The\ndifference (signal avg minus universe avg) is the signal's marginal\ncontribution.\n\n#### R8: Check sector concentration of the signal group.\nA signal that appears profitable in aggregate may be overweight in one\nsector. If MFI >= 50 stocks are 60% tech in 2020-2021, the \"alpha\" is\nsector beta disguised as signal alpha. The `general_info` table has\n`gics_sector` (11 GICS sectors, ~5,800 of ~9,950 rows populated).\n\nRequired behavior:\n- For any signal-based backtest, compute the sector breakdown of the\n  signal group versus the full universe.\n- If any single sector accounts for more than 40% of the signal group\n  (or is 2x its universe weight), flag it explicitly.\n- Note that ~4,150 symbols have NULL `gics_sector` (ETFs, preferred\n  shares, closed-end funds). Report the NULL count but do not exclude\n  these rows from the return calculation — only from the sector\n  breakdown.\n\nSector-concentration pattern:\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, ti.mfi_14\n  FROM shibui.stock_quotes sq\n  INNER JOIN shibui.technical_indicators ti USING (symbol, date)\n  WHERE sq.date >= '2020-01-01' AND sq.date <= '2022-01-01'\n    AND ti.mfi_14 IS NOT NULL\n),\nsignal_symbols AS (\n  SELECT DISTINCT symbol FROM base WHERE mfi_14 >= 50\n),\nuniverse_symbols AS (\n  SELECT DISTINCT symbol FROM base\n)\nSELECT\n  g.gics_sector,\n  COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL) AS signal_count,\n  COUNT(*) AS universe_count,\n  ROUND(COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL) * 100.0\n    / NULLIF(SUM(COUNT(*) FILTER (WHERE ss.symbol IS NOT NULL)) OVER (), 0), 1)\n    AS signal_pct,\n  ROUND(COUNT(*) * 100.0\n    / NULLIF(SUM(COUNT(*)) OVER (), 0), 1) AS universe_pct\nFROM universe_symbols us\nINNER JOIN shibui.general_info g ON us.symbol = g.symbol\nLEFT JOIN signal_symbols ss ON us.symbol = ss.symbol\nWHERE g.gics_sector IS NOT NULL\nGROUP BY g.gics_sector\nORDER BY signal_pct DESC\nLIMIT 20\n```\n\nIf `signal_pct` for any sector is substantially higher than\n`universe_pct`, the signal is sector-concentrated. Note this in the\nresponse and suggest re-running the backtest sector-neutral\n(equal-weighting sectors or excluding the dominant sector) to see if\nthe signal survives.\n\n#### R9: Flag multiple-testing bias when several thresholds are compared.\nIf the user tests MFI >= 40, 45, 50, 55, 60 and picks the best\nresult, the winning threshold is biased upward. With five independent\ntests at the 5% significance level, the probability of at least one\nfalse positive is ~23%. This is the classic data-mining / p-hacking\nproblem and applies equally to threshold sweeps, indicator selection,\nand holding-period optimization.\n\nRequired behavior:\n- If the conversation includes multiple backtest variants (different\n  thresholds, indicators, or holding periods), explicitly note that\n  the best-performing variant benefits from selection bias.\n- State: \"The best result out of N variants is expected to look better\n  than its true forward performance. Out-of-sample validation or\n  walk-forward testing (see Risk & validation patterns) is needed\n  before treating this result as reliable.\"\n- Never present the best-of-N result as the expected forward\n  performance without this caveat.\n- When feasible, suggest Bonferroni-style framing: \"With N tests, the\n  significance bar is higher — a result that looks marginal at the\n  single-test level is likely noise.\"\n\n### Caveats to include in the response (always, not optional)\n\nWhen presenting backtest results to the user, the response must include\na \"Caveats\" section. The exact wording depends on the specific query,\nbut the section must address each of the following that applies:\n\n1. **Survivorship**: What percentage of signals had NULL forward\n   prices, and what direction does that bias results?\n2. **Outliers**: Are extreme returns being filtered, capped, or\n   included raw? If filtered or capped, how does that affect the mean?\n3. **Sampling design**: Single-date or multi-date? What does that\n   imply for robustness?\n4. **Indicator validity**: Is the indicator used the one the user\n   asked for, or a proxy? What's the difference?\n5. **Sample size**: Are any group sizes too small to draw conclusions?\n6. **Calendar conventions**: Are returns labeled by signal date or\n   exit date? Are weekends/holidays handled correctly?\n7. **Transaction costs and slippage**: The backtest does not model\n   trading costs, bid-ask spread, or market impact. Real-world returns\n   would be lower, especially for strategies with high turnover.\n8. **Look-ahead bias**: Is any data used in the signal that wasn't\n   available at signal time? (Usually not, with our point-in-time data,\n   but verify when fundamental signals are involved — restated\n   fundamentals would be look-ahead.)\n9. **Benchmark comparison**: Does the signal outperform the universe\n   average? How large is the spread? A positive signal return with a\n   near-zero or negative spread is not alpha (R7).\n10. **Sector concentration**: Is the signal group overweight in any\n    sector relative to the universe? If so, the result may be driven\n    by sector performance rather than the signal itself (R8).\n11. **Multiple testing**: Were multiple variants tested in this\n    conversation? If so, the best result is biased upward by selection\n    and should not be taken at face value without out-of-sample\n    validation (R9).\n\nThe response should be honest without being so long that the user\nstops reading. Aim for: result table, 2-3 sentences of headline\ninterpretation, then a \"Caveats\" section of 3-5 bullets covering the\nissues most relevant to this specific backtest.\n\n### Anti-patterns to avoid\n\n- **Don't** present a backtest as conclusive evidence. The honest\n  framing is \"in this sample, with these assumptions, the result was\n  X.\" Forward-testing or out-of-sample validation is needed before\n  any signal should be acted on.\n- **Don't** compare two strategies on average return alone. Compare\n  on risk-adjusted basis (Sharpe-style: mean / stddev), win rate,\n  max drawdown, and worst-year. A strategy with higher mean and\n  much higher variance is not strictly better.\n- **Don't** ignore the universe-definition question. \"All US stocks\"\n  vs \"S&P 500 constituents\" vs \"market cap > $1B\" produces very\n  different backtest results for the same signal. Be explicit about\n  the universe and acknowledge that the result is conditional on it.\n  Compare signal returns against the universe average (R7) and check\n  for sector concentration (R8).\n- **Don't** confuse \"the signal correlates with positive returns\" with\n  \"the signal causes positive returns\" or \"buying on the signal is a\n  good strategy.\" Many signals correlate with returns because they\n  correlate with broader factors (size, momentum, value, volatility)\n  that drive returns. A proper backtest would benchmark against those\n  factors or use factor-neutral construction. When multiple thresholds\n  or variants are tested, the best result is subject to data-mining\n  bias (R9).\n- **Don't** present signal returns without the universe baseline. A\n  12% signal return means nothing if the universe returned 14%. Always\n  compute and show the spread (R7).\n\n### Risk & validation patterns\n\nThese patterns implement the risk-adjusted comparison and validation\nsteps referenced above. Each integrates survivorship accounting (R1)\nand winsorization (R2) rather than silently filtering NULLs.\n\n#### Risk-adjusted metrics (Sharpe and Sortino)\n\nSharpe measures return per unit of total volatility; Sortino uses\nonly downside volatility, which matters more for the skewed return\ndistributions common in backtests. Always report both alongside raw\nand winsorized means.\n\n```sql\nWITH base AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nreturns AS (\n  SELECT entry_price, price_1yr,\n    CASE WHEN price_1yr IS NOT NULL\n      THEN (price_1yr - entry_price) / NULLIF(entry_price, 0) * 100\n    END AS return_pct\n  FROM base WHERE entry_price IS NOT NULL\n),\nbounds AS (\n  SELECT\n    PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY return_pct) AS p01,\n    PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY return_pct) AS p99\n  FROM returns WHERE return_pct IS NOT NULL\n)\nSELECT\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND((COUNT(*) - COUNT(return_pct)) * 100.0 / NULLIF(COUNT(*), 0), 1) AS excluded_pct,\n  ROUND(AVG(return_pct), 2) AS raw_mean,\n  ROUND(AVG(GREATEST(LEAST(return_pct, b.p99), b.p01)), 2) AS winsorized_mean,\n  ROUND(STDDEV(return_pct), 2) AS raw_stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe,\n  ROUND(AVG(return_pct) / NULLIF(\n    STDDEV(CASE WHEN return_pct < 0 THEN return_pct END), 0\n  ), 3) AS sortino,\n  ROUND(PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY return_pct), 2) AS p05,\n  ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY return_pct), 2) AS p25,\n  ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY return_pct), 2) AS p75,\n  ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY return_pct), 2) AS p95\nFROM returns CROSS JOIN bounds b\n```\n\nWhen comparing two strategies, compute Sharpe and Sortino for each\ngroup. A strategy with higher mean return but lower Sharpe is taking\non proportionally more risk — the higher return may not compensate.\n\n#### Maximum drawdown\n\nDrawdown measures the worst peak-to-trough decline in cumulative\nreturns. Use with R3's monthly rebalance pattern to track strategy\nperformance across time and surface regime-dependent behavior.\n\n```sql\nWITH monthly_signals AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 21) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1mo,\n    ROW_NUMBER() OVER (\n      PARTITION BY sq.symbol, DATE_TRUNC('month', sq.date) ORDER BY sq.date\n    ) AS rn\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nperiod_returns AS (\n  SELECT\n    DATE_TRUNC('month', date) AS month,\n    AVG((price_1mo - entry_price) / NULLIF(entry_price, 0) * 100)\n      FILTER (WHERE price_1mo IS NOT NULL) AS avg_return,\n    COUNT(*) AS signals,\n    COUNT(*) - COUNT(price_1mo) AS survivorship_excluded\n  FROM monthly_signals\n  WHERE rn = 1 AND entry_price IS NOT NULL\n  GROUP BY DATE_TRUNC('month', date)\n),\nwith_peak AS (\n  SELECT month, avg_return, signals, survivorship_excluded,\n    SUM(avg_return) OVER (ORDER BY month) AS cumulative,\n    MAX(SUM(avg_return) OVER (ORDER BY month)) OVER (\n      ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n    ) AS peak\n  FROM period_returns\n)\nSELECT month,\n  ROUND(avg_return, 2) AS period_return,\n  ROUND(cumulative, 2) AS cumulative_return,\n  ROUND(cumulative - peak, 2) AS drawdown,\n  signals, survivorship_excluded\nFROM with_peak\nORDER BY month\nLIMIT 200\n```\n\nReport the maximum drawdown (most negative value) and the month it\noccurred. Strategies with similar average returns but very different\nmax drawdowns have very different risk profiles.\n\n#### Walk-forward validation\n\nWalk-forward tests a strategy across sequential non-overlapping\nwindows. If results are consistent across windows, the signal is\nmore robust. If one window drives most of the aggregate return,\nthe strategy may be overfitted to that market regime.\n\n```sql\nWITH windows AS (\n  SELECT gs::date AS window_start,\n    (gs + INTERVAL '3 years')::date AS window_end\n  FROM generate_series(\n    '2010-01-01'::date, '2022-01-01'::date, '3 years'::interval\n  ) AS t(gs)\n),\nbase AS (\n  SELECT sq.symbol, sq.date, sq.close AS entry_price,\n    LEAD(sq.close, 252) OVER (PARTITION BY sq.symbol ORDER BY sq.date) AS price_1yr\n  FROM shibui.stock_quotes sq\n  WHERE sq.date >= '2010-01-01'\n),\nwindowed AS (\n  SELECT w.window_start, w.window_end,\n    b.entry_price, b.price_1yr,\n    CASE WHEN b.price_1yr IS NOT NULL\n      THEN (b.price_1yr - b.entry_price) / NULLIF(b.entry_price, 0) * 100\n    END AS return_pct\n  FROM base b\n  INNER JOIN windows w ON b.date >= w.window_start AND b.date < w.window_end\n  WHERE b.entry_price IS NOT NULL\n)\nSELECT\n  window_start, window_end,\n  COUNT(*) AS total_signals,\n  COUNT(return_pct) AS with_forward_price,\n  COUNT(*) - COUNT(return_pct) AS survivorship_excluded,\n  ROUND(AVG(return_pct), 2) AS avg_return,\n  ROUND(STDDEV(return_pct), 2) AS stddev,\n  ROUND(AVG(return_pct) / NULLIF(STDDEV(return_pct), 0), 3) AS sharpe\nFROM windowed\nGROUP BY window_start, window_end\nORDER BY window_start\nLIMIT 200\n```\n\nIf Sharpe varies widely across windows (e.g., positive in one,\nnegative in another), the aggregate result is misleading. Report\nper-window results alongside the aggregate.\n\n### Output format for backtest responses\n\nStructure backtest responses as:\n\n1. **Headline result** (1-2 sentences): the most important takeaway,\n   stated plainly. \"MFI ≥ 50 produced an average 1-year return of X%\n   vs Y% for MFI < 50, over Z signals across 2010-2024.\"\n2. **Result table**: the grouped statistics, with sample sizes always\n   visible.\n3. **Caveats** (3-5 bullets): the methodological issues most relevant\n   to this specific backtest. Be specific — \"survivorship bias likely\n   inflates returns by ~X%\" is more useful than \"results may be biased.\"\n4. **Suggested next step**: if the result is encouraging, what would\n   validate it? Out-of-sample test, different universe, different\n   sampling date, factor-neutral construction, etc. Treat the backtest\n   as the first step of validation, not the last.\n"
  7. 1 tool update
    • Addedload_backtesting_workflow
  8. 1 tool update
    • Addedget_query_patterns

Frequently Asked Questions

Discussions

No comments yet. Be the first to start the discussion!

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables financial research on US-listed equities by answering natural language questions with structured data from fundamentals, prices, earnings, and insider activity.
    3
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Provides as-reported US equity fundamentals, live quotes, financial statements, valuation comps, and a screener from SEC filings, with per-cell filing provenance for citations.
    7
    MIT
  • A
    license
    C
    quality
    B
    maintenance
    Enables AI agents and LLM apps to answer natural-language financial questions using live market data, including stocks, crypto, forex, futures, indices, ETFs, economic data, news, sentiment, SEC filings, earnings, financials, insider trading, ESG, credit ratings, and web traffic.
    132
    499
    MIT
Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

TDQS

A4.4/5.0
Disambiguation4/5

Most tools have clearly distinct domains (backtesting, comparison, earnings, filings, fundamentals, insider, screening, technical), and descriptions provide specific trigger conditions. However, stock_data_query and export_to_excel are very similar (same query, different output), and some workflow boundaries overlap (e.g., earnings vs. fundamental both mention revenue trends; filing vs. insider both involve SEC documents).

Naming Consistency3/5

All names use snake_case, but the pattern is inconsistent: get_database_schema and get_query_patterns follow verb_noun, the eight load_*_workflow tools follow verb_noun (consistent among themselves), but stock_data_query is a noun phrase with no verb, and export_to_excel includes a preposition. The mixed conventions are still readable but not uniform.

Tool Count4/5

At 12 tools, the count is within the expected 3-15 range and appropriate for the broad scope of comprehensive stock analysis. However, eight of the tools are 'load_*_workflow' entries that are structurally identical, which makes the set feel slightly heavier than necessary, though each covers a distinct analytical domain.

Completeness5/5

The tool set covers the full lifecycle of the domain: schema discovery, query guidance, raw query execution, export in a branded format, and eight specialized workflows covering backtesting, comparisons, earnings, filings, fundamentals, insider trading, screening, and technical analysis. No significant gaps are apparent for the stated purpose of US stock/financial data analysis.

Resources