bossaDataReader - aplikacja do pobierania historycznych notowań/wycen/kursów

1

Hej,
mój pierwszy post na forum i od razu zwracam się do Was z gorącą prośbą o podzielenie się opinią na temat aplikacji, którą tworzę.

Wersja live: http://marketdata.com.pl (powinna działać poprawnie w Safari, Chrome, Firefox)
Repozytorium: https://bitbucket.org/olafd/bossadatareader/src/master/

Na wstępie dodam, że doświadczenia zawodowego nie mam (a sama aplikacja ma mi pomóc w znalezieniu pracy jako programista), a uczę się programować od dobrego roku.

Wrzuciłem do repo plik .jar więc do uruchomienia powinna wystarczyć komenda:

java -Dspring.profiles.active=h2 -jar bossaDataReader-0.0.1-SNAPSHOT.jar

W takim wypadku aplikacja korzysta z bazy testowej H2, dla MySQL potrzebna jest konfiguracja pliku application.properties
Dane można załadować w wersji podstawowej lub rozszerzonej wówczas dodatkowe informacje są scrapowane z zewnętrznego serwisu.

Przetestowałem przed chwilą powyższą opcję i okazuje się, że na słabszej maszynie może pojawić się problem z pamięcią. Domyślam się zresztą, że taki sposób ładowania danych do bazy jest raczej mocno naiwny. Włączyłem i zwiększyłem batch oraz alokację id do absurdalnych rozmiarów co przyśpieszyło cały proces o jakieś +/- 50%, ale na końcu w starciu z darmowym AWS i tak takie rozwiązanie nie zdało egzaminu i obecnie dane ładuje lokalnie, łączę się zdalnie z bazą AWS i wrzucam już istniejącą DB. Wiem, że można wrzucić dane bezpośrednio z plików CSV, ale co wówczas z tworzeniem relacji oraz mapowaniem w Javie? Jak ktoś ma jakieś sugestie jak to lepiej rozwiązać to zapraszam do dyskusji.

Podstawowym celem jest umożliwienie użytkownikowi pobieranie historycznych notowań:

  • spółek z rynku głównego GPW;
  • spółek z rynku NewConnect;
  • indeksów GPW;
  • wybranych indeksów giełd światowych.

Jak i również wycen/kursów:

  • jednostek uczestnictwa funduszy inwestycyjnych;
  • OFE;
  • kursów walut NBP.

Bardziej szczegółowy opis można znaleźć w pliku ReadMe w repo.

Stack: Java 8, Spring, Spring Boot, Thymeleaf + jQuery i odrobina Bootstrapa oraz AWS jako hosting. Dlaczego tak, a nie inaczej? Bo w tym potrafiłem to zrealizować.

Obecnie jestem całkiem zadowolony z całej tej konstrukcji zwłaszcza gdy porównam wersje obecną z tą, która jako pierwsza trafiła na AWS. Ostatnie 2 miesiące to ciągły refactoring, który gdy spojrzałem na liczby (wszystko poszło w jednym komincie, a jak) skończyło się na 207 stworzonych/edytowanych plikach czyli, o zgrozo, prawie cała aplikacja powstała od nowa.

Z czego nie jestem zadowolony to na pewno mam sporo rzutowania, którego nie potrafiłem wyeliminować. Najbardziej jednak nie podoba mi się zastosowanie tabeli pośredniczącej dla każdego typu aktywa, która zawiera id każdego profilu oraz id ostatniego notowania/wyceny/kursu. Próbowałem to rozwiązać. Miałem JoinColumn, które mi fajnie wyciągało ostatnie dane, ale wówczas nie mogłem wyciągnąć jednym zapytaniem wszystkich aktywów z ostatnimi danymi, a było to kluczowe dla aktualizacji.

@ManyToOne
@JoinFormula(value = "(SELECT s.id FROM single_listing_days s where s.company = id ORDER BY s.date DESC LIMIT 1)")
private SingleListingDay singleListingDay;

