feat:create custom data types for market data

This commit is contained in:
hackerESQ
2024-10-29 16:34:18 -05:00
parent 4f6e3c3711
commit 4e6dcd6ff4
11 changed files with 437 additions and 94 deletions
@@ -5,6 +5,10 @@ namespace App\Interfaces\MarketData;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Split;
use App\Interfaces\MarketData\Types\Dividend;
use App\Interfaces\MarketData\Types\Ohlc;
use Tschucki\Alphavantage\Facades\Alphavantage;
class AlphaVantageMarketData implements MarketDataInterface
@@ -15,11 +19,13 @@ class AlphaVantageMarketData implements MarketDataInterface
return $this->quote($symbol)->isNotEmpty();
}
public function quote(String $symbol): Collection
public function quote(String $symbol): Quote
{
$quote = Alphavantage::core()->quoteEndpoint($symbol);
$quote = Arr::get($quote, 'Global Quote', []);
if (empty($quote)) return new Quote();
$fundamental = cache()->remember(
'av-symbol-'.$symbol,
1440,
@@ -28,21 +34,21 @@ class AlphaVantageMarketData implements MarketDataInterface
}
);
return collect([
return new Quote([
'name' => Arr::get($fundamental, 'Name'),
'symbol' => Arr::get($fundamental, 'Symbol'),
'market_value' => (float) Arr::get($quote, '05. price'),
'fifty_two_week_high' => (float) Arr::get($fundamental, '52WeekHigh'),
'fifty_two_week_low' => (float) Arr::get($fundamental, '52WeekLow'),
'forward_pe' => (float) Arr::get($fundamental, 'ForwardPE'),
'trailing_pe' => (float) Arr::get($fundamental, 'TrailingPE'),
'market_cap' => (int) Arr::get($fundamental, 'MarketCapitalization'),
'book_value' => (float) Arr::get($fundamental, 'BookValue'),
'market_value' => Arr::get($quote, '05. price'),
'fifty_two_week_high' => Arr::get($fundamental, '52WeekHigh'),
'fifty_two_week_low' => Arr::get($fundamental, '52WeekLow'),
'forward_pe' => Arr::get($fundamental, 'ForwardPE'),
'trailing_pe' => Arr::get($fundamental, 'TrailingPE'),
'market_cap' => Arr::get($fundamental, 'MarketCapitalization'),
'book_value' => Arr::get($fundamental, 'BookValue'),
'last_dividend_date' => Arr::get($fundamental, 'DividendDate') != 'None'
? Arr::get($fundamental, 'DividendDate')
: null,
'dividend_yield' => Arr::get($fundamental, 'DividendYield') != 'None'
? (float) Arr::get($fundamental, 'DividendYield')
? Arr::get($fundamental, 'DividendYield')
: null
]);
}
@@ -59,12 +65,11 @@ class AlphaVantageMarketData implements MarketDataInterface
})
->map(function($dividend) use ($symbol) {
return [
return new Dividend([
'symbol' => $symbol,
'date' => Carbon::parse(Arr::get($dividend, 'ex_dividend_date'))
->format('Y-m-d H:i:s'),
'date' => Carbon::parse(Arr::get($dividend, 'ex_dividend_date')),
'dividend_amount' => Arr::get($dividend, 'amount'),
];
]);
});
}
@@ -80,12 +85,11 @@ class AlphaVantageMarketData implements MarketDataInterface
})
->map(function($split) use ($symbol) {
return [
return new Split([
'symbol' => $symbol,
'date' => Carbon::parse(Arr::get($split, 'effective_date'))
->format('Y-m-d H:i:s'),
'date' => Carbon::parse(Arr::get($split, 'effective_date')),
'split_amount' => Arr::get($split, 'split_factor'),
];
]);
});
}
@@ -105,11 +109,11 @@ class AlphaVantageMarketData implements MarketDataInterface
$date = Carbon::parse($date)->format('Y-m-d');
return [ $date => [
return [ $date => new Ohlc([
'symbol' => $symbol,
'date' => $date,
'close' => (float) Arr::get($history, '4. close')
]];
'close' => Arr::get($history, '4. close')
]) ];
});
}
}
+29 -25
View File
@@ -4,6 +4,10 @@ namespace App\Interfaces\MarketData;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Dividend;
use App\Interfaces\MarketData\Types\Ohlc;
use App\Interfaces\MarketData\Types\Split;
class FakeMarketData implements MarketDataInterface
{
@@ -13,21 +17,21 @@ class FakeMarketData implements MarketDataInterface
return true;
}
public function quote(String $symbol): Collection
public function quote(String $symbol): Quote
{
return collect([
return new Quote([
'name' => 'ACME Company Ltd',
'symbol' => $symbol,
'market_value' => (float) 230.19,
'fifty_two_week_high' => (float) 512.90,
'fifty_two_week_low' => (float) 341.20,
'forward_pe' => (float) 20.1,
'trailing_pe' => (float) 30.34,
'market_cap' => (int) 9800700600,
'book_value' => (float) 4.7,
'market_value' => 230.19,
'fifty_two_week_high' => 512.90,
'fifty_two_week_low' => 341.20,
'forward_pe' => 20.1,
'trailing_pe' => 30.34,
'market_cap' => 9800700600,
'book_value' => 4.7,
'last_dividend_date' => now()->subDays(45),
'dividend_yield' => (float) 0.033
'dividend_yield' => 0.033
]);
}
@@ -35,21 +39,21 @@ class FakeMarketData implements MarketDataInterface
{
return collect([
[
new Dividend([
'symbol' => $symbol,
'date' => now()->subMonths(3)->format('Y-m-d H:i:s'),
'date' => now()->subMonths(3),
'dividend_amount' => 2.11,
],
[
]),
new Dividend([
'symbol' => $symbol,
'date' => now()->subMonths(6)->format('Y-m-d H:i:s'),
'date' => now()->subMonths(6),
'dividend_amount' => 1.89,
],
[
]),
new Dividend([
'symbol' => $symbol,
'date' => now()->subMonths(9)->format('Y-m-d H:i:s'),
'date' => now()->subMonths(9),
'dividend_amount' => 0.95,
],
]),
]);
}
@@ -57,11 +61,11 @@ class FakeMarketData implements MarketDataInterface
{
return collect([
[
new Split([
'symbol' => $symbol,
'date' => now()->subMonths(36)->format('Y-m-d H:i:s'),
'date' => now()->subMonths(36),
'split_amount' => 10,
],
])
]);
}
@@ -73,11 +77,11 @@ class FakeMarketData implements MarketDataInterface
$date = now()->subDays($i)->format('Y-m-d');
$series[$date] = [
$series[$date] = new Ohlc([
'symbol' => $symbol,
'date' => $date,
'close' => (float) rand(150, 400),
];
'close' => rand(150, 400),
]);
}
return collect($series);
+25 -25
View File
@@ -5,6 +5,10 @@ namespace App\Interfaces\MarketData;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use App\Interfaces\MarketData\Types\Ohlc;
use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Split;
use App\Interfaces\MarketData\Types\Dividend;
class FinnhubMarketData implements MarketDataInterface
{
@@ -24,11 +28,11 @@ class FinnhubMarketData implements MarketDataInterface
return $this->quote($symbol)->isNotEmpty();
}
public function quote($symbol): Collection
public function quote(string $symbol): Quote
{
$quote = $this->client->quote($symbol);
if (empty($quote)) return new Quote();
$fundamental = cache()->remember(
'fh-symbol-'.$symbol,
@@ -38,20 +42,18 @@ class FinnhubMarketData implements MarketDataInterface
}
);
if (empty($fundamental)) return collect();
return collect([
return new Quote([
'name' => Arr::get($fundamental, 'metric.name'),
'symbol' => $symbol,
'market_value' => (float) Arr::get($quote, 'c'),
'fifty_two_week_high' => (float) Arr::get($fundamental, 'metric.52WeekHigh'),
'fifty_two_week_low' => (float) Arr::get($fundamental, 'metric.52WeekLow'),
'forward_pe' => (float) Arr::get($fundamental, 'metric.forwardPE'), // confirm
'trailing_pe' => (float) Arr::get($fundamental, 'metric.trailingPE'), // confirm
'market_cap' => (int) Arr::get($fundamental, 'metric.marketCapitalization'), // confirm
'book_value' => (float) Arr::get($fundamental, 'metric.bookValuePerShare'), // confirm
'market_value' => Arr::get($quote, 'c'),
'fifty_two_week_high' => Arr::get($fundamental, 'metric.52WeekHigh'),
'fifty_two_week_low' => Arr::get($fundamental, 'metric.52WeekLow'),
'forward_pe' => Arr::get($fundamental, 'metric.forwardPE'), // confirm
'trailing_pe' => Arr::get($fundamental, 'metric.trailingPE'), // confirm
'market_cap' => Arr::get($fundamental, 'metric.marketCapitalization'), // confirm
'book_value' => Arr::get($fundamental, 'metric.bookValuePerShare'), // confirm
'last_dividend_date' => Arr::get($fundamental, 'metric.lastDivDate'), // confirm
'dividend_yield' => (float) Arr::get($fundamental, 'metric.dividendYield'), // confirm
'dividend_yield' => Arr::get($fundamental, 'metric.dividendYield'), // confirm
]);
}
@@ -61,12 +63,11 @@ class FinnhubMarketData implements MarketDataInterface
return collect($dividends)->map(function($dividend) use ($symbol) {
return [
return new Dividend([
'symbol' => $symbol,
'date' => Carbon::parse(Arr::get($dividend, 'date'))
->format('Y-m-d H:i:s'),
'date' => Carbon::parse(Arr::get($dividend, 'date')),
'dividend_amount' => Arr::get($dividend, 'amount'),
];
]);
});
}
@@ -77,12 +78,11 @@ class FinnhubMarketData implements MarketDataInterface
return collect($splits)->map(function($split) use ($symbol) {
return [
return new Split([
'symbol' => $symbol,
'date' => Carbon::parse(Arr::get($split, 'date'))
->format('Y-m-d H:i:s'),
'date' => Carbon::parse(Arr::get($split, 'date')),
'split_amount' => Arr::get($split, 'toFactor') / Arr::get($split, 'fromFactor'),
];
]);
});
}
@@ -96,11 +96,11 @@ class FinnhubMarketData implements MarketDataInterface
return collect($timestamps)->mapWithKeys(function ($timestamp, $index) use ($symbol, $closes) {
$date = Carbon::createFromTimestamp($timestamp)->format('Y-m-d');
return [ $date => [
return [ $date => new Ohlc([
'symbol' => $symbol,
'date' => $date,
'close' => (float) $closes[$index],
]];
'close' => $closes[$index],
]) ];
});
}
}
@@ -3,6 +3,7 @@
namespace App\Interfaces\MarketData;
use Illuminate\Support\Collection;
use App\Interfaces\MarketData\Types\Quote;
interface MarketDataInterface
{
@@ -20,9 +21,9 @@ interface MarketDataInterface
*
* @param String $symbol
*
* @return Collection
* @return Quote
*/
public function quote(String $symbol): Collection;
public function quote(String $symbol): Quote;
/**
* Get dividend data
@@ -0,0 +1,43 @@
<?php
namespace App\Interfaces\MarketData\Types;
use DateTime;
use Illuminate\Support\Carbon;
use App\Interfaces\MarketData\Types\MarketDataType;
class Dividend extends MarketDataType
{
public function setSymbol(string $symbol): self
{
$this->items['symbol'] = $symbol;
return $this;
}
public function getSymbol(): string
{
return $this->items['symbol'] ?? '';
}
public function setDividendAmount($dividendAmount): self
{
$this->items['dividend_amount'] = (float) $dividendAmount;
return $this;
}
public function getDividendAmount(): float
{
return $this->items['dividend_amount'] ?? 0.0;
}
public function setDate(String|DateTime $date): self
{
$this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s');
return $this;
}
public function getDate(): ?DateTime
{
return $this->items['date'] ?? null;
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Interfaces\MarketData\Types;
use Illuminate\Support\Str;
use Illuminate\Support\Collection;
class MarketDataType extends Collection
{
/**
*
*/
public function __construct($items = [])
{
foreach($this->getArrayableItems($items) as $key => $value) {
$this->{$key} = $value;
}
}
public function toArray()
{
return $this->items;
}
public function __set($key, $value)
{
$this->{'set'.Str::studly($key)}($value);
}
public function __get($key)
{
return $this->items[$key] ?? null;
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace App\Interfaces\MarketData\Types;
use DateTime;
use Illuminate\Support\Carbon;
use App\Interfaces\MarketData\Types\MarketDataType;
class Ohlc extends MarketDataType
{
public function setSymbol(string $symbol): self
{
$this->items['symbol'] = $symbol;
return $this;
}
public function getSymbol(): string
{
return $this->items['symbol'] ?? '';
}
public function setOpen($open): self
{
$this->items['open'] = (float) $open;
return $this;
}
public function getOpen(): float
{
return $this->items['open'] ?? 0.0;
}
public function setHigh($high): self
{
$this->items['high'] = (float) $high;
return $this;
}
public function getHigh(): float
{
return $this->items['high'] ?? 0.0;
}
public function setLow($low): self
{
$this->items['low'] = (float) $low;
return $this;
}
public function getLow(): float
{
return $this->items['low'] ?? 0.0;
}
public function setClose($close): self
{
$this->items['close'] = (float) $close;
return $this;
}
public function getClose(): float
{
return $this->items['close'] ?? 0.0;
}
public function setDate(String|DateTime $date): self
{
$this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s');
return $this;
}
public function getDate(): ?DateTime
{
return $this->items['date'] ?? null;
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
namespace App\Interfaces\MarketData\Types;
use DateTime;
use Illuminate\Support\Carbon;
use App\Interfaces\MarketData\Types\MarketDataType;
class Quote extends MarketDataType
{
public function setName($name): self
{
$this->items['name'] = (string) $name;
return $this;
}
public function getName(): string
{
return $this->items['name'] ?? '';
}
public function setSymbol($symbol): self
{
$this->items['symbol'] = (string) $symbol;
return $this;
}
public function getSymbol(): string
{
return $this->items['symbol'] ?? '';
}
public function setMarketValue($marketValue): self
{
$this->items['market_value'] = (float) $marketValue;
return $this;
}
public function getMarketValue(): float
{
return $this->items['market_value'] ?? 0.0;
}
public function setFiftyTwoWeekHigh($high): self
{
$this->items['fifty_two_week_high'] = (float) $high;
return $this;
}
public function getFiftyTwoWeekHigh(): float
{
return $this->items['fifty_two_week_high'] ?? 0.0;
}
public function setFiftyTwoWeekLow($low): self
{
$this->items['fifty_two_week_low'] = (float) $low;
return $this;
}
public function getFiftyTwoWeekLow(): float
{
return $this->items['fifty_two_week_low'] ?? 0.0;
}
public function setForwardPE($pe): self
{
$this->items['forward_pe'] = (float) $pe;
return $this;
}
public function getForwardPE(): float
{
return $this->items['forward_pe'] ?? 0.0;
}
public function setTrailingPE($pe): self
{
$this->items['trailing_pe'] = (float) $pe;
return $this;
}
public function getTrailingPE(): float
{
return $this->items['trailing_pe'] ?? 0.0;
}
public function setMarketCap($cap): self
{
$this->items['market_cap'] = (int) $cap;
return $this;
}
public function getMarketCap(): int
{
return $this->items['market_cap'] ?? 0;
}
public function setBookValue($value): self
{
$this->items['book_value'] = (float) $value;
return $this;
}
public function getBookValue(): float
{
return $this->items['book_value'] ?? 0.0;
}
public function setLastDividendDate(mixed $date): self
{
$this->items['last_dividend_date'] = is_null($date) ? null : Carbon::parse($date)->format('Y-m-d H:i:s');
return $this;
}
public function getLastDividendDate(): ?DateTime
{
return $this->items['last_dividend_date'] ?? null;
}
public function setDividendYield($yield): self
{
$this->items['dividend_yield'] = (float) $yield;
return $this;
}
public function getDividendYield(): float
{
return $this->items['dividend_yield'] ?? 0.0;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Interfaces\MarketData\Types;
use DateTime;
use Illuminate\Support\Carbon;
use App\Interfaces\MarketData\Types\MarketDataType;
class Split extends MarketDataType
{
public function setSymbol(string $symbol): self
{
$this->items['symbol'] = $symbol;
return $this;
}
public function getSymbol(): string
{
return $this->items['symbol'] ?? '';
}
public function setSplitAmount($splitAmount): self
{
$this->items['split_amount'] = (float) $splitAmount;
return $this;
}
public function getSplitAmount(): float
{
return $this->items['split_amount'] ?? 0.0;
}
public function setDate(String|DateTime $date): self
{
$this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s');
return $this;
}
public function getDate(): ?DateTime
{
return $this->items['date'] ?? null;
}
}
+23 -19
View File
@@ -4,6 +4,10 @@ namespace App\Interfaces\MarketData;
use Illuminate\Support\Collection;
use Scheb\YahooFinanceApi\ApiClient;
use App\Interfaces\MarketData\Types\Ohlc;
use App\Interfaces\MarketData\Types\Quote;
use App\Interfaces\MarketData\Types\Split;
use App\Interfaces\MarketData\Types\Dividend;
use Scheb\YahooFinanceApi\ApiClientFactory as YahooFinance;
class YahooMarketData implements MarketDataInterface
@@ -22,25 +26,25 @@ class YahooMarketData implements MarketDataInterface
return $this->quote($symbol)->isNotEmpty();
}
public function quote(String $symbol): Collection
public function quote(String $symbol): Quote
{
$quote = $this->client->getQuote($symbol);
if (empty($quote)) return collect();
return collect([
return new Quote([
'name' => $quote->getLongName() ?? $quote->getShortName(),
'symbol' => $quote->getSymbol(),
'market_value' => (float) $quote->getRegularMarketPrice(),
'fifty_two_week_high' => (float) $quote->getFiftyTwoWeekHigh(),
'fifty_two_week_low' => (float) $quote->getFiftyTwoWeekLow(),
'forward_pe' => (float) $quote->getForwardPE(),
'trailing_pe' => (float) $quote->getTrailingPE(),
'market_cap' => (int) $quote->getMarketCap(),
'book_value' => (float) $quote->getBookValue(),
'market_value' => $quote->getRegularMarketPrice(),
'fifty_two_week_high' => $quote->getFiftyTwoWeekHigh(),
'fifty_two_week_low' => $quote->getFiftyTwoWeekLow(),
'forward_pe' => $quote->getForwardPE(),
'trailing_pe' => $quote->getTrailingPE(),
'market_cap' => $quote->getMarketCap(),
'book_value' => $quote->getBookValue(),
'last_dividend_date' => $quote->getDividendDate(),
'dividend_yield' => (float) $quote->getTrailingAnnualDividendYield() * 100
'dividend_yield' => $quote->getTrailingAnnualDividendYield() * 100
]);
}
@@ -50,11 +54,11 @@ class YahooMarketData implements MarketDataInterface
return collect($this->client->getHistoricalDividendData($symbol, $startDate, $endDate))
->map(function($dividend) use ($symbol) {
return [
return new Dividend([
'symbol' => $symbol,
'date' => $dividend->getDate()->format('Y-m-d H:i:s'),
'date' => $dividend->getDate(),
'dividend_amount' => $dividend->getDividends(),
];
]);
});
}
@@ -65,11 +69,11 @@ class YahooMarketData implements MarketDataInterface
->map(function($split) use ($symbol) {
$split_amount = explode(':', $split->getStockSplits());
return [
return new Split([
'symbol' => $symbol,
'date' => $split->getDate()->format('Y-m-d H:i:s'),
'date' => $split->getDate(),
'split_amount' => $split_amount[0] / $split_amount[1],
];
]);
});
}
@@ -81,11 +85,11 @@ class YahooMarketData implements MarketDataInterface
$date = $history->getDate()->format('Y-m-d');
return [ $date => [
return [ $date => new Ohlc([
'symbol' => $symbol,
'date' => $date,
'close' => (float) $history->getClose(),
]];
'close' => $history->getClose(),
]) ];
});
}
}
+3 -2
View File
@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Log;
use App\Interfaces\MarketData\YahooMarketData;
use App\Interfaces\MarketData\FallbackInterface;
use App\Interfaces\MarketData\AlphaVantageMarketData;
use App\Interfaces\MarketData\Types\Quote;
class FallbackInterfaceTest extends TestCase
{
@@ -32,7 +33,7 @@ class FallbackInterfaceTest extends TestCase
$alphaMock = Mockery::mock(AlphaVantageMarketData::class);
$alphaMock->shouldReceive('quote')
->andReturn(collect(['Alpha data']));
->andReturn(new Quote(['market_value' => 10]));
$this->app->instance(YahooMarketData::class, $yahooMock);
$this->app->instance(AlphaVantageMarketData::class, $alphaMock);
@@ -41,7 +42,7 @@ class FallbackInterfaceTest extends TestCase
$result = $fallbackInterface->quote('ACME');
$this->assertEquals(collect(['Alpha data']), $result);
$this->assertEquals(new Quote(['market_value' => 10]), $result);
Log::shouldHaveReceived('warning')->with('Failed calling method quote (yahoo): Yahoo failed');
}