Skip to content

Commit 75dbc6e

Browse files
committed
Iterating
1 parent 40d010d commit 75dbc6e

File tree

11 files changed

+391
-89
lines changed

11 files changed

+391
-89
lines changed

php-symfony-neuron/config/services.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ services:
4646
App\Service\ApiClient\StockClientsFactory:
4747
arguments:
4848
$useMockData: '%env(default::bool:USE_MOCK_DATA)%'
49+
50+
# SEC API Client Factory Configuration
51+
App\Service\ApiClient\SecApiClientFactory:
52+
arguments:
53+
$useMockData: '%env(default::bool:USE_MOCK_DATA)%'
4954

5055
# LLM Client Factory Configuration
5156
App\Service\LlmClientFactory:

php-symfony-neuron/src/Controller/CompanyController.php

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,43 +43,55 @@ public function index(CompanyRepository $companyRepository): Response
4343
#[Route('/search', name: 'company_search', methods: ['GET', 'POST'])]
4444
public function search(Request $request, CompanyRepository $companyRepository, StockDataService $stockDataService): Response
4545
{
46-
$form = $this->createForm(CompanySearchType::class);
47-
$form->handleRequest($request);
48-
4946
$dbResults = [];
5047
$apiResults = [];
51-
$searchTerm = '';
52-
53-
if ($form->isSubmitted() && $form->isValid()) {
54-
$searchTerm = $form->get('searchTerm')->getData();
48+
$searchTerm = $request->query->get('searchTerm');
5549

50+
if ($searchTerm) {
5651
// First search in local database
5752
$dbResults = $companyRepository->findBySearchCriteria($searchTerm);
5853

59-
// If no local results or explicitly searching by ticker
60-
if (count($dbResults) === 0 || preg_match('/^[A-Za-z]{1,5}$/', $searchTerm)) {
61-
try {
62-
// Search in external APIs
63-
$apiResults = $stockDataService->searchCompanies($searchTerm);
54+
// If we have any database results, collect their ticker symbols
55+
$existingSymbols = [];
56+
foreach ($dbResults as $company) {
57+
$existingSymbols[] = $company->getTickerSymbol();
58+
}
59+
60+
// Get external API results
61+
$apiResults = [];
62+
try {
63+
// Search in external APIs
64+
$allApiResults = $stockDataService->searchCompanies($searchTerm);
6465

65-
// Filter out companies that already exist in DB results
66-
$existingSymbols = array_map(function($company) {
67-
return $company->getTickerSymbol();
68-
}, $dbResults);
66+
// Filter out companies that already exist in DB results by ticker symbol
67+
$filteredApiResults = array_filter($allApiResults, function($result) use ($existingSymbols) {
68+
return !in_array($result['symbol'], $existingSymbols);
69+
});
6970

70-
$apiResults = array_filter($apiResults, function($result) use ($existingSymbols) {
71-
return !in_array($result['symbol'], $existingSymbols);
72-
});
71+
// Group results by provider
72+
$resultsByProvider = [];
73+
foreach ($filteredApiResults as $result) {
74+
$provider = $result['provider'] ?? 'Unknown';
75+
if (!isset($resultsByProvider[$provider])) {
76+
$resultsByProvider[$provider] = [];
77+
}
78+
$resultsByProvider[$provider][] = $result;
79+
}
7380

74-
} catch (\Exception $e) {
75-
// Log error but continue with DB results
76-
$this->addFlash('warning', 'Could not fetch additional results from external sources');
81+
// Take only the first result from each provider
82+
foreach ($resultsByProvider as $provider => $results) {
83+
if (!empty($results)) {
84+
$apiResults[] = $results[0]; // Add only the first result from each provider
85+
}
7786
}
87+
88+
} catch (\Exception $e) {
89+
// Log error but continue with DB results
90+
$this->addFlash('warning', 'Could not fetch additional results from external sources');
7891
}
7992
}
8093

8194
return $this->render('company/search.html.twig', [
82-
'form' => $form->createView(),
8395
'dbResults' => $dbResults,
8496
'apiResults' => $apiResults,
8597
'searchTerm' => $searchTerm,

php-symfony-neuron/src/Entity/SecFiling.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ class SecFiling
5151
#[ORM\Column(length: 255, nullable: true)]
5252
private ?string $textUrl = null;
5353

54+
#[ORM\Column(length: 255, nullable: true)]
55+
private ?string $pdfUrl = null;
56+
57+
#[ORM\Column(length: 255, nullable: true)]
58+
private ?string $xbrlUrl = null;
59+
60+
#[ORM\Column(length: 255, nullable: true)]
61+
private ?string $ixbrlUrl = null;
62+
63+
#[ORM\Column(length: 255, nullable: true)]
64+
private ?string $filer = null;
65+
5466
#[ORM\Column(type: 'text', nullable: true)]
5567
private ?string $content = null;
5668

@@ -329,4 +341,73 @@ public function setFiscalYear(?string $fiscalYear): static
329341

330342
return $this;
331343
}
344+
345+
public function getPdfUrl(): ?string
346+
{
347+
return $this->pdfUrl;
348+
}
349+
350+
public function setPdfUrl(?string $pdfUrl): static
351+
{
352+
$this->pdfUrl = $pdfUrl;
353+
354+
return $this;
355+
}
356+
357+
public function getXbrlUrl(): ?string
358+
{
359+
return $this->xbrlUrl;
360+
}
361+
362+
public function setXbrlUrl(?string $xbrlUrl): static
363+
{
364+
$this->xbrlUrl = $xbrlUrl;
365+
366+
return $this;
367+
}
368+
369+
public function getIxbrlUrl(): ?string
370+
{
371+
return $this->ixbrlUrl;
372+
}
373+
374+
public function setIxbrlUrl(?string $ixbrlUrl): static
375+
{
376+
$this->ixbrlUrl = $ixbrlUrl;
377+
378+
return $this;
379+
}
380+
381+
public function getFiler(): ?string
382+
{
383+
return $this->filer;
384+
}
385+
386+
public function setFiler(?string $filer): static
387+
{
388+
$this->filer = $filer;
389+
390+
return $this;
391+
}
392+
393+
/**
394+
* Get a formatted title for display in listings
395+
*
396+
* @return string A formatted title for the filing
397+
*/
398+
public function getFormattedTitle(): string
399+
{
400+
// Concatenate fiscal year and form type if available
401+
if ($this->company && $this->fiscalYear) {
402+
return "FY{$this->fiscalYear} {$this->formType}";
403+
}
404+
405+
// Fallback to just the form type and date if no fiscal year
406+
if ($this->filingDate) {
407+
return "{$this->formType} " . $this->filingDate->format('Y');
408+
}
409+
410+
// Simple fallback
411+
return $this->formType ?? 'SEC Filing';
412+
}
332413
}

php-symfony-neuron/src/Repository/CompanyRepository.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,16 @@ public function remove(Company $entity, bool $flush = false): void
4242
/**
4343
* Find companies by name, ticker symbol, industry, or sector
4444
*/
45-
public function findBySearchCriteria(string $searchTerm): array
45+
public function findBySearchCriteria(string $searchTerm, int $limit = 25): array
4646
{
4747
$queryBuilder = $this->createQueryBuilder('c')
4848
->where('c.name LIKE :searchTerm')
4949
->orWhere('c.tickerSymbol LIKE :searchTerm')
5050
->orWhere('c.industry LIKE :searchTerm')
5151
->orWhere('c.sector LIKE :searchTerm')
5252
->setParameter('searchTerm', '%' . $searchTerm . '%')
53-
->orderBy('c.name', 'ASC');
53+
->orderBy('c.name', 'ASC')
54+
->setMaxResults($limit);
5455

5556
return $queryBuilder->getQuery()->getResult();
5657
}

php-symfony-neuron/src/Service/ApiClient/KaleidoscopeApiClient.php

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ protected function initialize(): void
4444
protected function getAuthParams(): array
4545
{
4646
return [
47-
'key' => $this->apiKey
47+
'key' => $this->apiKey,
48+
'content' => 'sec' // Required parameter
4849
];
4950
}
5051

@@ -119,22 +120,52 @@ private function normalizeFilingData(array $filing): array
119120
'companyName' => $filing['Company Name'] ?? '',
120121
'formType' => $filing['Form'] ?? '',
121122
'formDescription' => $filing['Form_Desc'] ?? '',
123+
'formGroup' => $filing['Form_Group'] ?? '', // Added form group
124+
'filer' => $filing['Filer'] ?? '', // Added filer
122125
'filingDate' => $date,
123126
'reportDate' => $date, // Use same date as filing date if report date not provided
124127
'accessionNumber' => $filing['acc'] ?? '',
125128
'fileNumber' => '', // Not provided by Kaleidoscope
126129
'htmlUrl' => $filing['html'] ?? '',
130+
'ixbrlUrl' => $filing['ixbrl'] ?? '', // Added IXBRL URL
127131
'pdfUrl' => $filing['pdf'] ?? '',
128132
'textUrl' => '', // Kaleidoscope doesn't provide a direct text URL
129133
'wordUrl' => $filing['word'] ?? '',
130134
'xbrlUrl' => $filing['xbrl'] ?? '',
131135
'xlsUrl' => $filing['xls'] ?? '',
132136
'ticker' => $filing['ticker'] ?? '',
133137
'description' => $filing['Form_Desc'] ?? '',
134-
'fiscalYear' => $date ? date('Y', strtotime($date)) : ''
138+
'fiscalYear' => $date ? date('Y', strtotime($date)) : '',
139+
// Add a documentUrl field using the HTML URL as fallback
140+
'documentUrl' => $filing['html'] ?? $filing['pdf'] ?? ''
135141
];
136142
}
137143

144+
/**
145+
* Get paginated SEC filings with complete metadata
146+
*
147+
* @param string $ticker Stock ticker symbol
148+
* @param int $limit Maximum number of results to return
149+
* @param int $offset Pagination offset
150+
* @return array Array with 'data', 'total', 'start', and 'end' keys
151+
*/
152+
public function searchFilingsWithPagination(string $ticker, int $limit = 10, int $offset = 0): array
153+
{
154+
$params = [
155+
'limit' => $limit,
156+
'start' => $offset
157+
];
158+
159+
$result = $this->searchFilings($ticker, $params);
160+
161+
return [
162+
'data' => array_map([$this, 'normalizeFilingData'], $result['data'] ?? []),
163+
'total' => $result['total'] ?? 0,
164+
'start' => $result['start'] ?? 0,
165+
'end' => $result['end'] ?? 0
166+
];
167+
}
168+
138169
/**
139170
* Get 10-K reports for a company
140171
*
@@ -144,8 +175,26 @@ private function normalizeFilingData(array $filing): array
144175
*/
145176
public function get10KReports(string $ticker, int $limit = 5): array
146177
{
147-
$filings = $this->searchFilings($ticker);
148-
return $this->filterFilingsByType($filings, '10-K', $limit);
178+
// Using the API's built-in form filtering instead of client-side filtering
179+
$params = [
180+
'form' => '10-K',
181+
'limit' => $limit
182+
];
183+
184+
try {
185+
$response = $this->searchFilings($ticker, $params);
186+
if (empty($response['data'])) {
187+
return [];
188+
}
189+
190+
// Still normalize the data to our expected format
191+
return array_map([$this, 'normalizeFilingData'], array_slice($response['data'], 0, $limit));
192+
} catch (\Exception $e) {
193+
if ($this->logger) {
194+
$this->logger->error("Failed to fetch 10-K reports: " . $e->getMessage());
195+
}
196+
return [];
197+
}
149198
}
150199

151200
/**

php-symfony-neuron/src/Service/SecFilingService.php

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,57 @@ public function import10KReports(Company $company, bool $downloadContent = false
7777
$filing = new SecFiling();
7878
$filing->setCompany($company);
7979
$filing->setFormType($report['formType']);
80-
try { $filing->setFilingDate(new \DateTime($report['filingDate'])); } catch (\Exception $e) {}
80+
// Set the type field (required by the entity)
81+
$filing->setType($report['formType'] ?? 'UNKNOWN');
82+
try {
83+
$filing->setFilingDate(new \DateTime($report['filingDate']));
84+
} catch (\Exception $e) {
85+
$this->logger->warning('Error setting filing date: ' . $e->getMessage());
86+
$filing->setFilingDate(new \DateTime()); // Set current date as fallback
87+
}
8188
if (isset($report['reportDate']) && $report['reportDate']) {
82-
try { $filing->setReportDate(new \DateTime($report['reportDate'])); } catch (\Exception $e) {}
89+
try {
90+
$filing->setReportDate(new \DateTime($report['reportDate']));
91+
} catch (\Exception $e) {
92+
$this->logger->warning('Error setting report date: ' . $e->getMessage());
93+
// Fallback to filing date if available
94+
$filing->setReportDate($filing->getFilingDate());
95+
}
96+
}
97+
// Check if accessionNumber is set, otherwise generate a fallback
98+
if (!empty($report['accessionNumber'])) {
99+
$filing->setAccessionNumber($report['accessionNumber']);
100+
} else {
101+
// Generate a fallback accession number using company ticker symbol and current timestamp
102+
$ticker = str_replace('^', '', $company->getTickerSymbol() ?? 'UNKNOWN');
103+
$fallbackAcc = $ticker . '-' . date('Ymd') . '-' . uniqid();
104+
$filing->setAccessionNumber($fallbackAcc);
105+
$this->logger->warning('Generated fallback accession number: ' . $fallbackAcc);
83106
}
84-
$filing->setAccessionNumber($report['accessionNumber']);
85107
$filing->setFileNumber($report['fileNumber'] ?? null);
86-
$filing->setDescription($report['description'] ?? null);
87-
$filing->setDocumentUrl($report['documentUrl']);
108+
$filing->setDescription($report['description'] ?? $report['formDescription'] ?? $report['formType'] . ' filing for ' . $company->getName());
109+
110+
// Use different URL fields with fallbacks
111+
$filing->setDocumentUrl($report['documentUrl'] ?? $report['htmlUrl'] ?? $report['pdfUrl'] ?? '');
88112
$filing->setHtmlUrl($report['htmlUrl'] ?? null);
113+
$filing->setUrl($report['documentUrl'] ?? $report['htmlUrl'] ?? $report['pdfUrl'] ?? '');
89114
$filing->setTextUrl($report['textUrl'] ?? null);
115+
116+
// Set additional URL fields
117+
$filing->setPdfUrl($report['pdfUrl'] ?? null);
118+
$filing->setXbrlUrl($report['xbrlUrl'] ?? null);
119+
$filing->setIxbrlUrl($report['ixbrlUrl'] ?? null);
120+
121+
// Save additional data from enhanced API response
122+
if (isset($report['filer'])) {
123+
// Store filer info in dedicated field
124+
$filing->setFiler($report['filer']);
125+
126+
// Also include it in the description if not the company name
127+
if ($report['filer'] !== $company->getName()) {
128+
$filing->setDescription($filing->getDescription() . ' (Filed by: ' . $report['filer'] . ')');
129+
}
130+
}
90131
$filingDate = $filing->getFilingDate();
91132
$fiscalYear = $filingDate ? $filingDate->format('Y') : date('Y');
92133
if ($filingDate && $filingDate->format('m') <= 3) {

php-symfony-neuron/src/Service/StockDataService.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,35 @@ public function __construct(
5252
/**
5353
* Search for companies by name or ticker symbol
5454
*/
55-
public function searchCompanies(string $term): array
55+
public function searchCompanies(string $term, int $limit = 25): array
5656
{
5757
$cacheKey = 'company_search_' . md5($term);
58-
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($term) {
58+
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($term, $limit) {
5959
$item->expiresAfter(3600); // Cache for 1 hour
6060
$this->logger->info('Cache miss for company search', ['term' => $term]);
6161
try {
6262
$results = $this->alphaVantageClient->searchCompanies($term);
63+
// Add provider information to each result
64+
foreach ($results as &$result) {
65+
$result['provider'] = 'Alpha Vantage API';
66+
}
6367
if (empty($results)) {
6468
$this->logger->info('No results from Alpha Vantage, trying Yahoo Finance');
6569
$results = $this->yahooFinanceClient->searchCompanies($term);
70+
// Add provider information to each result
71+
foreach ($results as &$result) {
72+
$result['provider'] = 'Yahoo Finance API';
73+
}
6674
}
6775
return $results;
6876
} catch (\Exception $e) {
6977
$this->logger->error('Error searching companies: ' . $e->getMessage());
7078
try {
7179
$results = $this->yahooFinanceClient->searchCompanies($term);
80+
// Add provider information to each result
81+
foreach ($results as &$result) {
82+
$result['provider'] = 'Yahoo Finance API';
83+
}
7284
return $results;
7385
} catch (\Exception $e2) {
7486
$this->logger->error('Error with fallback search: ' . $e2->getMessage());

0 commit comments

Comments
 (0)