Nie wiem, może trzeba by użyć bezpośrednio EntityManagera chociaż pewnie jest sposób żeby to zrobić przy użyciu samego springowego repo. Dodatkowo też w kilku miejsca interfejs ma metodę, która ma faktyczną implementację tylko w niektórych implementujących go klasach. Średnio mi się takie rozwiązanie podoba.

Domyślam się również, że sporo może być uwag odnośnie frontendu. Przede wszystkim nie jest to element na którym chce się skupiać i powstał przede wszystkim żeby móc testować i zaprezentować funkcjonalności. Jest to również temat w którym mam najmniej godzin przy kodzie dlatego wiele elementów sprawiało mi problem. Można więc uznać to bardziej za prototyp. Nie wykluczam jednak, że wraz z dodaniem kolejnych funkcji zdecyduję się na przebudowę i być może wykorzystam to jako pole do nauki jakiegoś frameworka. Zdaje sobie również sprawę, że punktowe wykorzystanie Bootstrapa z jego wyizolowanym CSS nie jest zbyt fortunnym rozwiązaniem, ale na ten moment jest jak jest bo bym nie skończył ciągle czegoś poprawiać.

Generalnie to co możecie obejrzeć to dla mnie core aplikacji, który musiałem stworzyć żeby móc dalej myśleć o tworzeniu tego co było moim celem od początku czyli przetwarzanie tych danych. Chcę zacząć od podstaw jak stopy zwrotu, inne miary statystyczne, możliwość porównywania poszczególnych spółek, być może symulacje wyników danego portfela. Jeszcze nie mam pojęcia jak to zrobię, ale bez tego co do tej pory stworzyłem nie byłbym w stanie nic zrobić. Dlatego proszę Was o podzielenie się sugestiami przede wszystkim w tym kontekście co by można teraz jeszcze poprawić żeby później nie wpaść na minę.

Za wszelkie sugestie/opinie z góry dziękuję.

3

Zapomniałeś zacommitować katalogu z testami :)

Na szybko:

Generalnie fajnie, jednocześnie:

  1. Brak testów
  2. Model (dto) ma jakiś kod związany z wypisywaniem ( getPrintHeaders()) - to nie jest odpowiedzialność tej warstwy
  3. Dlaczego serwisy są w webie?
  4. AssetService - to jakiś wzorzec projektowy? Co Ci to daje? Podobnie Letters
  5. Encje mają zależność do dto - nie rozumiem
  6. Użycie Float zapala czerwoną lampkę (w dodatku Float[]) - dlaczego korzystasz z tego typu?
  7. Lombok - użycie @Data powoduje, że wszystko masz mutowalne - zapewne nawet o tym nie wiesz :)
  8. Używasz JPA do wyciągania danych - czy nie lepiej skorzystać z JDBC albo coś w stylu JOOQ? Ominąłbyś sporo gimnastyki, widzę, że w niektórych miejscach nawet stosujesz native query.
  9. Szukajki findAll - brak stronicowania
  10. Piszesz, że aplikacja ładuje 3M rekordów i zajmie to co najmniej 10 minut. Dobrze myślę, że za pół roku będzie to 30M rekordów, a za parę lat wysadzi mi pamięć?
  11. @Autowired na polach
  12. Klasa CellWriter - dziwne są te defaultowe metody, nie zwracają Cell, którą tam modyfikujesz - to działa?
0
Charles_Ray napisał(a):

Zapomniałeś zacommitować katalogu z testami :)

Na szybko:

Generalnie fajnie, jednocześnie:

  1. Brak testów>

Winny. Aczkolwiek z mojej perspektywy dopiero teraz jest sens pisać testy. To był jeden wielki plac budowy, a TDD to póki co dla mnie sci-fi.

  1. Model (dto) ma jakiś kod związany z wypisywaniem ( getPrintHeaders()) - to nie jest odpowiedzialność tej warstwy>

