Compare commits

..

6 Commits

Author SHA1 Message Date
hackerESQ 23ed2e7155 wip 2026-03-24 21:04:40 -05:00
hackerESQ e8997ecc3e Create BinanceMarketData.php 2026-03-24 19:01:09 -05:00
hackerESQ d449a89349 Show only market gain on performance chart (#190)
(by default) Allow other options as a choice
2026-03-20 20:51:19 -05:00
hackerESQ a98dcd0732 fix escaping issue 2026-03-18 20:30:09 -05:00
hackerESQ eaaa218582 Clean up formatting of locale form and dropdowns (#186) 2026-03-18 20:23:51 -05:00
hackerESQ 67396b23f1 Add french translations (#185) 2026-03-18 20:23:31 -05:00
5 changed files with 155 additions and 10 deletions
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace App\Interfaces\MarketData;
use App\Interfaces\MarketData\Types\Ohlc;
use App\Interfaces\MarketData\Types\Quote;
use Carbon\CarbonInterval;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class BinanceMarketData implements MarketDataInterface
{
public PendingRequest $client;
public string $apiBaseUrl = 'https://data-api.binance.vision/api/v3/';
public function __construct()
{
$this->createNewClient();
}
private function createNewClient()
{
$this->client = Http::withOptions([
'headers' => [
'content-type' => 'application/json',
'accept' => 'application/json',
],
]);
}
public function exists(string $symbol): bool
{
return (bool) $this->quote($symbol);
}
public function quote(string $symbol): Quote
{
$response = $this->client
->baseUrl($this->apiBaseUrl)
->withQueryParameters([
'symbol' => $symbol,
])->get('ticker');
$quote = $response->json();
throw_if(empty(Arr::get($quote, 'weightedAvgPrice')), NotFoundHttpException::class, "Symbol `{$symbol}` was not found");
$fundamental = cache()->remember(
'binance-fdmtl-'.$symbol,
1440,
function () use ($symbol) {
$this->createNewClient();
$response = $this->client
->baseUrl($this->apiBaseUrl)
->withQueryParameters(['symbol' => $symbol])
->get('exchangeInfo');
return $response->json();
}
);
return new Quote([
'name' => $symbol,
'symbol' => $symbol,
'currency' => Arr::get($fundamental, 'symbols.0.quoteAsset'),
'market_value' => (float) Arr::get($quote, 'weightedAvgPrice'),
]);
}
public function dividends(string $symbol, $startDate, $endDate): Collection
{
// noop
return collect();
}
public function splits(string $symbol, $startDate, $endDate): Collection
{
// noop
return collect();
}
public function history(string $symbol, $startDate, $endDate): Collection
{
$startDate = Carbon::parse($startDate);
$endDate = Carbon::parse($endDate);
$allHistory = collect();
$chunks = 500;
$period = CarbonInterval::days($chunks)->toPeriod($startDate, $endDate);
foreach ($period as $startDate) {
$chunkEnd = $startDate->copy()->addDays($chunks - 1);
if ($chunkEnd->gt($endDate)) {
$chunkEnd = $endDate;
}
$this->createNewClient();
$response = $this->client
->baseUrl($this->apiBaseUrl)
->withQueryParameters([
'symbol' => $symbol,
'interval' => '1d',
'startTime' => $startDate->timestamp * 1000,
'endTime' => $chunkEnd->timestamp * 1000,
])->get('klines');
$history = $response->json();
throw_if(empty($history), NotFoundHttpException::class, "Symbol `{$symbol}` was not found");
$chunkedHistory = collect($history)
->mapWithKeys(function ($history_item) use ($symbol) {
$date = Carbon::parse($history_item[0])->format('Y-m-d');
return [$date => new Ohlc([
'symbol' => $symbol,
'date' => $date,
'close' => $history_item[4],
])];
});
$allHistory = $allHistory->merge($chunkedHistory);
}
return $allHistory;
}
}
@@ -99,6 +99,7 @@
this.data.tooltip = { this.data.tooltip = {
enabled: true, enabled: true,
shared: false,
y: { y: {
formatter: (value, { series, seriesIndex, dataPointIndex, w }) => { formatter: (value, { series, seriesIndex, dataPointIndex, w }) => {
const firstDataPoint = this.data.series[seriesIndex].data[0][1] const firstDataPoint = this.data.series[seriesIndex].data[0][1]
@@ -75,26 +75,29 @@ new #[Lazy] class extends Component
foreach ($dailyChange as $data) { foreach ($dailyChange as $data) {
$date = $data->date; $date = $data->date;
$marketGainData[] = [$date, round($data->total_market_gain, 2)];
$marketValueData[] = [$date, round($data->total_market_value, 2)]; $marketValueData[] = [$date, round($data->total_market_value, 2)];
$costBasisData[] = [$date, round($data->total_cost_basis, 2)]; $costBasisData[] = [$date, round($data->total_cost_basis, 2)];
$marketGainData[] = [$date, round($data->total_market_gain, 2)];
// $dividendSeries[] = [$date, round($data->total_dividends_earned, 2)]; // $dividendSeries[] = [$date, round($data->total_dividends_earned, 2)];
// $realizedGainSeries[] = [$date, round($data->realized_gains, 2)]; // $realizedGainSeries[] = [$date, round($data->realized_gains, 2)];
} }
return [ return [
'series' => [ 'series' => [
[
'name' => __('Market Gain'),
'data' => $marketGainData,
],
[ [
'name' => __('Market Value'), 'name' => __('Market Value'),
'data' => $marketValueData, 'data' => $marketValueData,
'hidden' => true,
], ],
[ [
'name' => __('Cost Basis'), 'name' => __('Cost Basis'),
'data' => $costBasisData, 'data' => $costBasisData,
], 'hidden' => true,
[
'name' => __('Market Gain'),
'data' => $marketGainData,
], ],
// [ // [
@@ -154,7 +154,7 @@ new class extends Component
<x-slot:actions> <x-slot:actions>
@if (auth()->user()->id != $user->id) @if (auth()->user()->id != $user->id)
<x-ui.select <x-ui.select
class="select select-ghost border-none focus:outline-none focus:ring-0" class="cursor-pointer select-ghost border-none focus:outline-none focus:ring-0"
:options="[['id' => 0, 'name' => __('Read only')], ['id' => 1, 'name' => __('Full access')]]" :options="[['id' => 0, 'name' => __('Read only')], ['id' => 1, 'name' => __('Full access')]]"
wire:model.live.number="permissions.{{ $user->id }}.full_access" wire:model.live.number="permissions.{{ $user->id }}.full_access"
/> />
@@ -68,8 +68,8 @@ new class extends Component
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<x-ui.select <x-ui.select
label="{{ __('Locale') }}" :label="__('Locale')"
class="select block mt-1 w-full" class=""
:options="config('app.available_locales')" :options="config('app.available_locales')"
option-value="locale" option-value="locale"
option-label="label" option-label="label"
@@ -83,8 +83,8 @@ new class extends Component
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<x-ui.select <x-ui.select
label="{{ __('Display Currency') }}" :label="__('Display Currency')"
class="select block mt-1 w-full" class=""
:options="$currencies" :options="$currencies"
option-value="currency" option-value="currency"
option-label="label" option-label="label"