Implemented features.

This commit is contained in:
Daniel Scheidle
2022-11-09 19:13:18 +01:00
parent 843def9ecd
commit 5f46bd167f
34 changed files with 460 additions and 104 deletions

View File

@@ -1,11 +1,11 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {SentimentComponent} from "./pages/sentiment/sentiment.component";
import {SentimentPageComponent} from "./pages/sentiment-page/sentiment-page.component";
import {LandingPageComponent} from "./pages/landing-page/landing-page.component";
const routes: Routes = [
{path: 'sentiment/:symbol', component: SentimentComponent},
{path: 'sentiment/:symbol', component: SentimentPageComponent},
{path: '**', component: LandingPageComponent}
];

View File

@@ -0,0 +1,4 @@
.content {
max-width: 60rem;
margin: auto;
}

View File

@@ -1 +1,3 @@
<router-outlet></router-outlet>
<div class="content">
<router-outlet></router-outlet>
</div>

View File

@@ -4,7 +4,7 @@ import {BrowserModule} from '@angular/platform-browser';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {HttpClientModule} from "@angular/common/http";
import {SentimentComponent} from './pages/sentiment/sentiment.component';
import {SentimentPageComponent} from './pages/sentiment-page/sentiment-page.component';
import {LandingPageComponent} from './pages/landing-page/landing-page.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatCardModule} from "@angular/material/card";
@@ -13,18 +13,25 @@ import {MatButtonModule} from "@angular/material/button";
import {MatInputModule} from "@angular/material/input";
import {MatDividerModule} from "@angular/material/divider";
import {FormsModule} from "@angular/forms";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MatSnackBarModule} from "@angular/material/snack-bar";
import {MatGridListModule} from "@angular/material/grid-list";
import { StockCardComponent } from './components/stock-card/stock-card.component';
import {StockCardComponent} from './components/stock-card/stock-card.component';
import {MatIconModule} from "@angular/material/icon";
import {SentimentCardComponent} from './components/sentiment-card/sentiment-card.component';
import {TendencyComponent} from './components/tendency/tendency.component';
import {SentimentComponent} from './components/sentinment/sentiment.component';
import { MonthPipe } from './pipes/month.pipe';
@NgModule({
declarations: [
AppComponent,
SentimentComponent,
SentimentPageComponent,
LandingPageComponent,
StockCardComponent
StockCardComponent,
SentimentCardComponent,
TendencyComponent,
SentimentComponent,
MonthPipe
],
imports: [
FormsModule,

View File

@@ -0,0 +1,12 @@
.mat-card {
margin: 1rem 0;
height: 14rem;
line-height: 2rem;
font-size: 16px;
}
.grid-container {
display: grid;
grid-template-columns: auto auto auto;
padding: 10px;
}

View File

@@ -0,0 +1,21 @@
<mat-card *ngIf="loaded; else loading">
<ng-container>
<mat-card-title>{{company.description}} ({{company.symbol}})</mat-card-title>
<mat-card-content>
<div class="grid-container content">
<div class="grid-item" *ngFor="let insiderSentiment of insiderSentiments ">
<app-sentiment [month]="insiderSentiment.month"
[change]="insiderSentiment.change"
[mspr]="insiderSentiment.mspr"></app-sentiment>
</div>
</div>
</mat-card-content>
</ng-container>
</mat-card>
<ng-template #loading>
<mat-card>
<mat-card-title>Loading...</mat-card-title>
<mat-card-content></mat-card-content>
</mat-card>
</ng-template>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SentimentCardComponent } from './sentiment-card.component';
describe('SentimentCardComponent', () => {
let component: SentimentCardComponent;
let fixture: ComponentFixture<SentimentCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SentimentCardComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(SentimentCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,46 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {Company, InsiderSentimentData} from "../../model/company-data";
import {FinnhubService} from "../../services/finnhub.service";
import {Subscription} from "rxjs";
@Component({
selector: 'app-sentiment-card',
templateUrl: './sentiment-card.component.html',
styleUrls: ['./sentiment-card.component.css']
})
export class SentimentCardComponent implements OnInit, OnDestroy {
@Input()
symbol = '';
loaded = false;
company!: Company;
insiderSentiments!: Array<InsiderSentimentData>;
private loadedSubs = 0;
private subs: Subscription[] = [];
constructor(private finnhubService: FinnhubService) {
}
ngOnInit(): void {
this.subs.push(this.finnhubService.getCompany(this.symbol).subscribe(value => {
this.company = value;
this.loaded = ++this.loadedSubs == this.subs.length;
}));
let current = new Date()
let from = new Date(current.getFullYear(), current.getMonth() - 3, 1);
let to = new Date(current.getFullYear(), current.getMonth() - 1, 1);
this.subs.push(this.finnhubService.getInsideSentiment(this.symbol, from, to).subscribe(value => {
this.insiderSentiments = value.data;
this.loaded = ++this.loadedSubs == this.subs.length;
}));
}
ngOnDestroy(): void {
this.subs.forEach(s => s.unsubscribe());
}
}

View File

@@ -0,0 +1,10 @@
.grid-container {
display: grid;
grid-template-columns: auto auto;
padding: 10px;
}
.grid-item .label {
font-weight: bold;
margin: 1rem;
}

View File

@@ -0,0 +1,11 @@
<div class="grid-container ">
<div class="grid-item">
<p><span class="label">{{month | month }}</span></p>
<p><span class="label">Change:</span><span class="value">{{change | number: '1.0-0'}}</span></p>
<p><span class="label">MSPR:</span><span class="value">{{mspr | number: '1.2-2'}}</span></p>
</div>
<div class="grid-item">
<app-tendency [value]="change"></app-tendency>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SentimentComponent } from './sentiment.component';
describe('SentimentComponent', () => {
describe('SentinmentComponent', () => {
let component: SentimentComponent;
let fixture: ComponentFixture<SentimentComponent>;

View File

@@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core';
import {FinnhubService} from "../../services/finnhub.service";
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'app-sentiment',
@@ -8,9 +7,19 @@ import {FinnhubService} from "../../services/finnhub.service";
})
export class SentimentComponent implements OnInit {
constructor(private finnhubService: FinnhubService) { }
@Input()
month!: number;
@Input()
change!: number;
@Input()
mspr!: number;
constructor() { }
ngOnInit(): void {
}
}
}

View File

@@ -1,16 +1,24 @@
.mat-card {
margin: 1rem;
margin: 1rem 0;
height: 14rem;
line-height: 2rem;
font-size: 16px;
}
.mat-card-title .mat-icon {
a.remove {
cursor: pointer;
float: right;
}
.mat-grid-tile .mat-icon {
font-size: 4rem;
height: 4rem;
width: 4rem;
.grid-container {
display: grid;
grid-template-columns: auto auto;
padding: 10px;
}
.grid-item .label {
font-weight: bold;
margin: 1rem;
}

View File

@@ -1,27 +1,54 @@
<mat-card>
<mat-card *ngIf="loaded; else loading">
<mat-card-title>{{company.description}} ({{company.symbol}})
<mat-icon (click)="remove()" fontIcon="close"></mat-icon>
<a (click)="remove()" id="{{'remove'+ company.symbol}}" class="remove">
<mat-icon fontIcon="close"></mat-icon>
</a>
</mat-card-title>
<mat-card-content>
<ng-container *ngIf="getQuote() | async; let quote">
<mat-grid-list cols="4" rowHeight="4:1">
<mat-grid-tile colspan="2"></mat-grid-tile>
<mat-grid-tile colspan="2" rowspan="3">
<mat-icon *ngIf="quote.dp>0" color="primary" fontIcon="arrow_upward"></mat-icon>
<mat-icon *ngIf="quote.dp<0" color="warn" fontIcon="arrow_downward"></mat-icon>
<mat-icon *ngIf="quote.dp==0" fontIcon="east"></mat-icon>
</mat-grid-tile>
<div class="grid-container content">
<div class="grid-item">
<div class="grid-container values">
<div class="grid-item">
<span class="label">Change today:</span>
<span class="value">{{quote.dp / 100 | percent: '1.1-1'}}</span>
</div>
<div class="grid-item">
<span class="label">Opening price:</span>
<span class="value">{{quote.o | currency: 'USD'}}</span>
</div>
<div class="grid-item">
<span class="label">Current price:</span>
<span class="value">{{quote.c | currency: 'USD'}}</span>
</div>
<div class="grid-item">
<span class="label">High price:</span>
<span class="value">{{quote.h | currency: 'USD'}}</span>
</div>
<mat-grid-tile><p>Change today: {{quote.dp / 100 | percent: '1.1-1'}}</p></mat-grid-tile>
<mat-grid-tile><p>Opening price: {{quote.o | currency: 'USD'}}</p></mat-grid-tile>
<mat-grid-tile><p>Current price: {{quote.c | currency: 'USD'}}</p></mat-grid-tile>
<mat-grid-tile><p>High price: {{quote.h | currency: 'USD'}}</p></mat-grid-tile>
</mat-grid-list>
</ng-container>
</div>
</div>
<div class="grid-item">
<app-tendency [value]="quote.dp"></app-tendency>
</div>
</div>
</mat-card-content>
<mat-card-actions align="end">
<button mat-button [routerLink]="'/sentiment/' + company.symbol">Go to social sentiment details</button>
<button mat-button
id="{{'sentiment'+ company.symbol}}"
[routerLink]="'/sentiment/' + symbol">Go to social sentiment details
</button>
</mat-card-actions>
</mat-card>
<ng-template #loading>
<mat-card>
<mat-card-title>Loading...</mat-card-title>
<mat-card-content></mat-card-content>
</mat-card>
</ng-template>

View File

@@ -1,7 +1,7 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {FinnhubService} from "../../services/finnhub.service";
import {Company, QuoteResponse} from "../../model/company-data";
import {Subject, Subscription} from "rxjs";
import {Subscription} from "rxjs";
import {StorageService} from "../../services/storage.service";
@Component({
@@ -12,9 +12,14 @@ import {StorageService} from "../../services/storage.service";
export class StockCardComponent implements OnInit, OnDestroy {
@Input()
company = {} as Company;
symbol = '';
private _quote$ = new Subject<QuoteResponse>();
loaded = false;
company!: Company;
quote!: QuoteResponse;
private loadedSubs = 0;
private subs: Subscription[] = [];
constructor(public finnhubService: FinnhubService,
@@ -22,19 +27,22 @@ export class StockCardComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.subs.push(this.finnhubService.getQuote(this.company.symbol).subscribe(value => this._quote$.next(value)));
this.subs.push(this.finnhubService.getCompany(this.symbol).subscribe(value => {
this.company = value;
this.loaded = ++this.loadedSubs == this.subs.length;
}));
this.subs.push(this.finnhubService.getQuote(this.symbol).subscribe(value => {
this.quote = value;
this.loaded = ++this.loadedSubs == this.subs.length;
}));
}
getQuote(): Subject<QuoteResponse> {
return this._quote$;
}
ngOnDestroy(): void {
this.subs.forEach(s => s.unsubscribe());
this._quote$.complete();
}
remove() {
this.storageService.remove(this.company.symbol);
this.storageService.remove(this.symbol);
}
}

View File

@@ -0,0 +1,6 @@
.mat-icon {
font-size: 6rem;
height: 100%;
width: 100%;
text-align: center;
}

View File

@@ -0,0 +1,3 @@
<mat-icon *ngIf="value>0" color="primary" fontIcon="arrow_upward"></mat-icon>
<mat-icon *ngIf="value<0" color="warn" fontIcon="arrow_downward"></mat-icon>
<mat-icon *ngIf="value==0" fontIcon="east"></mat-icon>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TendencyComponent } from './tendency.component';
describe('TendencyComponent', () => {
let component: TendencyComponent;
let fixture: ComponentFixture<TendencyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ TendencyComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(TendencyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,19 @@
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'app-tendency',
templateUrl: './tendency.component.html',
styleUrls: ['./tendency.component.css']
})
export class TendencyComponent implements OnInit {
@Input()
value!: number;
constructor() { }
ngOnInit(): void {
}
}

View File

@@ -19,3 +19,18 @@ export interface QuoteResponse {
o: number;
pc: number;
}
export interface InsiderSentimentData {
symbol: string;
year: number;
month: number;
change: number;
mspr: number;
}
export interface InsiderSentimentResponse {
data: Array<InsiderSentimentData>;
symbol: string;
}

View File

@@ -1,7 +1,11 @@
.mat-card {
margin: 1rem;
.search-box {
padding: 0;
}
.mat-divider {
margin: 0 1rem;
.mat-form-field {
margin-right: 1rem;
}
.mat-card {
margin: 1rem 0;
}

View File

@@ -1,26 +1,28 @@
<mat-card>
<mat-card-content>
<p>
Enter the symbol of a stock to track (i.e. AAPL, TSLA, GOOGL)
</p>
<div class="search-box">
<p>
Enter the symbol of a stock to track (i.e. AAPL, TSLA, GOOGL)
</p>
<mat-form-field>
<input id="stockInput"
matInput
[(ngModel)]="symbol"/>
</mat-form-field>
<mat-form-field>
<input id="stockInput"
matInput
[(ngModel)]="symbolInput"/>
</mat-form-field>
<button id="trackBtn"
mat-stroked-button
color="primary"
[disabled]="disabled"
(click)="addSymbol()">Track Stock
</button>
<button id="trackBtn"
mat-stroked-button
color="primary"
[disabled]="disabled"
(click)="addSymbol()">Track Stock
</button>
</div>
</mat-card-content>
</mat-card>
<mat-divider></mat-divider>
<ng-container *ngFor="let company of storageService.get() | async ">
<app-stock-card [company]="company"></app-stock-card>
<ng-container *ngFor="let symbol of storageService.get() | async ">
<app-stock-card [symbol]="symbol"></app-stock-card>
</ng-container>

View File

@@ -10,7 +10,7 @@ import {MatSnackBar} from "@angular/material/snack-bar";
})
export class LandingPageComponent implements OnInit {
symbol = '';
symbolInput = '';
disabled = false;
constructor(public finnhubService: FinnhubService,
@@ -24,29 +24,16 @@ export class LandingPageComponent implements OnInit {
addSymbol() {
console.log('search for ' + this.symbol)
// start loading...
this.disabled = true;
this.finnhubService.searchSymbol(this.symbol).subscribe(value => {
if (value.count > 0) {
let company = value.result.find(company => company.symbol == this.symbol);
if (company) {
this.storageService.add(company);
this.symbol = '';
} else {
this.snackBar.open('No unique result', 'close', {duration: 10000});
}
this.disabled = false
} else {
this.snackBar.open('No result', 'close', {duration: 10000});
this.disabled = false
}
}, (error) => {
this.snackBar.open('Communication error', 'close', {duration: 10000});
this.finnhubService.getCompany(this.symbolInput).subscribe(value => {
this.storageService.add(value.symbol);
this.symbolInput = '';
this.disabled = false;
}, (error: string) => {
this.snackBar.open(error, 'close', {duration: 10000});
this.disabled = false;
});
}
}

View File

@@ -0,0 +1,3 @@
.mat-card {
margin: 1rem 0;
}

View File

@@ -0,0 +1,5 @@
<ng-container *ngFor="let symbol of _symbols$ | async ">
<app-sentiment-card [symbol]="symbol"></app-sentiment-card>
</ng-container>
<button mat-button id="backBtn" [routerLink]="'/'">Back to list of stocks</button>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SentimentPageComponent } from './sentiment-page.component';
describe('SentimentComponent', () => {
let component: SentimentPageComponent;
let fixture: ComponentFixture<SentimentPageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SentimentPageComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(SentimentPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,25 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
import {BehaviorSubject} from "rxjs";
import {StorageService} from "../../services/storage.service";
@Component({
selector: 'app-sentiment-page',
templateUrl: './sentiment-page.component.html',
styleUrls: ['./sentiment-page.component.css']
})
export class SentimentPageComponent implements OnInit {
_symbols$ = new BehaviorSubject<string[]>([]);
constructor(private route: ActivatedRoute,
private storageService: StorageService) {
}
ngOnInit(): void {
this.route.params.subscribe(params => {
this._symbols$.next([params['symbol']]);
});
}
}

View File

@@ -1,4 +0,0 @@
<p>sentiment works!</p>
<button mat-button [routerLink]="'/'">Back to list of stocks</button>

View File

@@ -0,0 +1,8 @@
import { MonthPipe } from './month.pipe';
describe('MonthPipe', () => {
it('create an instance', () => {
const pipe = new MonthPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
import {formatDate} from "@angular/common";
@Pipe({
name: 'month'
})
export class MonthPipe implements PipeTransform {
transform(value: number): unknown {
let date = new Date();
date.setMonth(value);
return formatDate(date, 'MMMM','en-US');
}
}

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {QuoteResponse, SearchResponse} from "../model/company-data";
import {catchError, from, map, Observable} from "rxjs";
import {Company, InsiderSentimentResponse, QuoteResponse, SearchResponse} from "../model/company-data";
import {formatDate} from "@angular/common";
@Injectable({
providedIn: 'root'
@@ -12,7 +13,7 @@ export class FinnhubService {
private readonly api = 'https://finnhub.io/api/v1';
private readonly apiQuote = this.api + '/quote';
private readonly apiSearch = this.api + '/search';
private readonly apiSentiment = this.api + '/stock/insider-sentiment';
constructor(private httpClient: HttpClient) { }
@@ -21,9 +22,37 @@ export class FinnhubService {
return this.httpClient.get<SearchResponse>(this.apiSearch, {params: { ...this.token, q }});
}
getQuote(symbol: string) {
getCompany(symbol: string): Observable<Company> {
return this.searchSymbol(symbol).pipe(map(value => {
if (value.count > 0) {
let company = value.result.find(company => company.symbol == symbol);
if (company) {
return company;
} else {
throw new Error('No unique result');
}
} else {
throw new Error('No result');
}
}), catchError((err: Error, caught) => {
throw new Error(err.message);
}));
}
getQuote(symbol: string): Observable<QuoteResponse> {
return this.httpClient.get<QuoteResponse>(this.apiQuote, {params: { ...this.token, symbol }});
}
getInsideSentiment(symbol: string, fromDate: Date, toDate: Date): Observable<InsiderSentimentResponse> {
let from = formatDate(fromDate,'yyyy-MM-dd','en-US');
let to = formatDate(toDate,'yyyy-MM-dd','en-US');
return this.httpClient.get<InsiderSentimentResponse>(this.apiSentiment, {params: { ...this.token, symbol, from, to }});
}
}

View File

@@ -1,28 +1,26 @@
import {Injectable, OnDestroy} from '@angular/core';
import {BehaviorSubject} from "rxjs";
import {Company} from "../model/company-data";
@Injectable({
providedIn: 'root'
})
export class StorageService implements OnDestroy {
private storage: Company[] = [];
private _storage$ = new BehaviorSubject<Company[]>([]);
private storage: string[] = [];
private _storage$ = new BehaviorSubject<string[]>([]);
constructor() {
}
add(company: Company): void {
this.storage.push(company);
add(symbol: string): void {
this.storage.push(symbol);
this._storage$.next(this.storage)
}
remove(symbol: string): void {
console.log('Remove ' + symbol)
let index = this.storage.findIndex(company => company.symbol == symbol);
let index = this.storage.findIndex(item => item == symbol);
if (index > -1) {
this.storage.splice(index, 1);
}
@@ -31,7 +29,7 @@ export class StorageService implements OnDestroy {
this._storage$.next(this.storage)
}
get(): BehaviorSubject<Company[]> {
get(): BehaviorSubject<string[]> {
return this._storage$;
}

View File

@@ -1,4 +1,10 @@
/* You can add global styles to this file, and also import other style files */
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
html, body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}