Do modelu w MVC trafia po wyszukaniu profilu encja z bazy w postaci DTO i dalej z tego modelu do generowania wykresu oraz pobierania danych przez użytkownika. W zależności od typu aktywa nagłówki w CSV/XLS mogą się różnić. Metoda ta jest wykorzystywana przez Writera do ich pobrania. Jedyna alternatywa jaka mi teraz przychodzi do głowy to przerzucenie odpowiedzialności za ten etap na Writera i zastosowanie tam 'instance of'. Ewentualnie w momencie konwersji z bazy na DTO zastosowanie jakiejś dodatkowej zmiennej, która by określała typ. Jakiś może Enum w DTO?

  1. Dlaczego serwisy są w webie?>

Bo służą do obsługi webu. Przynajmniej ja się kierowałem taką logiką.

  1. AssetService - to jakiś wzorzec projektowy? Co Ci to daje? Podobnie Letters>

Każdy typ aktywa ma oddzielne repo do którego muszę się zwrócić. W momencie więc w którym użytkownik prosi np. o fundusz inwestycyjny to zwracany jest AssetService dedykowany danemu typowi z implementacją wyszukiwania. Z Letters historia jest podobna tylko, że jest ona rozszerzoną wersją AssetService ponieważ implementuje dodatkową funkcjonalność. Na froncie masz kilka opcji wyszukiwania profili. Pierwszy to wyszukiwarka, wówczas każdy AssetService woła fetchAllDTOsFromDB() i dalej wynik jest filtrowany do podanej frazy. Następnie można wybrać sobie z tabelki wszystkich aktywów (w przypadku gdy profili jest mało, kilkadziesiąt). Tutaj jednak jest wołana znowu ta sama metoda, ale tylko dla wybranego typu i front, a dokładnie Thymeleaf generuje button dla każdej encji. No i jest jeszcze opcja wyboru z tabeli, ale posegregowanej alfabetycznie z wyborem początkowej litery. To rozwiązanie dotyczy typów gdzie wyników jest dużo (kilkaset). Nie wszystkie AssetService korzystają z tej funkcji ponieważ jeżeli profili jest mało (np. indeksy światowe to zaledwie 25 sztuk) to nie było takiej potrzeby. Dlatego wydzieliłem dodatkowy interfejs dla tych, które mają go implementować. Letters korzysta z bardziej sprecyzowanego zapytania, które filtruje wyniki na podstawie pierwszej litery nazwy, a następnie stosuje dodatkowo paginację aby pojedyncza strona na froncie miała 5 wyników.

O ile Letters można by usunąć i wymusić implementację we wszystkich AssetService (tylko czy dobrym rozwiązaniem jest pisać kod, który jest niewykorzystywany?), o tyle nie wiem jak mógłbym inaczej obsłużyć samo pobieranie danych z bazy jeżeli nie dedykowanym AssetService.

  1. Encje mają zależność do dto - nie rozumiem>

Chodzi Tobie o convertToDTO()? Chciałem móc zawołać na encji konwersję do DTO. Czyli np. użytkownik pyta o profil spółki giełdowej, najpierw dostaje odpowiedni AssetService, ten wyciąga profil z bazy i konwertuje go do DTO i wrzuca do modelu, który tkwi w sesji do momentu wyszukania nowego profilu. W ten sposób jednym zapytaniem do bazy wyświetlam wykres na stronie i umożliwiam pobranie danych. Wcześniej miałem RESTowe zapytanie dla wykresu oraz oddzielną obsługę pobierania, które ponownie odpytywało bazę danych.

Mógłbym mieć konstruktor w klasie DTO, który przyjmie encje, ale wówczas muszę się odnosić do konkretnego typu, a nie mogę na każdym obiekcie klasy Asset zawołać convertToDTO(). Ewentualnie można by to przerzucić do wspomnianego AssetService, który by miał taką opcję.

  1. Użycie Float zapala czerwoną lampkę (w dodatku Float[]) - dlaczego korzystasz z tego typu?>

