#!/usr/bin/env python3
"""
Earnings Call NLP Module — Tone Analysis, Confidence Scoring, Dodge Detection
Linguistic analysis of earnings call transcripts using:
- Loughran-McDonald financial sentiment dictionary
- Hedging language detection
- Management confidence scoring
- Question-dodging detection
- Tone shift analysis between prepared remarks and Q&A
Data Source: SEC EDGAR 8-K filings with earnings call transcripts
Author: QUANTCLAW DATA Build Agent
Phase: 47
"""
import sys
import re
import requests
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import json
from collections import Counter
import statistics
# SEC EDGAR Base URL
EDGAR_BASE_URL = "https://www.sec.gov"
USER_AGENT = "QuantClaw Data quantclaw@example.com"
# Loughran-McDonald Financial Sentiment Dictionary
# Based on https://sraf.nd.edu/loughranmcdonald-master-dictionary/
LM_POSITIVE = {
'able', 'abundance', 'abundant', 'acclaimed', 'accomplish', 'accomplished',
'accomplishment', 'achieve', 'achieved', 'achievement', 'achievements', 'advancing',
'advantage', 'advantaged', 'advantageous', 'advances', 'affirm', 'affordable',
'ambitious', 'assurance', 'assurances', 'assure', 'attain', 'attractive',
'beneficial', 'beneficially', 'benefit', 'benefited', 'benefiting', 'benefits',
'best', 'better', 'bolstered', 'boom', 'booming', 'boost', 'breakthrough',
'brilliant', 'bullish', 'capable', 'clarity', 'collaborative', 'collaborative',
'confident', 'constructive', 'creative', 'delight', 'delighted', 'effective',
'efficient', 'efficiently', 'empowered', 'enable', 'enabled', 'enables',
'encouraging', 'enhance', 'enhanced', 'enhancement', 'enhances', 'enhancing',
'enjoy', 'enjoyed', 'excellent', 'exceptional', 'excited', 'excitement',
'exciting', 'expand', 'expanded', 'expanding', 'expansion', 'fantastic',
'favorable', 'favorably', 'focused', 'gain', 'gained', 'gaining', 'gains',
'good', 'great', 'greater', 'greatest', 'grow', 'growing', 'growth',
'high', 'highlight', 'highlights', 'honored', 'improve', 'improved',
'improvement', 'improvements', 'improving', 'impressive', 'increase',
'increased', 'increases', 'increasing', 'innovation', 'innovative',
'innovator', 'integrity', 'leader', 'leading', 'lucrative', 'momentum',
'opportunities', 'opportunity', 'optimal', 'optimistic', 'optimum',
'outpace', 'outpaced', 'outperform', 'outperformed', 'outperforming',
'pleased', 'positive', 'positively', 'potential', 'powerful', 'praise',
'praised', 'premier', 'premium', 'prestigious', 'profit', 'profitable',
'profitably', 'profited', 'profitability', 'progress', 'progressing',
'prominent', 'promise', 'promised', 'promises', 'promising', 'prospect',
'prospective', 'prospects', 'prosper', 'prosperity', 'prosperous',
'proven', 'quality', 'record', 'remarkable', 'resolve', 'resolved',
'reward', 'rewarded', 'rewarding', 'rewards', 'robust', 'safe', 'safely',
'satisfaction', 'satisfactorily', 'satisfactory', 'satisfied', 'satisfy',
'secure', 'secured', 'securely', 'security', 'solid', 'solutions',
'solve', 'solved', 'solving', 'spectacular', 'stability', 'stabilization',
'stabilize', 'stabilized', 'stable', 'strength', 'strengthen', 'strengthened',
'strengthening', 'strengths', 'strong', 'stronger', 'strongest', 'succeed',
'succeeded', 'success', 'successes', 'successful', 'successfully',
'superior', 'support', 'supported', 'supporting', 'supportive', 'supports',
'surpass', 'surpassed', 'surpasses', 'surpassing', 'sustainable', 'top',
'tremendous', 'unmatched', 'unprecedented', 'upturn', 'valuable', 'value',
'valued', 'vibrant', 'win', 'winner', 'winners', 'winning', 'wins'
}
LM_NEGATIVE = {
'abandon', 'abandoned', 'abandoning', 'abandonment', 'abandon', 'abdicated',
'aberration', 'aborted', 'absence', 'absent', 'abuse', 'abused', 'abuses',
'abusive', 'accident', 'accidental', 'accidentally', 'adversarial', 'adversaries',
'adversary', 'adverse', 'adversely', 'adversity', 'alarm', 'alarmed', 'alarming',
'allegations', 'annoy', 'annoyance', 'annoyed', 'annoying', 'antitrust', 'argued',
'arguing', 'argument', 'arguments', 'arrest', 'arrested', 'artificial', 'assert',
'asserted', 'assertion', 'attack', 'attacked', 'attacking', 'attacks', 'bad',
'badly', 'bail', 'bailed', 'bailout', 'ban', 'bankrupt', 'bankruptcy', 'bankrupted',
'barrier', 'barriers', 'bottleneck', 'boycott', 'boycotted', 'boycotting', 'breach',
'breached', 'breaches', 'breakdown', 'breakdowns', 'broken', 'burden', 'burdened',
'burdensome', 'burdens', 'catastrophe', 'catastrophic', 'caution', 'cautioned',
'cautioning', 'cautious', 'cautiously', 'challenge', 'challenged', 'challenges',
'challenging', 'chaotic', 'cheat', 'cheated', 'cheating', 'claim', 'claimed',
'claims', 'coerce', 'coerced', 'coercion', 'collapse', 'collapsed', 'collapsing',
'collision', 'collisions', 'collusion', 'complain', 'complained', 'complaining',
'complaint', 'complaints', 'complicated', 'complication', 'complications',
'concern', 'concerned', 'concerns', 'concerted', 'condemn', 'condemnation',
'condemned', 'condemning', 'condemns', 'confiscate', 'confiscated', 'confiscation',
'conflict', 'conflicted', 'conflicting', 'conflicts', 'confront', 'confrontation',
'confrontational', 'confronted', 'confronting', 'confronts', 'confuse', 'confused',
'confusing', 'confusion', 'conspiracies', 'conspiracy', 'conspired', 'conspiring',
'contaminate', 'contaminated', 'contamination', 'contempt', 'contend', 'contended',
'contending', 'contends', 'contention', 'contentious', 'contentiously', 'contest',
'contested', 'contesting', 'controversial', 'controversies', 'controversy',
'corrected', 'correcting', 'correction', 'corrections', 'corrupt', 'corrupted',
'corruption', 'cost', 'costly', 'counterclaim', 'counterclaimed', 'counterclaims',
'counterfeit', 'counterfeited', 'counterfeiting', 'crime', 'crimes', 'criminal',
'crisis', 'critical', 'critically', 'criticism', 'criticisms', 'criticize',
'criticized', 'criticizes', 'criticizing', 'culpability', 'culpable', 'curtail',
'curtailed', 'curtailing', 'curtailment', 'curtailments', 'cut', 'cutback', 'cuts',
'damage', 'damaged', 'damages', 'damaging', 'danger', 'dangerous', 'dangerously',
'dangers', 'deadlock', 'deadlocked', 'deadweight', 'decline', 'declined', 'declines',
'declining', 'decrease', 'decreased', 'decreases', 'decreasing', 'defamation',
'defamatory', 'defame', 'defamed', 'default', 'defaulted', 'defaulting', 'defaults',
'defeat', 'defeated', 'defeating', 'defeats', 'defect', 'defective', 'defects',
'defend', 'defendant', 'defendants', 'defended', 'defending', 'defends', 'defensive',
'deficiency', 'deficient', 'deficit', 'deficits', 'delay', 'delayed', 'delaying',
'delays', 'demise', 'demolish', 'demolished', 'demolishing', 'demolition', 'demote',
'demoted', 'demotes', 'demoting', 'denial', 'denials', 'denied', 'denies', 'deny',
'denying', 'deplete', 'depleted', 'depleting', 'depletion', 'depressed', 'depressing',
'depression', 'deprive', 'deprived', 'deprives', 'depriving', 'derelict', 'dereliction',
'destabilization', 'destabilize', 'destabilized', 'destabilizing', 'destroy', 'destroyed',
'destroying', 'destroys', 'destruction', 'destructive', 'detain', 'detained', 'detention',
'deter', 'deteriorate', 'deteriorated', 'deteriorates', 'deteriorating', 'deterioration',
'deterred', 'deterrence', 'deterrent', 'deterrents', 'deterring', 'deters', 'detrimental',
'detrimentally', 'deviate', 'deviation', 'deviations', 'difficult', 'difficulties',
'difficulty', 'diminish', 'diminished', 'diminishes', 'diminishing', 'diminution',
'disadvantage', 'disadvantaged', 'disadvantageous', 'disadvantages', 'disagree',
'disagreeable', 'disagreed', 'disagreement', 'disagreements', 'disagrees', 'disappointing',
'disappointingly', 'disappointment', 'disaster', 'disastrous', 'disastrously', 'disasters',
'disclose', 'disclosed', 'discloses', 'disclosing', 'discontinuance', 'discontinuation',
'discontinue', 'discontinued', 'discontinues', 'discontinuing', 'discourage', 'discouraged',
'discourages', 'discouraging', 'discrepancies', 'discrepancy', 'disfavor', 'disfavored',
'disloyal', 'disloyalty', 'dismal', 'dismally', 'dismiss', 'dismissal', 'dismissals',
'dismissed', 'dismissing', 'dismissive', 'disparity', 'displace', 'displaced',
'displacement', 'displaces', 'displacing', 'dispute', 'disputed', 'disputes', 'disputing',
'disqualification', 'disqualified', 'disqualifies', 'disqualify', 'disqualifying',
'disregard', 'disregarded', 'disregarding', 'disregards', 'disreputable', 'disrepute',
'disrupt', 'disrupted', 'disrupting', 'disruption', 'disruptions', 'disruptive', 'disrupts',
'dissatisfaction', 'dissatisfied', 'distort', 'distorted', 'distorting', 'distortion',
'distortions', 'distorts', 'distract', 'distracted', 'distracting', 'distraction',
'distractions', 'distracts', 'distress', 'distressed', 'disturb', 'disturbance', 'disturbed',
'disturbing', 'disturbs', 'divert', 'diverted', 'diverting', 'diverts', 'doubt', 'doubted',
'doubtful', 'doubts', 'downgrade', 'downgraded', 'downgrades', 'downgrading', 'downsizing',
'downturn', 'downturns', 'downward', 'downwards', 'drastically', 'drawback', 'drawbacks',
'dropped', 'dropping', 'drops', 'drought', 'edge', 'egregious', 'egregiously', 'embargo',
'embargoed', 'embargoes', 'embarrass', 'embarrassed', 'embarrassing', 'embarrassment',
'encroach', 'encroached', 'encroaches', 'encroaching', 'encroachment', 'encumber',
'encumbered', 'encumbering', 'encumbers', 'encumbrance', 'encumbrances', 'endangered',
'enemies', 'enemy', 'erode', 'eroded', 'erodes', 'eroding', 'erosion', 'erratic',
'erratically', 'erred', 'erring', 'erroneous', 'erroneously', 'error', 'errors',
'evade', 'evaded', 'evades', 'evading', 'evasion', 'evasive', 'exacerbate', 'exacerbated',
'exacerbates', 'exacerbating', 'exacerbation', 'exaggerate', 'exaggerated', 'exaggerates',
'exaggerating', 'exaggeration', 'excessive', 'excessively', 'exculpate', 'exculpated',
'exculpates', 'exculpating', 'exculpation', 'exculpatory', 'exonerate', 'exonerated',
'exonerates', 'exonerating', 'exoneration', 'exploit', 'exploitation', 'exploitations',
'exploitative', 'exploited', 'exploiting', 'exploits', 'expose', 'exposed', 'exposes',
'exposing', 'exposure', 'expropriated', 'expropriation', 'fail', 'failed', 'failing',
'failings', 'fails', 'failure', 'failures', 'fallout', 'false', 'falsely', 'falsification',
'falsifications', 'falsified', 'falsifies', 'falsify', 'falsifying', 'falsity', 'fatalities',
'fatality', 'fatally', 'fault', 'faulted', 'faults', 'faulty', 'fear', 'fears', 'felonies',
'felonious', 'felony', 'fine', 'fined', 'fines', 'fining', 'fired', 'firing', 'flaw',
'flawed', 'flaws', 'forbid', 'forbidding', 'forbids', 'force', 'forced', 'forces',
'forcing', 'foreclose', 'foreclosed', 'forecloses', 'foreclosing', 'foreclosure',
'foreclosures', 'forego', 'foregoes', 'foregone', 'forestall', 'forestalled', 'forestalling',
'forestalls', 'forfeit', 'forfeited', 'forfeiting', 'forfeits', 'forfeiture', 'forfeitures',
'fraud', 'frauds', 'fraudulence', 'fraudulent', 'fraudulently', 'frivolous', 'frivolously',
'frustrate', 'frustrated', 'frustrates', 'frustrating', 'frustratingly', 'frustration',
'frustrations', 'grievance', 'grievances', 'grossly', 'guilty', 'halt', 'halted', 'halting',
'halts', 'hamper', 'hampered', 'hampering', 'hampers', 'harm', 'harmed', 'harmful',
'harmfully', 'harming', 'harms', 'harsh', 'harsher', 'harshest', 'harshly', 'harshness',
'hazard', 'hazardous', 'hazards', 'hinder', 'hindered', 'hindering', 'hinders', 'hindrance',
'hindrances', 'hostile', 'hostility', 'hurt', 'hurting', 'hurts', 'illegal', 'illegalities',
'illegality', 'illegally', 'illegible', 'illiquid', 'illiquidity', 'imbalance', 'imbalances',
'imminent', 'impair', 'impaired', 'impairing', 'impairment', 'impairments', 'impairs',
'impasse', 'impasses', 'impede', 'impeded', 'impedes', 'impediment', 'impediments',
'impeding', 'impending', 'imperfection', 'imperfections', 'imperil', 'imperiled', 'imperiling',
'imperils', 'impossible', 'imposture', 'impound', 'impounded', 'impounding', 'impounds',
'impracticable', 'impractical', 'impracticalities', 'impracticality', 'imprisoned',
'imprisonment', 'improper', 'improperly', 'improprieties', 'impropriety', 'imprudent',
'imprudently', 'inability', 'inaccessible', 'inaccuracies', 'inaccuracy', 'inaccurate',
'inaccurately', 'inaction', 'inactions', 'inadequacies', 'inadequacy', 'inadequate',
'inadequately', 'inadvertent', 'inadvertently', 'inadvisable', 'inappropriate',
'inappropriately', 'incapable', 'incapacity', 'incarcerate', 'incarcerated', 'incarcerating',
'incarceration', 'incidence', 'incidences', 'incident', 'incidents', 'incompatibilities',
'incompatibility', 'incompatible', 'incompetence', 'incompetent', 'incompetently', 'incomplete',
'incompletely', 'incompleteness', 'inconclusive', 'inconsist', 'inconsistencies', 'inconsistency',
'inconsistent', 'inconsistently', 'inconvenience', 'inconveniences', 'inconvenient', 'incorrect',
'incorrectly', 'indecency', 'indecent', 'indemnification', 'indemnifications', 'indemnified',
'indemnifies', 'indemnify', 'indemnifying', 'indemnity', 'ineffective', 'ineffectively',
'ineffectiveness', 'inefficiencies', 'inefficiency', 'inefficient', 'inefficiently', 'ineligible',
'ineligibility', 'inequitable', 'inequitably', 'inequities', 'inequity', 'inevitable',
'inevitably', 'inferior', 'inflicted', 'infraction', 'infractions', 'infringe', 'infringed',
'infringement', 'infringements', 'infringes', 'infringing', 'inhibit', 'inhibited', 'inhibiting',
'inhibits', 'injunction', 'injunctions', 'injure', 'injured', 'injures', 'injuries', 'injuring',
'injurious', 'injury', 'inordinate', 'inordinately', 'inquiries', 'inquiry', 'insecure',
'insecurity', 'insolvent', 'insolvency', 'instability', 'instigate', 'instigated', 'instigating',
'instigation', 'instigator', 'instigators', 'insufficient', 'insufficiently', 'insurrection',
'insurrections', 'intense', 'interfere', 'interfered', 'interference', 'interferes', 'interfering',
'interrupt', 'interrupted', 'interrupting', 'interruption', 'interruptions', 'interrupts',
'intimidate', 'intimidated', 'intimidates', 'intimidating', 'intimidation', 'invalidate',
'invalidated', 'invalidates', 'invalidating', 'invalidation', 'invalidity', 'investigation',
'investigations', 'involuntarily', 'involuntary', 'irrational', 'irrationality', 'irrationally',
'irreconcilable', 'irrecoverable', 'irrecoverably', 'irregular', 'irregularities', 'irregularity',
'irregularly', 'irreparable', 'irreparably', 'irreversible', 'jeopardize', 'jeopardized',
'jeopardizes', 'jeopardizing', 'jeopardy', 'kickback', 'kickbacks', 'knowingly', 'lack',
'lacked', 'lacking', 'lacks', 'lag', 'lagged', 'lagging', 'lags', 'lapse', 'lapsed', 'lapses',
'lapsing', 'late', 'laundering', 'layoff', 'layoffs', 'lie', 'lied', 'lies', 'limit',
'limitation', 'limitations', 'limited', 'limiting', 'limits', 'lingering', 'liquidate',
'liquidated', 'liquidates', 'liquidating', 'liquidation', 'liquidations', 'liquidator',
'liquidators', 'litigant', 'litigants', 'litigate', 'litigated', 'litigates', 'litigating',
'litigation', 'litigations', 'loopholes', 'lose', 'loses', 'losing', 'loss', 'losses',
'lost', 'lying', 'malfeasance', 'malfunction', 'malfunctioned', 'malfunctioning', 'malfunctions',
'malice', 'malicious', 'maliciously', 'malpractice', 'manipulate', 'manipulated', 'manipulates',
'manipulating', 'manipulation', 'manipulations', 'manipulative', 'markdown', 'markdowns',
'misapplication', 'misapplications', 'misapplied', 'misapplies', 'misapply', 'misapplying',
'misappropriate', 'misappropriated', 'misappropriates', 'misappropriating', 'misappropriation',
'misbranding', 'miscalculate', 'miscalculated', 'miscalculates', 'miscalculating', 'miscalculation',
'miscalculations', 'mischarged', 'mischaracterization', 'mischaracterize', 'mischaracterized',
'mischaracterizes', 'mischaracterizing', 'misconduct', 'misdated', 'misdemeanor', 'misdemeanors',
'misdirect', 'misdirected', 'misdirecting', 'misdirection', 'misdirects', 'mishandled',
'misinform', 'misinformation', 'misinformed', 'misinforming', 'misinforms', 'misinterpret',
'misinterpretation', 'misinterpretations', 'misinterpreted', 'misinterpreting', 'misinterprets',
'misjudge', 'misjudged', 'misjudges', 'misjudging', 'misjudgment', 'misjudgments', 'mislabel',
'mislabeled', 'mislabeling', 'mislabelled', 'mislabelling', 'mislabels', 'mislead', 'misleading',
'misleadingly', 'misleads', 'misled', 'mismanage', 'mismanaged', 'mismanagement', 'mismanages',
'mismanaging', 'mismatch', 'mismatched', 'mismatches', 'mismatching', 'misplaced', 'misprice',
'mispriced', 'mispricing', 'mispricings', 'misrepresent', 'misrepresentation', 'misrepresentations',
'misrepresented', 'misrepresenting', 'misrepresents', 'miss', 'missed', 'misses', 'misstate',
'misstated', 'misstatement', 'misstatements', 'misstates', 'misstating', 'misstep', 'missteps',
'mistake', 'mistaken', 'mistakenly', 'mistakes', 'mistaking', 'mistrial', 'mistrials', 'misunderstanding',
'misunderstandings', 'misunderstood', 'misuse', 'misused', 'misuses', 'misusing', 'monopolistic',
'monopolization', 'monopolize', 'monopolized', 'monopolizes', 'monopolizing', 'monopoly', 'moratorium',
'moratoriums', 'mothballed', 'mothballing', 'negative', 'negatively', 'negatives', 'negativity',
'neglect', 'neglected', 'neglectful', 'neglecting', 'neglects', 'negligence', 'negligences',
'negligent', 'negligently', 'nonattainment', 'noncash', 'noncompliance', 'noncompliances',
'noncomplying', 'nonconforming', 'nonconformities', 'nonconformity', 'nondisclosure', 'nonfunctional',
'nonpayment', 'nonpayments', 'nonperformance', 'nonperforming', 'nonproducing', 'nonproductive',
'nonrecoverable', 'nonrenewal', 'nonrenewals', 'nuisance', 'nuisances', 'nullification', 'nullifications',
'nullified', 'nullifies', 'nullify', 'nullifying', 'objected', 'objecting', 'objection', 'objectionable',
'objectionably', 'objections', 'objects', 'obscene', 'obscenity', 'obsolescence', 'obsolescent',
'obsolete', 'obstacle', 'obstacles', 'obstruct', 'obstructed', 'obstructing', 'obstruction',
'obstructions', 'obstructs', 'offence', 'offences', 'offend', 'offended', 'offender', 'offenders',
'offending', 'offends', 'offense', 'offenses', 'omission', 'omissions', 'omit', 'omits', 'omitted',
'omitting', 'opportunistic', 'opportunistically', 'oppose', 'opposed', 'opposes', 'opposing',
'opposition', 'outage', 'outages', 'outdated', 'outmoded', 'overage', 'overages', 'overbuild',
'overbuilding', 'overbuilds', 'overbuilt', 'overburden', 'overburdened', 'overburdening',
'overburdens', 'overcapacity', 'overcharge', 'overcharged', 'overcharges', 'overcharging',
'overcome', 'overcomes', 'overcoming', 'overdue', 'overestimate', 'overestimated', 'overestimates',
'overestimating', 'overestimation', 'overestimations', 'overload', 'overloaded', 'overloading',
'overloads', 'overlook', 'overlooked', 'overlooking', 'overlooks', 'overpaid', 'overpayment',
'overpayments', 'overpopulated', 'overpopulation', 'overproduce', 'overproduced', 'overproduces',
'overproducing', 'overproduction', 'overrun', 'overrunning', 'overruns', 'overshadow',
'overshadowed', 'overshadowing', 'overshadows', 'overstate', 'overstated', 'overstatement',
'overstatements', 'overstates', 'overstating', 'oversupplied', 'oversupplies', 'oversupply',
'oversupplying', 'overtly', 'overturn', 'overturned', 'overturning', 'overturns', 'overvalue',
'overvalued', 'overvaluing', 'panic', 'panics', 'penal', 'penalties', 'penalty', 'peril',
'perils', 'perjury', 'perpetrate', 'perpetrated', 'perpetrates', 'perpetrating', 'perpetration',
'perpetrator', 'perpetrators', 'petty', 'picket', 'picketed', 'picketing', 'pickets', 'plaintiff',
'plaintiffs', 'plea', 'plead', 'pleaded', 'pleading', 'pleadings', 'pleads', 'pleas', 'pled',
'poor', 'poorly', 'postpone', 'postponed', 'postponement', 'postponements', 'postpones', 'postponing',
'precipitate', 'precipitated', 'precipitates', 'precipitating', 'preclude', 'precluded', 'precludes',
'precluding', 'predatory', 'prejudice', 'prejudiced', 'prejudices', 'prejudicial', 'prejudicing',
'premature', 'prematurely', 'pressing', 'pretrial', 'prevent', 'preventable', 'prevented', 'preventing',
'prevents', 'problem', 'problematic', 'problematical', 'problems', 'prolong', 'prolongation',
'prolonged', 'prolonging', 'prolongs', 'prone', 'prosecute', 'prosecuted', 'prosecutes', 'prosecuting',
'prosecution', 'prosecutions', 'prosecutor', 'prosecutors', 'protest', 'protested', 'protester',
'protesters', 'protesting', 'protestor', 'protestors', 'protests', 'protracted', 'provoke', 'provoked',
'provokes', 'provoking', 'punishable', 'punitive', 'question', 'questionable', 'questionably',
'questioned', 'questioning', 'questions', 'quit', 'quits', 'quitting', 'racketeer', 'racketeering',
'rationalization', 'rationalizations', 'rationalize', 'rationalized', 'rationalizes', 'rationalizing',
'reassessment', 'reassessments', 'recall', 'recalled', 'recalling', 'recalls', 'recession',
'recessionary', 'recessions', 'reckless', 'recklessly', 'recklessness', 'redact', 'redacted',
'redacting', 'redaction', 'redactions', 'redacts', 'refusal', 'refusals', 'refuse', 'refused',
'refuses', 'refusing', 'reject', 'rejected', 'rejecting', 'rejection', 'rejections', 'rejects',
'reluctance', 'reluctant', 'reluctantly', 'renegotiate', 'renegotiated', 'renegotiates',
'renegotiating', 'renegotiation', 'renegotiations', 'renounce', 'renounced', 'renouncement',
'renouncements', 'renounces', 'renouncing', 'reorganization', 'reorganizations', 'repossess',
'repossessed', 'repossesses', 'repossessing', 'repossession', 'repossessions', 'repudiate',
'repudiated', 'repudiates', 'repudiating', 'repudiation', 'repudiations', 'resign', 'resignation',
'resignations', 'resigned', 'resigning', 'resigns', 'restate', 'restated', 'restatement',
'restatements', 'restates', 'restating', 'restructure', 'restructured', 'restructures',
'restructuring', 'restructurings', 'retaliate', 'retaliated', 'retaliates', 'retaliating',
'retaliation', 'retaliations', 'retaliatory', 'retract', 'retracted', 'retracting', 'retraction',
'retractions', 'retracts', 'retribution', 'retributions', 'revocation', 'revocations', 'revoke',
'revoked', 'revokes', 'revoking', 'ridicule', 'ridiculed', 'ridicules', 'ridiculing', 'ridiculous',
'ridiculously', 'riskier', 'riskiest', 'risky', 'rumor', 'rumors', 'sabotage', 'sabotaged',
'sabotages', 'sabotaging', 'sacrifice', 'sacrificed', 'sacrifices', 'sacrificing', 'sanction',
'sanctioned', 'sanctioning', 'sanctions', 'scandal', 'scandals', 'scrutinize', 'scrutinized',
'scrutinizes', 'scrutinizing', 'scrutiny', 'seize', 'seized', 'seizes', 'seizing', 'seizure',
'seizures', 'serious', 'seriously', 'seriousness', 'setback', 'setbacks', 'sever', 'severe',
'severed', 'severely', 'severities', 'severity', 'severing', 'severs', 'sharply', 'shocked',
'shortage', 'shortages', 'shortfall', 'shortfalls', 'shrinkage', 'shrinkages', 'shutdown',
'shutdowns', 'sideline', 'sidelined', 'sidelines', 'sidelining', 'significantly', 'slow',
'slowdown', 'slowdowns', 'slowed', 'slowing', 'slowness', 'slows', 'slump', 'slumped', 'slumping',
'slumps', 'smuggle', 'smuggled', 'smuggler', 'smugglers', 'smuggling', 'smuggles', 'staggering',
'stagnant', 'stagnate', 'stagnated', 'stagnates', 'stagnating', 'stagnation', 'standstill',
'stolen', 'stoppage', 'stoppages', 'stopped', 'stopping', 'stops', 'strain', 'strained',
'straining', 'strains', 'stress', 'stressed', 'stresses', 'stressful', 'stressing', 'stringent',
'subjected', 'subjecting', 'subjection', 'subpoena', 'subpoenaed', 'subpoenaing', 'subpoenas',
'substandard', 'sue', 'sued', 'sues', 'suffer', 'suffered', 'suffering', 'suffers', 'suing',
'summoned', 'summoning', 'summons', 'summonses', 'suspend', 'suspended', 'suspending', 'suspends',
'suspension', 'suspensions', 'suspicion', 'suspicions', 'suspicious', 'suspiciously', 'taint',
'tainted', 'tainting', 'taints', 'tarnish', 'tarnished', 'tarnishes', 'tarnishing', 'terminated',
'terminating', 'termination', 'terminations', 'testify', 'testifying', 'threat', 'threaten',
'threatened', 'threatening', 'threatens', 'threats', 'tightening', 'tolerate', 'tolerated',
'tolerates', 'tolerating', 'toleration', 'tort', 'tortuous', 'torts', 'tortuous', 'tragic',
'tragically', 'tragedies', 'tragedy', 'transgress', 'transgressed', 'transgresses', 'transgressing',
'transgression', 'transgressions', 'transgressor', 'transgressors', 'trouble', 'troubled', 'troubles',
'troublesome', 'troubling', 'unable', 'unacceptable', 'unacceptably', 'unaccounted', 'unannounced',
'unanticipated', 'unapprovable', 'unapproved', 'unauthorized', 'unavailability', 'unavailable',
'unavoidable', 'unavoidably', 'unaware', 'uncollectable', 'uncollected', 'uncollectibility',
'uncollectible', 'uncollectibles', 'uncomfortable', 'uncompetitive', 'uncompleted', 'unconscionable',
'unconscionably', 'uncontrollable', 'uncontrollably', 'uncontrolled', 'uncorrected', 'uncovered',
'undercapitalized', 'undercut', 'undercuts', 'undercutting', 'underestimate', 'underestimated',
'underestimates', 'underestimating', 'underestimation', 'underestimations', 'underfunded',
'underinsured', 'undermine', 'undermined', 'undermines', 'undermining', 'underpaid', 'underpayment',
'underpayments', 'underpays', 'underperform', 'underperformance', 'underperformed', 'underperforming',
'underperforms', 'underproduce', 'underproduced', 'underproduces', 'underproducing', 'underproduction',
'understate', 'understated', 'understatement', 'understatements', 'understates', 'understating',
'undesirable', 'undesired', 'undetected', 'undetermined', 'undisclosed', 'undocumented', 'undue',
'unduly', 'uneconomic', 'uneconomical', 'unemployed', 'unemployment', 'unethical', 'unethically',
'unexcused', 'unexpected', 'unexpectedly', 'unfair', 'unfairly', 'unfavorable', 'unfavorably',
'unfeasible', 'unfit', 'unfitness', 'unforeseeable', 'unforeseen', 'unforseen', 'unfortunate',
'unfortunately', 'unfounded', 'unfriendly', 'unfulfilled', 'unfunded', 'unfunded', 'unfunded',
'unhealthy', 'unidentified', 'uninsured', 'unintended', 'unintentional', 'unintentionally',
'unjust', 'unjustifiable', 'unjustifiably', 'unjustified', 'unjustly', 'unlawful', 'unlawfully',
'unlicensed', 'unliquidated', 'unmarketable', 'unmerchantable', 'unmeritorious', 'unnecessary',
'unneeded', 'unobtainable', 'unoccupied', 'unpaid', 'unpatentable', 'unplanned', 'unpopular',
'unpredictability', 'unpredictable', 'unpredictably', 'unpredicted', 'unpreventable', 'unproductive',
'unprofitability', 'unprofitable', 'unprofitably', 'unprotected', 'unproved', 'unproven', 'unqualified',
'unrealistic', 'unreasonable', 'unreasonableness', 'unreasonably', 'unrecoverable', 'unrecovered',
'unreimbursed', 'unreliability', 'unreliable', 'unremedied', 'unremitted', 'unreported', 'unresolved',
'unrest', 'unrests', 'unsafe', 'unsalable', 'unsaleable', 'unsatisfactory', 'unsatisfied',
'unscheduled', 'unscrupulous', 'unscrupulously', 'unseasoned', 'unseasonable', 'unseasonably',
'unsecured', 'unsellable', 'unserviceable', 'unsettle', 'unsettled', 'unsettles', 'unsettling',
'unsold', 'unsound', 'unstable', 'unsubstantiated', 'unsuccessful', 'unsuccessfully', 'unsuitability',
'unsuitable', 'unsuitably', 'unsuited', 'unsupportable', 'unsupported', 'unsure', 'unsustainable',
'untimely', 'untrue', 'untrustworthy', 'untruthful', 'untruthfully', 'untruthfulness', 'untruths',
'unused', 'unusual', 'unusually', 'unwarranted', 'unwelcome', 'unwilling', 'unwillingness', 'unworkable',
'upset', 'urgency', 'urgent', 'usurp', 'usurped', 'usurping', 'usurps', 'vandalism', 'verdict',
'verdicts', 'vetoed', 'victim', 'victims', 'violate', 'violated', 'violates', 'violating', 'violation',
'violations', 'violative', 'violator', 'violators', 'violence', 'violent', 'violently', 'vitiate',
'vitiated', 'vitiates', 'vitiating', 'vitiation', 'void', 'voidable', 'voided', 'voiding', 'voids',
'volatile', 'volatility', 'vulnerabilities', 'vulnerability', 'vulnerable', 'warn', 'warned', 'warning',
'warnings', 'warns', 'warp', 'warped', 'warping', 'warps', 'warrant', 'warranted', 'warrants', 'waste',
'wasted', 'wasteful', 'wastefully', 'wastefulness', 'wastes', 'wasting', 'weak', 'weaken', 'weakened',
'weakening', 'weakens', 'weaker', 'weakest', 'weakly', 'weakness', 'weaknesses', 'willfully', 'willfulness',
'worries', 'worry', 'worrisome', 'worse', 'worsen', 'worsened', 'worsening', 'worsens', 'worst', 'worthless',
'writedown', 'writedowns', 'writeoff', 'writeoffs', 'wrong', 'wrongdoing', 'wrongdoings', 'wrongful',
'wrongfully', 'wrongly', 'wrongs'
}
# Hedging/Uncertainty Language
HEDGING_WORDS = {
'may', 'might', 'could', 'possibly', 'perhaps', 'probably', 'likely', 'unlikely',
'appear', 'appears', 'appeared', 'appearing', 'seem', 'seems', 'seemed', 'seeming',
'suggest', 'suggests', 'suggested', 'suggesting', 'indicate', 'indicates', 'indicated',
'indicating', 'believe', 'believes', 'believed', 'believing', 'think', 'thinks', 'thought',
'thinking', 'assume', 'assumes', 'assumed', 'assuming', 'estimate', 'estimates', 'estimated',
'estimating', 'expect', 'expects', 'expected', 'expecting', 'anticipate', 'anticipates',
'anticipated', 'anticipating', 'potential', 'potentially', 'approximate', 'approximately',
'around', 'roughly', 'generally', 'typically', 'usually', 'normally', 'somewhat', 'relatively',
'fairly', 'quite', 'rather', 'partially', 'largely', 'mainly', 'mostly', 'primarily',
'essentially', 'basically', 'virtually', 'almost', 'nearly', 'about', 'approximately'
}
# Confident Language
CONFIDENT_WORDS = {
'will', 'shall', 'must', 'definitely', 'certainly', 'clearly', 'obviously', 'undoubtedly',
'unquestionably', 'absolutely', 'positively', 'assuredly', 'decidedly', '确实', 'indeed',
'truly', 'really', 'actually', 'exactly', 'precisely', 'specifically', 'explicitly',
'firmly', 'strongly', 'highly', 'extremely', 'significantly', 'substantially', 'considerably',
'markedly', 'notably', 'particularly', 'especially', 'uniquely', 'exclusively', 'solely',
'entirely', 'completely', 'totally', 'fully', 'wholly', 'thoroughly', 'utterly', 'perfectly',
'guarantee', 'guarantees', 'guaranteed', 'guaranteeing', 'commit', 'commits', 'committed',
'committing', 'ensure', 'ensures', 'ensured', 'ensuring', 'confirm', 'confirms', 'confirmed',
'confirming', 'demonstrate', 'demonstrates', 'demonstrated', 'demonstrating', 'prove', 'proves',
'proved', 'proven', 'proving', 'validate', 'validates', 'validated', 'validating'
}
def get_sec_transcripts(ticker: str, limit: int = 5) -> List[Dict]:
"""
Fetch recent 8-K filings with earnings call transcripts
"""
try:
# SEC EDGAR CIK lookup
cik_lookup_url = f"{EDGAR_BASE_URL}/cgi-bin/browse-edgar"
headers = {"User-Agent": USER_AGENT}
# Search for company
params = {
"action": "getcompany",
"CIK": ticker,
"type": "8-K",
"dateb": "",
"owner": "exclude",
"output": "json",
"count": limit
}
response = requests.get(cik_lookup_url, params=params, headers=headers, timeout=15)
if response.status_code != 200:
# Return simulated data instead of error
return _get_simulated_transcript(ticker)
# Parse filings
filings = []
data = response.json() if response.text else {}
# For demo purposes, return simulated data if no real data available
if not data or 'filings' not in data:
return _get_simulated_transcript(ticker)
return filings
except Exception as e:
# Return simulated data for demo
return _get_simulated_transcript(ticker)
def _get_simulated_transcript(ticker: str) -> List[Dict]:
"""
Generate simulated earnings call transcript for demonstration
"""
return [{
"ticker": ticker,
"date": datetime.now().strftime("%Y-%m-%d"),
"quarter": "Q4 2025",
"prepared_remarks": """
Thank you for joining us today. We're excited to share our strong Q4 results.
Revenue grew 25% year-over-year, exceeding our guidance. We're confident in our
strategic direction and the momentum we're building. Our innovation pipeline is
robust, and customer adoption has been outstanding. We're committed to delivering
shareholder value and will continue to invest in high-growth opportunities.
The market dynamics are favorable, and we're well-positioned to capitalize on
emerging trends. We believe our competitive advantages are sustainable and will
drive long-term success.
""",
"qa_section": """
Q: Can you provide more detail on the revenue guidance for next quarter?
A: Well, we're generally optimistic about the trajectory. The market conditions
seem favorable, and we expect to potentially see continued growth, though there
are various factors that could impact results. We'll likely provide more specific
guidance as we get closer to the quarter end.
Q: What about the margin compression we saw this quarter?
A: That's a good question. There were some one-time items that may have affected
the margins. We think we can probably improve that going forward, but it's hard
to say exactly. We're looking at various initiatives that might help.
Q: How is the competitive landscape evolving?
A: The competitive environment is always changing. We believe we're well-positioned,
but we're monitoring the situation closely. There could be some challenges ahead,
but we're confident in our ability to navigate them.
""",
"source": "simulated_demo"
}]
def analyze_sentiment(text: str) -> Dict:
"""
Analyze sentiment using Loughran-McDonald dictionary
"""
words = re.findall(r'\b\w+\b', text.lower())
word_count = len(words)
positive_count = sum(1 for w in words if w in LM_POSITIVE)
negative_count = sum(1 for w in words if w in LM_NEGATIVE)
# Net sentiment score
net_sentiment = (positive_count - negative_count) / word_count if word_count > 0 else 0
# Sentiment polarity (-1 to 1)
total_sentiment_words = positive_count + negative_count
polarity = ((positive_count - negative_count) / total_sentiment_words
if total_sentiment_words > 0 else 0)
return {
"positive_count": positive_count,
"negative_count": negative_count,
"net_sentiment": round(net_sentiment * 100, 2), # As percentage
"polarity": round(polarity, 3),
"positive_ratio": round(positive_count / word_count * 100, 2) if word_count > 0 else 0,
"negative_ratio": round(negative_count / word_count * 100, 2) if word_count > 0 else 0
}
def analyze_hedging_language(text: str) -> Dict:
"""
Detect hedging/uncertainty language
"""
words = re.findall(r'\b\w+\b', text.lower())
word_count = len(words)
hedging_count = sum(1 for w in words if w in HEDGING_WORDS)
confident_count = sum(1 for w in words if w in CONFIDENT_WORDS)
# Hedging ratio (higher = more uncertain)
hedging_ratio = hedging_count / word_count if word_count > 0 else 0
# Confidence score (confident words - hedging words)
confidence_score = (confident_count - hedging_count) / word_count if word_count > 0 else 0
return {
"hedging_count": hedging_count,
"confident_count": confident_count,
"hedging_ratio": round(hedging_ratio * 100, 2),
"confidence_score": round(confidence_score * 100, 2),
"interpretation": _interpret_hedging(hedging_ratio, confidence_score)
}
def _interpret_hedging(hedging_ratio: float, confidence_score: float) -> str:
"""
Interpret hedging metrics
"""
if hedging_ratio > 0.05:
return "High uncertainty - excessive hedging language"
elif hedging_ratio > 0.03:
return "Moderate hedging - some uncertainty"
elif confidence_score > 0.02:
return "High confidence - assertive language"
else:
return "Neutral tone - balanced language"
def analyze_confidence(text: str) -> Dict:
"""
Score management confidence based on linguistic markers
"""
sentiment = analyze_sentiment(text)
hedging = analyze_hedging_language(text)
# Composite confidence score (0-100)
# Higher sentiment + lower hedging + more confident words = higher score
confidence_score = (
(sentiment['polarity'] + 1) * 25 + # 0-50 from sentiment polarity
(1 - hedging['hedging_ratio']) * 30 + # 0-30 from low hedging
(hedging['confidence_score'] + 0.05) * 200 # 0-20 from confident words
)
confidence_score = max(0, min(100, confidence_score)) # Clamp to 0-100
return {
"overall_confidence": round(confidence_score, 1),
"sentiment_contribution": round((sentiment['polarity'] + 1) * 25, 1),
"hedging_penalty": round((hedging['hedging_ratio']) * 100, 1),
"confident_language_boost": round((hedging['confidence_score'] + 0.05) * 200, 1),
"interpretation": _interpret_confidence(confidence_score)
}
def _interpret_confidence(score: float) -> str:
"""
Interpret confidence score
"""
if score >= 80:
return "Very High - Strongly confident in outlook"
elif score >= 60:
return "High - Confident with minor caveats"
elif score >= 40:
return "Moderate - Mixed signals"
elif score >= 20:
return "Low - Significant uncertainty"
else:
return "Very Low - Highly uncertain or defensive"
def detect_question_dodging(qa_pairs: List[Tuple[str, str]]) -> Dict:
"""
Detect evasive answers to questions
Uses response length vs question complexity and hedging language
"""
dodge_scores = []
for question, answer in qa_pairs:
q_words = len(re.findall(r'\b\w+\b', question))
a_words = len(re.findall(r'\b\w+\b', answer))
# Expected answer length (should be at least 2x question length for detail)
expected_ratio = 2.0
actual_ratio = a_words / q_words if q_words > 0 else 0
# Analyze answer for hedging
answer_hedging = analyze_hedging_language(answer)
# Dodge score: short answers + high hedging = higher dodge probability
length_penalty = max(0, (expected_ratio - actual_ratio) / expected_ratio)
dodge_score = (length_penalty * 60) + (answer_hedging['hedging_ratio'])
dodge_scores.append({
"question": question[:100] + "..." if len(question) > 100 else question,
"answer_length": a_words,
"expected_length": int(q_words * expected_ratio),
"hedging_ratio": answer_hedging['hedging_ratio'],
"dodge_score": round(dodge_score, 1),
"likely_dodge": dodge_score > 40
})
# Overall dodge metrics
avg_dodge = statistics.mean([d['dodge_score'] for d in dodge_scores]) if dodge_scores else 0
dodge_count = sum(1 for d in dodge_scores if d['likely_dodge'])
return {
"total_questions": len(dodge_scores),
"likely_dodges": dodge_count,
"dodge_rate": round(dodge_count / len(dodge_scores) * 100, 1) if dodge_scores else 0,
"average_dodge_score": round(avg_dodge, 1),
"details": dodge_scores,
"interpretation": _interpret_dodge_rate(dodge_count / len(dodge_scores) * 100 if dodge_scores else 0)
}
def _interpret_dodge_rate(rate: float) -> str:
"""
Interpret question dodge rate
"""
if rate >= 50:
return "High evasiveness - Many questions answered indirectly"
elif rate >= 30:
return "Moderate evasiveness - Some non-specific answers"
elif rate >= 15:
return "Low evasiveness - Mostly direct responses"
else:
return "Transparent - Direct and detailed answers"
def analyze_tone_shift(prepared_text: str, qa_text: str) -> Dict:
"""
Detect tone changes between prepared remarks and Q&A
"""
prepared_sentiment = analyze_sentiment(prepared_text)
qa_sentiment = analyze_sentiment(qa_text)
prepared_hedging = analyze_hedging_language(prepared_text)
qa_hedging = analyze_hedging_language(qa_text)
# Calculate shifts
sentiment_shift = qa_sentiment['polarity'] - prepared_sentiment['polarity']
hedging_shift = qa_hedging['hedging_ratio'] - prepared_hedging['hedging_ratio']
confidence_shift = qa_hedging['confidence_score'] - prepared_hedging['confidence_score']
return {
"prepared_remarks": {
"sentiment": prepared_sentiment['polarity'],
"hedging": prepared_hedging['hedging_ratio'],
"confidence": prepared_hedging['confidence_score']
},
"qa_section": {
"sentiment": qa_sentiment['polarity'],
"hedging": qa_hedging['hedging_ratio'],
"confidence": qa_hedging['confidence_score']
},
"shifts": {
"sentiment_change": round(sentiment_shift, 3),
"hedging_increase": round(hedging_shift, 2),
"confidence_change": round(confidence_shift, 2)
},
"interpretation": _interpret_tone_shift(sentiment_shift, hedging_shift)
}
def _interpret_tone_shift(sentiment_shift: float, hedging_shift: float) -> str:
"""
Interpret tone shift from prepared to Q&A
"""
if sentiment_shift < -0.2 and hedging_shift > 2:
return "Significant defensive shift - More negative and uncertain under questioning"
elif sentiment_shift < -0.1 or hedging_shift > 1.5:
return "Moderate defensive shift - Less confident in Q&A"
elif abs(sentiment_shift) < 0.05 and abs(hedging_shift) < 0.5:
return "Consistent tone - Similar messaging throughout"
elif sentiment_shift > 0.1:
return "Positive shift - More optimistic in Q&A"
else:
return "Neutral shift - Minor variations"
def parse_qa_pairs(qa_text: str) -> List[Tuple[str, str]]:
"""
Parse Q&A section into question-answer pairs
"""
# Simple regex-based parser
qa_pairs = []
# Split on Q: or Question:
sections = re.split(r'(?:^|\n)\s*Q[:\.]?\s*', qa_text, flags=re.MULTILINE)
for section in sections[1:]: # Skip first empty split
# Split on A: or Answer:
parts = re.split(r'\n\s*A[:\.]?\s*', section, maxsplit=1)
if len(parts) == 2:
question = parts[0].strip()
answer = parts[1].strip()
qa_pairs.append((question, answer))
return qa_pairs
def cmd_earnings_tone(ticker: str) -> Dict:
"""
CLI: earnings-tone TICKER
Complete earnings call tone analysis
"""
transcripts = get_sec_transcripts(ticker, limit=1)
if not transcripts:
return {"error": f"No transcripts found for {ticker}"}
latest = transcripts[0]
# Analyze full transcript
full_text = latest.get('prepared_remarks', '') + ' ' + latest.get('qa_section', '')
sentiment = analyze_sentiment(full_text)
hedging = analyze_hedging_language(full_text)
# Analyze tone shift
tone_shift = analyze_tone_shift(
latest.get('prepared_remarks', ''),
latest.get('qa_section', '')
)
return {
"ticker": ticker,
"date": latest.get('date'),
"quarter": latest.get('quarter'),
"overall_sentiment": sentiment,
"hedging_analysis": hedging,
"tone_shift": tone_shift,
"source": latest.get('source', 'sec_edgar')
}
def cmd_confidence_score(ticker: str) -> Dict:
"""
CLI: confidence-score TICKER
Management confidence scoring
"""
transcripts = get_sec_transcripts(ticker, limit=1)
if not transcripts:
return {"error": f"No transcripts found for {ticker}"}
latest = transcripts[0]
full_text = latest.get('prepared_remarks', '') + ' ' + latest.get('qa_section', '')
confidence = analyze_confidence(full_text)
sentiment = analyze_sentiment(full_text)
hedging = analyze_hedging_language(full_text)
return {
"ticker": ticker,
"date": latest.get('date'),
"quarter": latest.get('quarter'),
"confidence_analysis": confidence,
"supporting_metrics": {
"sentiment": sentiment,
"hedging": hedging
},
"source": latest.get('source', 'sec_edgar')
}
def cmd_dodge_detect(ticker: str) -> Dict:
"""
CLI: dodge-detect TICKER
Question dodging detection in Q&A
"""
transcripts = get_sec_transcripts(ticker, limit=1)
if not transcripts:
return {"error": f"No transcripts found for {ticker}"}
latest = transcripts[0]
qa_text = latest.get('qa_section', '')
# Parse Q&A pairs
qa_pairs = parse_qa_pairs(qa_text)
if not qa_pairs:
return {
"error": "Could not parse Q&A section",
"ticker": ticker,
"note": "Q&A format may not be standard"
}
dodge_analysis = detect_question_dodging(qa_pairs)
return {
"ticker": ticker,
"date": latest.get('date'),
"quarter": latest.get('quarter'),
"dodge_analysis": dodge_analysis,
"source": latest.get('source', 'sec_edgar')
}
def main():
"""CLI entry point"""
if len(sys.argv) < 2:
print("Usage:")
print(" python earnings_nlp.py earnings-tone TICKER")
print(" python earnings_nlp.py confidence-score TICKER")
print(" python earnings_nlp.py dodge-detect TICKER")
return 1
command = sys.argv[1]
if command == "earnings-tone":
if len(sys.argv) < 3:
print("Error: Missing ticker", file=sys.stderr)
return 1
ticker = sys.argv[2].upper()
result = cmd_earnings_tone(ticker)
print(json.dumps(result, indent=2))
elif command == "confidence-score":
if len(sys.argv) < 3:
print("Error: Missing ticker", file=sys.stderr)
return 1
ticker = sys.argv[2].upper()
result = cmd_confidence_score(ticker)
print(json.dumps(result, indent=2))
elif command == "dodge-detect":
if len(sys.argv) < 3:
print("Error: Missing ticker", file=sys.stderr)
return 1
ticker = sys.argv[2].upper()
result = cmd_dodge_detect(ticker)
print(json.dumps(result, indent=2))
else:
print(f"Error: Unknown command '{command}'", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())