O ile czegoś nie przeoczyłem to ten typ danych jest wymagany przez bibliotekę do generowania wykresów na froncie - Highcharts JS. Aczkolwiek zdążyłem przerobić problem z zastosowaniem tego typu w bazie i szukaniem powodu różnic w danych rzędu setnych dziesiętnych w pojedynczych rekordach.

  1. Lombok - użycie @Data powoduje, że wszystko masz mutowalne - zapewne nawet o tym nie wiesz :)>

Gdzieś trafiłem na tę informację, ale nie sądzę abym brał to pod uwagę (tak jak wiedziałem, że należy stosować BigDecimal dla danych finansowych, ale na początku i tak wpakowałem się w floata). Z drugiej strony skoro to tylko DTO to pewnie zrobiłem tak żeby było jak najmniej kodu. Tym bardziej, że to tylko DTO.

  1. Używasz JPA do wyciągania danych - czy nie lepiej skorzystać z JDBC albo coś w stylu JOOQ? Ominąłbyś sporo gimnastyki, widzę, że w niektórych miejscach nawet stosujesz native query.>

Najniżej z czym miałem do czynienia to Hibernate czyli praktycznie ten sam poziom co Spring Data. Pewnie bym dał radę się nauczyć niższego poziomu, ale jak zaczynałem tworzyć aplikację to nie brałem tego pod uwagę. Jak wspomniałem, napisałem w czym potrafiłem, a skoro korzystam ze Springa to nawet nie brałem nawet pod uwagę czystego Hibernate'a, a co dopiero niższej implementacje. Pytanie co bym zyskał? Aczkolwiek nie mówię nie, być może to by był dobry pomysł. Lubię wiedzieć dlaczego i jak coś działa. Już zdążyłem dojść do wniosku, że im bardziej coś jest przykryte tym trudniej jest czasami rozwiązać problem bo nie wiemy dlaczego coś może nie działać.

  1. Szukajki findAll - brak stronicowania>

Traktowałem to raczej jako opcję, którą można wykorzystać aniżeli dobrą praktykę?

  1. Piszesz, że aplikacja ładuje 3M rekordów i zajmie to co najmniej 10 minut. Dobrze myślę, że za pół roku będzie to 30M rekordów, a za parę lat wysadzi mi pamięć?>

Nie. :) Zauważ, że aplikacja umożliwia pobieranie danych historycznych. Dla takiego indeksu S&P500 możesz cofnąć się aż do 02.01.1970. Dla polskiej giełdy to jest połowa 1991 roku. Chociaż nie wiem czy jakaś spółka przetrwała jeszcze od pierwszego notowania do dzisiaj to pewnie jest trochę niewiele młodszych. Na szybko sprawdziłem i pojedyncza aktualizacja (każdy dzień roboczy z wyjątkami) to 1000 nowych rekordów. Czyli w miesiąc to +/- 20000 czyli 240 000 rocznie.

  1. @Autowired na polach>

??? Że nie na konstruktorze? Spotkałem się z informacją, że zalecane jest na konstruktorze, ale jeżeli potrzebuję daną klasę np. CurrencyRepository w CurrencyService to jak mam ją inaczej tam podpiąć? Chyba, że coś innego masz na myśli.

  1. Klasa CellWriter - dziwne są te defaultowe metody, nie zwracają Cell, którą tam modyfikujesz - to działa?

Drukuje się poprawnie więc raczej tak. Zauważ, że one działają na obiekcie Row, który ma numerowane komórki jak to Excel. Przekazuje konkretny row oraz numer komórki do której mają być zapisane dane. Po zapisie nie ma potrzeby zwracać jej bo zapis został już dokonany.

1

??? Że nie na konstruktorze? Spotkałem się z informacją, że zalecane jest na konstruktorze, ale jeżeli potrzebuję daną klasę np. CurrencyRepository w CurrencyService to jak mam ją inaczej tam podpiąć? Chyba, że coś innego masz na myśli.

Autowired na polach to zło. Utrudnia testowanie i daje za dużo pokusę wrzucania zależności. Zrób po bożemu konstruktor i normalnie przez niego podawaj

1 użytkowników online, w tym zalogowanych: 0, gości: 1