commit 365ad5a560564220398ba8ab4b900eb1ca26c8cd Author: Dale Mellor Date: Mon Mar 23 11:52:18 2020 +0000 Genesis. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eccf9f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +*~ +*.o +*.lo +*.la +.deps +.libs +ABOUT-NLS +aclocal.m4 +auto-config.h +auto-config.h.in +autom4te.cache +config.log +config.status +configure +curlpp/ +libtool +m4 +makefile +makefile.in +quote/ +stamp-h1 +TAGS +test-driver +trader-desk.pc +trader-desk/trader-desk diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..9def3c1 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,11 @@ +Dale Mellor + + +______________________________________________________________________ +Copyright (c) 2017 Dale Mellor + +Copying and distribution of this file, with or without modification, +are permitted in any medium without royalty provided the copyright +notice and this notice are preserved. This file is offered as-is, +without any warranty. + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..e69de29 diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..b7ec2e5 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,26 @@ +2017-02-10 gettextize + + * configure.ac (AC_CONFIG_FILES): Add po/Makefile.in. + +2017-02-10 gettextize + + * configure.ac (AC_CONFIG_FILES): Add po/Makefile.in. + +2017-02-09 gettextize + + * configure.ac (AM_GNU_GETTEXT_VERSION): Bump to 0.19.4. + +2017-02-09 gettextize + + * configure.ac (AM_GNU_GETTEXT_VERSION): Bump to 0.19.3. + + + + +______________________________________________________________________ +Copyright (c) 2017 Dale Mellor + +Copying and distribution of this file, with or without modification, +are permitted in any medium without royalty provided the copyright +notice and this notice are preserved. This file is offered as-is, +without any warranty. diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..a89d8e5 --- /dev/null +++ b/INSTALL @@ -0,0 +1,25 @@ +* Build instructions + +*** From a GIT checkout + + First, cd into the top-level source directory, then type + + autoreconf --install && ./configure --prefix=$PWD/install && \ + make install + + (you can of course run each piece separately), after which the program will + be installed at install/bin/cosmosis. + + + If you make subsequent changes in the code base, you only need to run + + make && make install + + to have your changes implemented in the installation. + + +*** From a tarball + + Unpack the tar file and cd into the top directory, then type + + ./configure --prefix=$PWD/install && make install diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..f435cf4 --- /dev/null +++ b/NEWS @@ -0,0 +1,13 @@ +2017-11-03 Planned first release to public. + +2007-11-03 Development begins in earnest. + + + +______________________________________________________________________ +Copyright (c) 2017 Dale Mellor + +Copying and distribution of this file, with or without modification, +are permitted in any medium without royalty provided the copyright +notice and this notice are preserved. This file is offered as-is, +without any warranty. diff --git a/README b/README new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/README @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2bfef9 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +> Github is a secondary distribution point for this project; please see +> https://rdmp.org/dmbcs/trader-desk to be sure to see the most up to date +> version and complete details. + +# DMBCS Trader-Desk Application + +> This is a beta release of C++ code designed to be built and run on a +> Gnu/Linux-based system. + +While stock charting applications are ten a penny, it seems like every +time you look at a different stock chart you see a completely different +picture of the situation. Especially when it is your broker: things +always look great until the time you buy in and then you see things are +all really downhill. + +So we want a desk which shows us data we can believe, and allows us to +very quickly view and analyze the data in lots of ways, at lots of time +scales; we want to be live and interactive with our data. + +Specifically, our requirements are + +* Show full, at-a-glance, view of an entire market +* Instantly bring up all the details of any one company +* Have all data (ten years, all markets) on local machine, in a + proper database, for speed +* Application must be lightning fast, we want to pan and + zoom around the data to our heart’s content + * Ultimately we want trading robots which can react to + events super-fast +* Provide plug-in infrastructure for analysis modules, and + ultimately for robot traders + +## The Project + +We are a team of professional C++ Linux programmers and this has been a +personal side project, and thus isn’t really designed for portability or +ease of installation. But we have made an effort, and the application +does include a wizard which will go some way towards getting your database +set up correctly. + +### Prerequisites + +The requirements are specific and quite hard: + +* `gcc 9.3` + * Yep, this is written to C++20 standards with **concepts** and the + **fmt** library in use. +* `fmt` +* `mariadb` +* `dmbcs-market-data-api` +* An account at *AlphaVantage* (free); you will need to acquire an *API + key* from that site and enter it at the bottom of the preferences dialog + box in the `trader-desk` application + +It has only been built on a Debian 10 (current stable) system. The file +`setup-hint.sh` at the root of the distribution is a pseudo-script for +configuring and installing into a clean-built Debian 10 machine. It is +not expected that you would just run this blindly, but use it as a guide +for manually setting your own system up at the command line; in particular +it will inform you how to get the dependencies listed above. + + +## Download + +The `dmbcs-trader-desk` source code is managed with *GIT* (configured with +*autotools*, built with *make* and a good C++20 compiler). Type + +> `git clone http://rdmp.org/dmbcs/trader-desk.git dmbcs-trader-desk` + +at the command line to obtain a copy. + +This repository also comes with some database pre-population data, so that +you will have something interesting to look at as soon as you start the +application running! + +## Documentation + +As per above, build and installation instructions take the form of the +`setup-hint.sh` pseudo-script included with the sources. The +source is about 50% covered by *Doxygen* notes in the header files. + +Real end-user documentation is non-existent right now. You should have +had a look at the video above, and then you will be able to follow your +instincts and find your own way around the application. + +## Contact + +Please click [here](https://rdmp.org/dmbcs/contact) if you wish to send us +a message. + +### Mailing list + +If you would like to receive e-mail notices of matters arising about this +application, you may request this through the contact form above. + +### Contribution to development + +We will happily consider contributions to the source code if you provide +the address of a GIT repository we can pull from, or send a pull request +via Github, and will consider all bug reports and feature requests. + +## Donations + +If you use this application please consider a bitcoin donation if you can. +A small amount informs us that there is interest and that we are providing +a useful service to the community; it will keep us motivated to continue +to make open source software. Donations can be made by bitcoin to the +address `1PWHez4zT2xt6PoyuAwKPJsgRznAKwTtF9`. + + +______________________________________________________________________ +Copyright (c) 2017, 2020 Dale Mellor + +Copying and distribution of this file, with or without modification, +are permitted in any medium without royalty provided the copyright +notice and this notice are preserved. This file is offered as-is, +without any warranty. diff --git a/build-aux/.gitignore b/build-aux/.gitignore new file mode 100644 index 0000000..3efacbc --- /dev/null +++ b/build-aux/.gitignore @@ -0,0 +1,8 @@ +compile +config.guess +config.rpath +config.sub +depcomp +install-sh +ltmain.sh +missing diff --git a/build-aux/gettext.h b/build-aux/gettext.h new file mode 100644 index 0000000..ac4d7d5 --- /dev/null +++ b/build-aux/gettext.h @@ -0,0 +1,287 @@ +/* Convenience header for conditional use of GNU . + Copyright (C) 1995-1998, 2000-2002, 2004-2006, 2009-2011 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#ifndef _LIBGETTEXT_H +#define _LIBGETTEXT_H 1 + +/* NLS can be disabled through the configure --disable-nls option. */ +#if ENABLE_NLS + +/* Get declarations of GNU message catalog functions. */ +# include + +/* You can set the DEFAULT_TEXT_DOMAIN macro to specify the domain used by + the gettext() and ngettext() macros. This is an alternative to calling + textdomain(), and is useful for libraries. */ +# ifdef DEFAULT_TEXT_DOMAIN +# undef gettext +# define gettext(Msgid) \ + dgettext (DEFAULT_TEXT_DOMAIN, Msgid) +# undef ngettext +# define ngettext(Msgid1, Msgid2, N) \ + dngettext (DEFAULT_TEXT_DOMAIN, Msgid1, Msgid2, N) +# endif + +#else + +/* Solaris /usr/include/locale.h includes /usr/include/libintl.h, which + chokes if dcgettext is defined as a macro. So include it now, to make + later inclusions of a NOP. We don't include + as well because people using "gettext.h" will not include , + and also including would fail on SunOS 4, whereas + is OK. */ +#if defined(__sun) +# include +#endif + +/* Many header files from the libstdc++ coming with g++ 3.3 or newer include + , which chokes if dcgettext is defined as a macro. So include + it now, to make later inclusions of a NOP. */ +#if defined(__cplusplus) && defined(__GNUG__) && (__GNUC__ >= 3) +# include +# if (__GLIBC__ >= 2 && !defined __UCLIBC__) || _GLIBCXX_HAVE_LIBINTL_H +# include +# endif +#endif + +/* Disabled NLS. + The casts to 'const char *' serve the purpose of producing warnings + for invalid uses of the value returned from these functions. + On pre-ANSI systems without 'const', the config.h file is supposed to + contain "#define const". */ +# undef gettext +# define gettext(Msgid) ((const char *) (Msgid)) +# undef dgettext +# define dgettext(Domainname, Msgid) ((void) (Domainname), gettext (Msgid)) +# undef dcgettext +# define dcgettext(Domainname, Msgid, Category) \ + ((void) (Category), dgettext (Domainname, Msgid)) +# undef ngettext +# define ngettext(Msgid1, Msgid2, N) \ + ((N) == 1 \ + ? ((void) (Msgid2), (const char *) (Msgid1)) \ + : ((void) (Msgid1), (const char *) (Msgid2))) +# undef dngettext +# define dngettext(Domainname, Msgid1, Msgid2, N) \ + ((void) (Domainname), ngettext (Msgid1, Msgid2, N)) +# undef dcngettext +# define dcngettext(Domainname, Msgid1, Msgid2, N, Category) \ + ((void) (Category), dngettext (Domainname, Msgid1, Msgid2, N)) +# undef textdomain +# define textdomain(Domainname) ((const char *) (Domainname)) +# undef bindtextdomain +# define bindtextdomain(Domainname, Dirname) \ + ((void) (Domainname), (const char *) (Dirname)) +# undef bind_textdomain_codeset +# define bind_textdomain_codeset(Domainname, Codeset) \ + ((void) (Domainname), (const char *) (Codeset)) + +#endif + +/* Prefer gnulib's setlocale override over libintl's setlocale override. */ +#ifdef GNULIB_defined_setlocale +# undef setlocale +# define setlocale rpl_setlocale +#endif + +/* A pseudo function call that serves as a marker for the automated + extraction of messages, but does not call gettext(). The run-time + translation is done at a different place in the code. + The argument, String, should be a literal string. Concatenated strings + and other string expressions won't work. + The macro's expansion is not parenthesized, so that it is suitable as + initializer for static 'char[]' or 'const char[]' variables. */ +#define gettext_noop(String) String + +/* The separator between msgctxt and msgid in a .mo file. */ +#define GETTEXT_CONTEXT_GLUE "\004" + +/* Pseudo function calls, taking a MSGCTXT and a MSGID instead of just a + MSGID. MSGCTXT and MSGID must be string literals. MSGCTXT should be + short and rarely need to change. + The letter 'p' stands for 'particular' or 'special'. */ +#ifdef DEFAULT_TEXT_DOMAIN +# define pgettext(Msgctxt, Msgid) \ + pgettext_aux (DEFAULT_TEXT_DOMAIN, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, LC_MESSAGES) +#else +# define pgettext(Msgctxt, Msgid) \ + pgettext_aux (NULL, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, LC_MESSAGES) +#endif +#define dpgettext(Domainname, Msgctxt, Msgid) \ + pgettext_aux (Domainname, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, LC_MESSAGES) +#define dcpgettext(Domainname, Msgctxt, Msgid, Category) \ + pgettext_aux (Domainname, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, Category) +#ifdef DEFAULT_TEXT_DOMAIN +# define npgettext(Msgctxt, Msgid, MsgidPlural, N) \ + npgettext_aux (DEFAULT_TEXT_DOMAIN, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, MsgidPlural, N, LC_MESSAGES) +#else +# define npgettext(Msgctxt, Msgid, MsgidPlural, N) \ + npgettext_aux (NULL, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, MsgidPlural, N, LC_MESSAGES) +#endif +#define dnpgettext(Domainname, Msgctxt, Msgid, MsgidPlural, N) \ + npgettext_aux (Domainname, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, MsgidPlural, N, LC_MESSAGES) +#define dcnpgettext(Domainname, Msgctxt, Msgid, MsgidPlural, N, Category) \ + npgettext_aux (Domainname, Msgctxt GETTEXT_CONTEXT_GLUE Msgid, Msgid, MsgidPlural, N, Category) + +#ifdef __GNUC__ +__inline +#else +#ifdef __cplusplus +inline +#endif +#endif +static const char * +pgettext_aux (const char *domain, + const char *msg_ctxt_id, const char *msgid, + int category) +{ + const char *translation = dcgettext (domain, msg_ctxt_id, category); + if (translation == msg_ctxt_id) + return msgid; + else + return translation; +} + +#ifdef __GNUC__ +__inline +#else +#ifdef __cplusplus +inline +#endif +#endif +static const char * +npgettext_aux (const char *domain, + const char *msg_ctxt_id, const char *msgid, + const char *msgid_plural, unsigned long int n, + int category) +{ + const char *translation = + dcngettext (domain, msg_ctxt_id, msgid_plural, n, category); + if (translation == msg_ctxt_id || translation == msgid_plural) + return (n == 1 ? msgid : msgid_plural); + else + return translation; +} + +/* The same thing extended for non-constant arguments. Here MSGCTXT and MSGID + can be arbitrary expressions. But for string literals these macros are + less efficient than those above. */ + +#include + +#if (((__GNUC__ >= 3 || __GNUG__ >= 2) && !defined __STRICT_ANSI__) \ + /* || __STDC_VERSION__ >= 199901L */ ) +# define _LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS 1 +#else +# define _LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS 0 +#endif + +#if !_LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS +#include +#endif + +#define pgettext_expr(Msgctxt, Msgid) \ + dcpgettext_expr (NULL, Msgctxt, Msgid, LC_MESSAGES) +#define dpgettext_expr(Domainname, Msgctxt, Msgid) \ + dcpgettext_expr (Domainname, Msgctxt, Msgid, LC_MESSAGES) + +#ifdef __GNUC__ +__inline +#else +#ifdef __cplusplus +inline +#endif +#endif +static const char * +dcpgettext_expr (const char *domain, + const char *msgctxt, const char *msgid, + int category) +{ + size_t msgctxt_len = strlen (msgctxt) + 1; + size_t msgid_len = strlen (msgid) + 1; + const char *translation; +#if _LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS + char msg_ctxt_id[msgctxt_len + msgid_len]; +#else + char buf[1024]; + char *msg_ctxt_id = + (msgctxt_len + msgid_len <= sizeof (buf) + ? buf + : (char *) malloc (msgctxt_len + msgid_len)); + if (msg_ctxt_id != NULL) +#endif + { + memcpy (msg_ctxt_id, msgctxt, msgctxt_len - 1); + msg_ctxt_id[msgctxt_len - 1] = '\004'; + memcpy (msg_ctxt_id + msgctxt_len, msgid, msgid_len); + translation = dcgettext (domain, msg_ctxt_id, category); +#if !_LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS + if (msg_ctxt_id != buf) + free (msg_ctxt_id); +#endif + if (translation != msg_ctxt_id) + return translation; + } + return msgid; +} + +#define npgettext_expr(Msgctxt, Msgid, MsgidPlural, N) \ + dcnpgettext_expr (NULL, Msgctxt, Msgid, MsgidPlural, N, LC_MESSAGES) +#define dnpgettext_expr(Domainname, Msgctxt, Msgid, MsgidPlural, N) \ + dcnpgettext_expr (Domainname, Msgctxt, Msgid, MsgidPlural, N, LC_MESSAGES) + +#ifdef __GNUC__ +__inline +#else +#ifdef __cplusplus +inline +#endif +#endif +static const char * +dcnpgettext_expr (const char *domain, + const char *msgctxt, const char *msgid, + const char *msgid_plural, unsigned long int n, + int category) +{ + size_t msgctxt_len = strlen (msgctxt) + 1; + size_t msgid_len = strlen (msgid) + 1; + const char *translation; +#if _LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS + char msg_ctxt_id[msgctxt_len + msgid_len]; +#else + char buf[1024]; + char *msg_ctxt_id = + (msgctxt_len + msgid_len <= sizeof (buf) + ? buf + : (char *) malloc (msgctxt_len + msgid_len)); + if (msg_ctxt_id != NULL) +#endif + { + memcpy (msg_ctxt_id, msgctxt, msgctxt_len - 1); + msg_ctxt_id[msgctxt_len - 1] = '\004'; + memcpy (msg_ctxt_id + msgctxt_len, msgid, msgid_len); + translation = dcngettext (domain, msg_ctxt_id, msgid_plural, n, category); +#if !_LIBGETTEXT_HAVE_VARIABLE_SIZE_ARRAYS + if (msg_ctxt_id != buf) + free (msg_ctxt_id); +#endif + if (!(translation == msg_ctxt_id || translation == msgid_plural)) + return translation; + } + return (n == 1 ? msgid : msgid_plural); +} + +#endif /* _LIBGETTEXT_H */ diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..c4afdb5 --- /dev/null +++ b/configure.ac @@ -0,0 +1,91 @@ +# -*- Autoconf -*- + + +# Copyright (c) 2017, 2020 Dale Mellor +# +# This file is part of the trader-desk package. +# +# The trader-desk package is free software: you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# The trader-desk package is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + + +# Process this file with autoconf to produce a configure script. + + +AC_PREREQ(2.69) +AC_INIT([trader-desk], [0.1], [dale@rdmp.org]) +AC_CONFIG_AUX_DIR([build-aux]) +AM_INIT_AUTOMAKE([silent-rules subdir-objects]) +AM_SILENT_RULES([yes]) +AC_CONFIG_SRCDIR([trader-desk/trader-desk.cc]) +AC_CONFIG_HEADERS([trader-desk/auto-config.h]) +AC_CONFIG_MACRO_DIRS([m4]) + +# Checks for programs. +AC_PROG_CC +AC_GNU_SOURCE +AC_PROG_CXX +AC_PROG_AWK +AC_PROG_CPP +AC_PROG_INSTALL +AC_PROG_LN_S +AC_PROG_MAKE_SET +AC_PROG_LIBTOOL +AM_GNU_GETTEXT([external]) +AM_GNU_GETTEXT_VERSION([0.19.3]) + +AC_CHECK_PROGS([mysql_config], [mariadb_config mysql_config], [no]) +if [[ "x$mysql_config" == "xno" ]]; then + AC_MSG_FAILURE([cannot find MySQL-type client libraries]) +fi +if [[ "x$mysql_config" == "xmysql_config" ]]; then + AC_SUBST([HAVE_MYSQL], [1]) +else + AC_SUBST([HAVE_MYSQL], [0]) +fi +if [[ "x$mysql_config" == "xmariadb_config" ]]; then + AC_SUBST([HAVE_MARIADB], [1]) +else + AC_SUBST([HAVE_MARIADB], [0]) +fi + +# Checks for libraries. +PKG_CHECK_MODULES([gtk_config], [gtkmm-3.0 gthread-2.0 dmbcs-market-data-api fmt]) +gtk_config_CFLAGS="${pkg_cv_gtk_config_CFLAGS} `${mysql_config} --include`" +gtk_config_LIBS="${pkg_cv_gtk_config_LIBS} `${mysql_config} --libs`" + +# Checks for header files. +AC_CHECK_HEADERS([libintl.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_CHECK_HEADER_STDBOOL +AC_C_INLINE +AC_TYPE_SIZE_T +AC_TYPE_UINT32_T + +# Checks for library functions. +AC_FUNC_MKTIME +AC_FUNC_STRTOD +AC_CHECK_FUNCS([floor localtime_r pow setlocale sqrt]) + +# Check for optional database pre-population data. +AM_CONDITIONAL([database_data], [test -f data.sql.xz]) + + +AC_CONFIG_FILES([trader-desk.pc + makefile + po/Makefile.in + trader-desk/makefile]) +dnl doc/makefile + +AC_OUTPUT diff --git a/data.sql.xz b/data.sql.xz new file mode 100644 index 0000000..8295e69 Binary files /dev/null and b/data.sql.xz differ diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..776f311 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1,18 @@ +trader-desk.aux +trader-desk.cp +trader-desk.dvi +trader-desk.fn +trader-desk.ky +trader-desk.log +trader-desk.pg +trader-desk.tmp +trader-desk.toc +trader-desk.tp +trader-desk.vr +trader-desk.bbl +trader-desk.blg +trader-desk.info +trader-desk.man +trader-desk.html +trader-desk.cps +trader-desk.pdf diff --git a/doc/detailed-analysis.png b/doc/detailed-analysis.png new file mode 100644 index 0000000..62e8d58 Binary files /dev/null and b/doc/detailed-analysis.png differ diff --git a/doc/detailed-analysis.xcf b/doc/detailed-analysis.xcf new file mode 100644 index 0000000..0a6fb38 Binary files /dev/null and b/doc/detailed-analysis.xcf differ diff --git a/doc/fdl.texi b/doc/fdl.texi new file mode 100644 index 0000000..9c3bbe5 --- /dev/null +++ b/doc/fdl.texi @@ -0,0 +1,505 @@ +@c The GNU Free Documentation License. +@center Version 1.3, 3 November 2008 + +@c This file is intended to be included within another document, +@c hence no sectioning command or @node. + +@display +Copyright @copyright{} 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc. +@uref{http://fsf.org/} + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. +@end display + +@enumerate 0 +@item +PREAMBLE + +The purpose of this License is to make a manual, textbook, or other +functional and useful document @dfn{free} in the sense of freedom: to +assure everyone the effective freedom to copy and redistribute it, +with or without modifying it, either commercially or noncommercially. +Secondarily, this License preserves for the author and publisher a way +to get credit for their work, while not being considered responsible +for modifications made by others. + +This License is a kind of ``copyleft'', which means that derivative +works of the document must themselves be free in the same sense. It +complements the GNU General Public License, which is a copyleft +license designed for free software. + +We have designed this License in order to use it for manuals for free +software, because free software needs free documentation: a free +program should come with manuals providing the same freedoms that the +software does. But this License is not limited to software manuals; +it can be used for any textual work, regardless of subject matter or +whether it is published as a printed book. We recommend this License +principally for works whose purpose is instruction or reference. + +@item +APPLICABILITY AND DEFINITIONS + +This License applies to any manual or other work, in any medium, that +contains a notice placed by the copyright holder saying it can be +distributed under the terms of this License. Such a notice grants a +world-wide, royalty-free license, unlimited in duration, to use that +work under the conditions stated herein. The ``Document'', below, +refers to any such manual or work. Any member of the public is a +licensee, and is addressed as ``you''. You accept the license if you +copy, modify or distribute the work in a way requiring permission +under copyright law. + +A ``Modified Version'' of the Document means any work containing the +Document or a portion of it, either copied verbatim, or with +modifications and/or translated into another language. + +A ``Secondary Section'' is a named appendix or a front-matter section +of the Document that deals exclusively with the relationship of the +publishers or authors of the Document to the Document's overall +subject (or to related matters) and contains nothing that could fall +directly within that overall subject. (Thus, if the Document is in +part a textbook of mathematics, a Secondary Section may not explain +any mathematics.) The relationship could be a matter of historical +connection with the subject or with related matters, or of legal, +commercial, philosophical, ethical or political position regarding +them. + +The ``Invariant Sections'' are certain Secondary Sections whose titles +are designated, as being those of Invariant Sections, in the notice +that says that the Document is released under this License. If a +section does not fit the above definition of Secondary then it is not +allowed to be designated as Invariant. The Document may contain zero +Invariant Sections. If the Document does not identify any Invariant +Sections then there are none. + +The ``Cover Texts'' are certain short passages of text that are listed, +as Front-Cover Texts or Back-Cover Texts, in the notice that says that +the Document is released under this License. A Front-Cover Text may +be at most 5 words, and a Back-Cover Text may be at most 25 words. + +A ``Transparent'' copy of the Document means a machine-readable copy, +represented in a format whose specification is available to the +general public, that is suitable for revising the document +straightforwardly with generic text editors or (for images composed of +pixels) generic paint programs or (for drawings) some widely available +drawing editor, and that is suitable for input to text formatters or +for automatic translation to a variety of formats suitable for input +to text formatters. A copy made in an otherwise Transparent file +format whose markup, or absence of markup, has been arranged to thwart +or discourage subsequent modification by readers is not Transparent. +An image format is not Transparent if used for any substantial amount +of text. A copy that is not ``Transparent'' is called ``Opaque''. + +Examples of suitable formats for Transparent copies include plain +ASCII without markup, Texinfo input format, La@TeX{} input +format, SGML or XML using a publicly available +DTD, and standard-conforming simple HTML, +PostScript or PDF designed for human modification. Examples +of transparent image formats include PNG, XCF and +JPG@. Opaque formats include proprietary formats that can be +read and edited only by proprietary word processors, SGML or +XML for which the DTD and/or processing tools are +not generally available, and the machine-generated HTML, +PostScript or PDF produced by some word processors for +output purposes only. + +The ``Title Page'' means, for a printed book, the title page itself, +plus such following pages as are needed to hold, legibly, the material +this License requires to appear in the title page. For works in +formats which do not have any title page as such, ``Title Page'' means +the text near the most prominent appearance of the work's title, +preceding the beginning of the body of the text. + +The ``publisher'' means any person or entity that distributes copies +of the Document to the public. + +A section ``Entitled XYZ'' means a named subunit of the Document whose +title either is precisely XYZ or contains XYZ in parentheses following +text that translates XYZ in another language. (Here XYZ stands for a +specific section name mentioned below, such as ``Acknowledgements'', +``Dedications'', ``Endorsements'', or ``History''.) To ``Preserve the Title'' +of such a section when you modify the Document means that it remains a +section ``Entitled XYZ'' according to this definition. + +The Document may include Warranty Disclaimers next to the notice which +states that this License applies to the Document. These Warranty +Disclaimers are considered to be included by reference in this +License, but only as regards disclaiming warranties: any other +implication that these Warranty Disclaimers may have is void and has +no effect on the meaning of this License. + +@item +VERBATIM COPYING + +You may copy and distribute the Document in any medium, either +commercially or noncommercially, provided that this License, the +copyright notices, and the license notice saying this License applies +to the Document are reproduced in all copies, and that you add no other +conditions whatsoever to those of this License. You may not use +technical measures to obstruct or control the reading or further +copying of the copies you make or distribute. However, you may accept +compensation in exchange for copies. If you distribute a large enough +number of copies you must also follow the conditions in section 3. + +You may also lend copies, under the same conditions stated above, and +you may publicly display copies. + +@item +COPYING IN QUANTITY + +If you publish printed copies (or copies in media that commonly have +printed covers) of the Document, numbering more than 100, and the +Document's license notice requires Cover Texts, you must enclose the +copies in covers that carry, clearly and legibly, all these Cover +Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on +the back cover. Both covers must also clearly and legibly identify +you as the publisher of these copies. The front cover must present +the full title with all words of the title equally prominent and +visible. You may add other material on the covers in addition. +Copying with changes limited to the covers, as long as they preserve +the title of the Document and satisfy these conditions, can be treated +as verbatim copying in other respects. + +If the required texts for either cover are too voluminous to fit +legibly, you should put the first ones listed (as many as fit +reasonably) on the actual cover, and continue the rest onto adjacent +pages. + +If you publish or distribute Opaque copies of the Document numbering +more than 100, you must either include a machine-readable Transparent +copy along with each Opaque copy, or state in or with each Opaque copy +a computer-network location from which the general network-using +public has access to download using public-standard network protocols +a complete Transparent copy of the Document, free of added material. +If you use the latter option, you must take reasonably prudent steps, +when you begin distribution of Opaque copies in quantity, to ensure +that this Transparent copy will remain thus accessible at the stated +location until at least one year after the last time you distribute an +Opaque copy (directly or through your agents or retailers) of that +edition to the public. + +It is requested, but not required, that you contact the authors of the +Document well before redistributing any large number of copies, to give +them a chance to provide you with an updated version of the Document. + +@item +MODIFICATIONS + +You may copy and distribute a Modified Version of the Document under +the conditions of sections 2 and 3 above, provided that you release +the Modified Version under precisely this License, with the Modified +Version filling the role of the Document, thus licensing distribution +and modification of the Modified Version to whoever possesses a copy +of it. In addition, you must do these things in the Modified Version: + +@enumerate A +@item +Use in the Title Page (and on the covers, if any) a title distinct +from that of the Document, and from those of previous versions +(which should, if there were any, be listed in the History section +of the Document). You may use the same title as a previous version +if the original publisher of that version gives permission. + +@item +List on the Title Page, as authors, one or more persons or entities +responsible for authorship of the modifications in the Modified +Version, together with at least five of the principal authors of the +Document (all of its principal authors, if it has fewer than five), +unless they release you from this requirement. + +@item +State on the Title page the name of the publisher of the +Modified Version, as the publisher. + +@item +Preserve all the copyright notices of the Document. + +@item +Add an appropriate copyright notice for your modifications +adjacent to the other copyright notices. + +@item +Include, immediately after the copyright notices, a license notice +giving the public permission to use the Modified Version under the +terms of this License, in the form shown in the Addendum below. + +@item +Preserve in that license notice the full lists of Invariant Sections +and required Cover Texts given in the Document's license notice. + +@item +Include an unaltered copy of this License. + +@item +Preserve the section Entitled ``History'', Preserve its Title, and add +to it an item stating at least the title, year, new authors, and +publisher of the Modified Version as given on the Title Page. If +there is no section Entitled ``History'' in the Document, create one +stating the title, year, authors, and publisher of the Document as +given on its Title Page, then add an item describing the Modified +Version as stated in the previous sentence. + +@item +Preserve the network location, if any, given in the Document for +public access to a Transparent copy of the Document, and likewise +the network locations given in the Document for previous versions +it was based on. These may be placed in the ``History'' section. +You may omit a network location for a work that was published at +least four years before the Document itself, or if the original +publisher of the version it refers to gives permission. + +@item +For any section Entitled ``Acknowledgements'' or ``Dedications'', Preserve +the Title of the section, and preserve in the section all the +substance and tone of each of the contributor acknowledgements and/or +dedications given therein. + +@item +Preserve all the Invariant Sections of the Document, +unaltered in their text and in their titles. Section numbers +or the equivalent are not considered part of the section titles. + +@item +Delete any section Entitled ``Endorsements''. Such a section +may not be included in the Modified Version. + +@item +Do not retitle any existing section to be Entitled ``Endorsements'' or +to conflict in title with any Invariant Section. + +@item +Preserve any Warranty Disclaimers. +@end enumerate + +If the Modified Version includes new front-matter sections or +appendices that qualify as Secondary Sections and contain no material +copied from the Document, you may at your option designate some or all +of these sections as invariant. To do this, add their titles to the +list of Invariant Sections in the Modified Version's license notice. +These titles must be distinct from any other section titles. + +You may add a section Entitled ``Endorsements'', provided it contains +nothing but endorsements of your Modified Version by various +parties---for example, statements of peer review or that the text has +been approved by an organization as the authoritative definition of a +standard. + +You may add a passage of up to five words as a Front-Cover Text, and a +passage of up to 25 words as a Back-Cover Text, to the end of the list +of Cover Texts in the Modified Version. Only one passage of +Front-Cover Text and one of Back-Cover Text may be added by (or +through arrangements made by) any one entity. If the Document already +includes a cover text for the same cover, previously added by you or +by arrangement made by the same entity you are acting on behalf of, +you may not add another; but you may replace the old one, on explicit +permission from the previous publisher that added the old one. + +The author(s) and publisher(s) of the Document do not by this License +give permission to use their names for publicity for or to assert or +imply endorsement of any Modified Version. + +@item +COMBINING DOCUMENTS + +You may combine the Document with other documents released under this +License, under the terms defined in section 4 above for modified +versions, provided that you include in the combination all of the +Invariant Sections of all of the original documents, unmodified, and +list them all as Invariant Sections of your combined work in its +license notice, and that you preserve all their Warranty Disclaimers. + +The combined work need only contain one copy of this License, and +multiple identical Invariant Sections may be replaced with a single +copy. If there are multiple Invariant Sections with the same name but +different contents, make the title of each such section unique by +adding at the end of it, in parentheses, the name of the original +author or publisher of that section if known, or else a unique number. +Make the same adjustment to the section titles in the list of +Invariant Sections in the license notice of the combined work. + +In the combination, you must combine any sections Entitled ``History'' +in the various original documents, forming one section Entitled +``History''; likewise combine any sections Entitled ``Acknowledgements'', +and any sections Entitled ``Dedications''. You must delete all +sections Entitled ``Endorsements.'' + +@item +COLLECTIONS OF DOCUMENTS + +You may make a collection consisting of the Document and other documents +released under this License, and replace the individual copies of this +License in the various documents with a single copy that is included in +the collection, provided that you follow the rules of this License for +verbatim copying of each of the documents in all other respects. + +You may extract a single document from such a collection, and distribute +it individually under this License, provided you insert a copy of this +License into the extracted document, and follow this License in all +other respects regarding verbatim copying of that document. + +@item +AGGREGATION WITH INDEPENDENT WORKS + +A compilation of the Document or its derivatives with other separate +and independent documents or works, in or on a volume of a storage or +distribution medium, is called an ``aggregate'' if the copyright +resulting from the compilation is not used to limit the legal rights +of the compilation's users beyond what the individual works permit. +When the Document is included in an aggregate, this License does not +apply to the other works in the aggregate which are not themselves +derivative works of the Document. + +If the Cover Text requirement of section 3 is applicable to these +copies of the Document, then if the Document is less than one half of +the entire aggregate, the Document's Cover Texts may be placed on +covers that bracket the Document within the aggregate, or the +electronic equivalent of covers if the Document is in electronic form. +Otherwise they must appear on printed covers that bracket the whole +aggregate. + +@item +TRANSLATION + +Translation is considered a kind of modification, so you may +distribute translations of the Document under the terms of section 4. +Replacing Invariant Sections with translations requires special +permission from their copyright holders, but you may include +translations of some or all Invariant Sections in addition to the +original versions of these Invariant Sections. You may include a +translation of this License, and all the license notices in the +Document, and any Warranty Disclaimers, provided that you also include +the original English version of this License and the original versions +of those notices and disclaimers. In case of a disagreement between +the translation and the original version of this License or a notice +or disclaimer, the original version will prevail. + +If a section in the Document is Entitled ``Acknowledgements'', +``Dedications'', or ``History'', the requirement (section 4) to Preserve +its Title (section 1) will typically require changing the actual +title. + +@item +TERMINATION + +You may not copy, modify, sublicense, or distribute the Document +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense, or distribute it is void, and +will automatically terminate your rights under this License. + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, receipt of a copy of some or all of the same material does +not give you any rights to use it. + +@item +FUTURE REVISIONS OF THIS LICENSE + +The Free Software Foundation may publish new, revised versions +of the GNU Free Documentation License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. See +@uref{http://www.gnu.org/copyleft/}. + +Each version of the License is given a distinguishing version number. +If the Document specifies that a particular numbered version of this +License ``or any later version'' applies to it, you have the option of +following the terms and conditions either of that specified version or +of any later version that has been published (not as a draft) by the +Free Software Foundation. If the Document does not specify a version +number of this License, you may choose any version ever published (not +as a draft) by the Free Software Foundation. If the Document +specifies that a proxy can decide which future versions of this +License can be used, that proxy's public statement of acceptance of a +version permanently authorizes you to choose that version for the +Document. + +@item +RELICENSING + +``Massive Multiauthor Collaboration Site'' (or ``MMC Site'') means any +World Wide Web server that publishes copyrightable works and also +provides prominent facilities for anybody to edit those works. A +public wiki that anybody can edit is an example of such a server. A +``Massive Multiauthor Collaboration'' (or ``MMC'') contained in the +site means any set of copyrightable works thus published on the MMC +site. + +``CC-BY-SA'' means the Creative Commons Attribution-Share Alike 3.0 +license published by Creative Commons Corporation, a not-for-profit +corporation with a principal place of business in San Francisco, +California, as well as future copyleft versions of that license +published by that same organization. + +``Incorporate'' means to publish or republish a Document, in whole or +in part, as part of another Document. + +An MMC is ``eligible for relicensing'' if it is licensed under this +License, and if all works that were first published under this License +somewhere other than this MMC, and subsequently incorporated in whole +or in part into the MMC, (1) had no cover texts or invariant sections, +and (2) were thus incorporated prior to November 1, 2008. + +The operator of an MMC Site may republish an MMC contained in the site +under CC-BY-SA on the same site at any time before August 1, 2009, +provided the MMC is eligible for relicensing. + +@end enumerate + +@page +@heading ADDENDUM: How to use this License for your documents + +To use this License in a document you have written, include a copy of +the License in the document and put the following copyright and +license notices just after the title page: + +@smallexample +@group + Copyright (C) @var{year} @var{your name}. + Permission is granted to copy, distribute and/or modify this document + under the terms of the GNU Free Documentation License, Version 1.3 + or any later version published by the Free Software Foundation; + with no Invariant Sections, no Front-Cover Texts, and no Back-Cover + Texts. A copy of the license is included in the section entitled ``GNU + Free Documentation License''. +@end group +@end smallexample + +If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, +replace the ``with@dots{}Texts.''@: line with this: + +@smallexample +@group + with the Invariant Sections being @var{list their titles}, with + the Front-Cover Texts being @var{list}, and with the Back-Cover Texts + being @var{list}. +@end group +@end smallexample + +If you have Invariant Sections without Cover Texts, or some other +combination of the three, merge those two alternatives to suit the +situation. + +If your document contains nontrivial examples of program code, we +recommend releasing these examples in parallel under your choice of +free software license, such as the GNU General Public License, +to permit their use in free software. + +@c Local Variables: +@c ispell-local-pdict: "ispell-dict" +@c End: diff --git a/doc/grid.png b/doc/grid.png new file mode 100644 index 0000000..e841221 Binary files /dev/null and b/doc/grid.png differ diff --git a/doc/grid.xcf b/doc/grid.xcf new file mode 100644 index 0000000..008a64b Binary files /dev/null and b/doc/grid.xcf differ diff --git a/doc/makefile.am b/doc/makefile.am new file mode 100644 index 0000000..44211da --- /dev/null +++ b/doc/makefile.am @@ -0,0 +1,24 @@ +man1_MANS = trader-desk.man + +info_INFO = trader-desk.info + +pkgdata_DATA = trader-desk.pdf trader-desk.html + +trader-desk : trader-desk.texinfo trader-desk.css + makeinfo --html --css-include=trader-desk.css $< + cp {detailed-analysis,grid,preferences}.png trader-desk + +trader-desk.html : trader-desk.texinfo trader-desk.css + makeinfo --html --no-split --no-headers --css-include=trader-desk.css $< + +trader-desk.pdf : trader-desk.texinfo + makeinfo --pdf --no-headers $< + +trader-desk.info : trader-desk.texinfo + makeinfo $< + +CLEANFILES = trader-desk.html trader-desk.info trader-desk.pdf \ + trader-desk.aux trader-desk.bbl trader-desk.blg trader-desk.cp \ + trader-desk.cps trader-desk.dvi trader-desk.fn \ + trader-desk.ky trader-desk.log trader-desk.pg trader-desk.tmp \ + trader-desk.toc trader-desk.tp trader-desk.vr diff --git a/doc/preferences.png b/doc/preferences.png new file mode 100644 index 0000000..be09b3b Binary files /dev/null and b/doc/preferences.png differ diff --git a/doc/preferences.xcf b/doc/preferences.xcf new file mode 100644 index 0000000..8fd937d Binary files /dev/null and b/doc/preferences.xcf differ diff --git a/doc/trader-desk.css b/doc/trader-desk.css new file mode 100644 index 0000000..d2dc7db --- /dev/null +++ b/doc/trader-desk.css @@ -0,0 +1,5 @@ +body { max-width: 800px; + font-family: times; + text-align: justify; + margin: 0 auto; + padding: 0 0.5em; } diff --git a/doc/trader-desk.texinfo b/doc/trader-desk.texinfo new file mode 100644 index 0000000..48550cd --- /dev/null +++ b/doc/trader-desk.texinfo @@ -0,0 +1,1025 @@ +\input texinfo @c -*-texinfo-*- +@c %**start of header +@setfilename trader-desk.info +@settitle Trader Desk +@c %**end of header +@copying +Trader Desk + +Copyright @copyright{} 2017 The @emph{trader-desk} developers + +@quotation +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.3 or +any later version published by the Free Software Foundation; with no +Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A +copy of the license is included in the section entitled ``GNU Free +Documentation License''. + +A copy of the license is also available from the Free Software +Foundation Web site at @url{http://www.gnu.org/licenses/fdl.html}. + +@end quotation +@end copying + +@titlepage +@title Trader Desk 0.1 +@subtitle Trading robot developer environment +@author Dale Mellor +@page +@vskip 0pt plus 1filll +@insertcopying +@end titlepage + +@ifnottex +@node Top, Foreword, (dir), (dir) +@top Trader Desk 0.1 + +@insertcopying +@end ifnottex + +@c Generate the nodes for this menu with `C-c C-u C-m'. +@menu +* Foreword:: +* Introduction:: +* Installation guide:: +* User Guide:: Invoking trader-desk. +* Algorithms:: +* Developer:: +* Time Line:: +* Copying This Manual:: +* Index:: +@end menu + +@c Update all node entries with `C-c C-u C-n'. +@c Insert new nodes with `C-c C-c n'. +@node Foreword, User Guide, Top, Top +@chapter Foreword + +@section Set up to Fail + +@subsection The Evil That Men Do + +There are many things wrong with the world. The worst three, in our +opinion from our haughty point of view in the West, are third-world food +shortages, the buy-to-let property industry, and the global stock +exchanges. The first problem is difficult, but the latter involve able +people sitting on their back-sides while actively screwing other people, +able or not, out of their own money, contributing nothing to the useful +output of any economy. At least the latter sin offers the prospect for +little people (like us) to shamefully bury their scruples and suckle at +the nipple of big business, and take just a tiny amount of glut for +themselves. Or, more probably, not. + +For we are the worst traders on Earth. It all started when times were +roaring, before the financial crisis and the fall of @emph{big} banks: +one day we bought shares for the first time, sold them later that same +day, and made a hundred pounds in profit there and then. Could life get +simpler? Unfortunately, such times have never returned, and over the +years, we have come to realize that the only way to make money is to +invest long-term in a solid (i.e. massive) company, and you will +hopefully do better than the meagre interest on a bank savings account. +Or maybe not. + +@cindex financial crisis +Unfortunately, after being burned badly by the financial crisis and lost +a lot of money (for us), we've been unable to resist the urge to try to +make a quick buck here and there, and, though we no longer dabble in the +folly of amateur day-trading, still play at trying to out-fox the market +and make a quick profit: buying one day in the hope that the price will +go up in the next few days to be able to pull a quick profit; it rarely +does. + +Being not very good traders, we've never bothered doing any actual +research in the companies we've bought in to, but go on historical +market data alone to decide where our next gamble will be. Thus, in +those years of trading we have found the need to assimilate market data +in a local database so that we can explore at will, quickly. We also +found the need to visualize the entire market, as well as explore +individual positions interactively and with great flexibility of +presentation. Finally, we have the desire to build us an artificial +intelligence to inform our trading decisions, based on the collected +historical market data. + +Applications and widgets abound which display stock market data every +which way you can, and each way seems to show the data in a different +light (a stock looks good to buy one minute, but hopeless when viewed +another way in a different application). This application aims to allow +for a more interactive exploration of market data, a maximal overview of +the whole market, and to provide a platform for artificial intelligence +algorithms to be developed to identify good buys and sells. + +@subsection Trading (don't)! + +There's a simple reason why you can't out-fox the markets: if a perfect +algorithm existed everyone would use it and nobody would win. As it is, +the banks and large agencies have far more resources than you to develop +good (very, very, good) algorithms, to the extent that if they ever find +out what algorithm you are using they will use the knowledge against +you: work out what your next move will likely be, and move to capitalize +on your mis-fortune. + +The markets are like a very complex game of chess. It is warfare. +Dog-eat-dog warfare. It is your own real, hard-won, money that you will +lose when you are defeated in battle. + +For this reason there is no point in us telling you the secret to our +(lack of) success. Nor is there any point in you taking anyone else's +advice. In fact, the large agencies are apt to give out lots of advice +to investors knowing how the advice itself will affect the markets and +moving to capitalize themselves. Never bother following @emph{The +Motley Fool}. Wish we didn't have to mention them. For this reason +@code{trader-desk} does not come with any trading strategies built in. +That would just be us shooting ourselves in the foot. + +The only chance you have of success is by developing a unique strategy +of your own and keeping it to yourself. The ultimate aim of +@code{trader-desk} is to help you to develop that strategy, and to +implement it in code. + +@subsection Caveat Emptor + +What you do in the stock markets is entirely up to you. We've already +tried to dissuade you from trying to play the game and get rich quick. +If you do, and you use this application, that's your choice. But your +ultimate actions are nothing to do with us. We hereby divulge ourselves +of all responsibility for your future well-being through use of this +application (or otherwise, actually). + +@section The @code{trader-desk} Application + +@cindex trader-desk +This introduction to the @code{trader-desk} application has been +deliberately meaty. We've tried our best to persuade you that using +this application is not a good idea, for the reasons given above. +However, if you've come this far and we haven't frightened you away, +welcome. We'll lighten the tone from now on, and get to thinking +positive! + +@contents + +@node Introduction, Installation guide, Foreword, Top +@chapter Introduction + +This manual pertains to pre-release version 0.1 of the +@code{trader-desk} application. Despite the lowness of this number +and the fact that it is the very first release, do not underestimate +the amount of effort which has already been invested in this project! + +@cindex alpha software +First, understand that this is the very first release of untested +alpha-grade software. It has some arbitrary limitations (availability +of market data, UK-centric outlook, lack of customizability), and some +idiosyncrasies that will be mildly annoying. Read the @ref{Time Line} +chapter for further information on the current state and direction of +the code base. + +@cindex bug reporting +To report bugs or make contact with the authors please go to +@url{http://rdmp.org/trader-desk/contact}. + +We take such pride in our work that hopefully there aren't any hard bugs +as such: ones which cause the program to shut down or become terminally +unresponsive, or corrupt the local database. But no guarantees. + +@node Installation guide, User Guide, Foreword, Top +@chapter Installation Guide + +The software has been successfully installed on Debian Jessie (stable), +Stretch (testing), Ubuntu 16.04 LTS, Ubuntu 17.04 (all Debian derivatives) +and Fedora 25 Workstation (a Red Hat derivative). We've also tried to +use Scientific Linux 7, but that is so far behind the curve that +@code{trader-desk} cannot be compiled here. + +The build is actually a very standard @emph{autotools} procedure: +@code{./configure --with-database-password=xxx; make; make install;}, +hence it should be possible to build on most POSIXish operating systems. +Instructions below are for the select specific Gnu/Linux distributions +mentioned above, and may need to be adapted for other systems. + +@menu +* System Preparation:: +* Database Preparation:: +* Trader-Desk Installation:: +* Issues:: +@end menu + +@node System Preparation, , Installation guide, Installation guide +@section System Preparation + +At the command line (you will need @code{sudo} privilege on the system, +and may be prompted -- just once -- for your login password). + +@enumerate + +@item Fedora 25 Workstation +@example +sudo yum install mariadb mariadb-server mariadb-devel gettext-devel \ + patch gcc-c++ libcurl-devel gtkmm24(-devel?) +sudo systemctl start mariadb +@end example + +@item Debian stable (Jessie) +@example +sudo apt install build-essential mariadb-client mariadb-server \ + libmariadb-client-lgpl-dev gettext pkg-config curl \ + gtkmm-2.4 libcurl4-openssl-dev +@end example + + +@item Debian testing (Stretch) +@example +sudo apt install build-essential mariadb-client mariadb-server \ + libmariadbclient-dev gettext pkg-config curl +@end example + + +@item Ubuntu 16.04 +@example +sudo apt install autoconf autopoint libtool mariadb-client \ + mariadb-server libmariadb-client-lgpl-dev gtkmm-2.4 \ + libcurl4-openssl-dev curl +@end example + + +@item Ubuntu 17.04 +@example +sudo apt install autoconf autopoint libtool mariadb-client \ + mariadb-server libmariadbclient-dev gtkmm-2.4 \ + libcurl4-openssl-dev curl +@end example + + +@end enumerate + +@node Database Preparation, , System Preparation, Installation guide +@section Database Preparation + +On all systems, at the command-line: + +@example +sudo mysql -u root +>> create user trader_desk@@localhost; +>> grant all privileges on *.* to trader_desk@@localhost; +>> exit; +mysql -u trader_desk +>> create database companies; +>> exit; +@end example + +@node Trader-Desk Installation, , Database Preparation, Installation guide +@section Trader-Desk Installation + +On all systems, at the command-line: + +@example +curl https://rdmp.org:9443/trader-desk/trader-desk-0.1.tar.gz | tar xzf - +cd trader-desk-0.1 +cp trader-desk/example-config.h trader-desk/config.h + +[ On Fedora only: edit trader-desk/config.h and change the + DATABASE_SOCKET to /var/lib/mysql/mysql.sock. ] + +./configure --with-database-password= --without-boost +make +sudo make install +@end example + +@subsection Variations + +The above instructions are deliberately the minimum you need to get +going. However you may need to vary these for your installation: + +@enumerate +@item + +Use a @code{--prefix} option on the @code{configure} line to specify +where to install the @code{trader-desk} application. You would need to +do this if you don't have @code{sudo} access (the default is to install +under @code{/usr/local}). + +@item + +Set a password on the @code{trader_desk} database account. Also, if the +@code{root} account has a password set, this will need adding to the +@code{mysql -u root} line. + +@item + +Make customizations of the @code{trader-desk/config.h} file. For +example, extend the date range for which market prices are initially +obtained. + +@end enumerate + + +@node Issues, , Trader-Desk Installation, Installation guide +@section Issues + +If you are using Ubuntu, the `Update' menu does not become inactive when +a single company is in the main display; using the items on this menu +when a single chart is displayed in the main window WILL cause the +program to crash. + +@node User Guide, Algorithms, Installation guide, Top +@chapter User Guide + +@menu +* Getting Started:: Running the program +@end menu + + +@node Getting Started, , User Guide, User Guide +@section Getting Started + +@cindex Running the program +@example +trader-desk [--version | --help] +@end example + +@cindex version +Unless you are simply looking to find out the version of the installed +program (using the --version option), simply running +@code{trader-desk} will start the program and produce a new window on +the screen. + +As soon as the program starts you will be presented with a number of +markets in which you may take an interest. Double-click on one of +these markets. It will take some time to ingest a few years' data for +the entire market; be patient. + +Now, maximize the window, enjoy your overview of recent market +movements, click on one of the components (companies), and move the +mouse around... + + +@menu +* Main screen:: +* Detailed analysis:: +* Preferences:: +* Menus:: +@end menu + + +@node Main screen, Detailed analysis, Getting Started, Getting Started +@section The Main Screen + +This screen shows the last 50 days' prices for all components in a +market, so that you can see at a glance which stocks are overall up and +which are overall down, and, more to the point, which are turning. + +The menu is fully serviceable on this screen: data from other markets +can be obtained from the `File' menu, the components of a different +market can be shown from the `Display' menu, and the most up to date +data can be obtained for the current market from the `Market' menu. + +Finally, to analyze a company's stocks in more detail, simply click on +the appropriate chart. + +@image{grid,,,Screen with thumb-nail chart for every company,png} + + +@node Detailed analysis, Preferences, Main screen, Getting Started +@section The Detailed Analysis + +This screen shows the price data for a single company, and the user can +use the mouse to change the nature of the display, show precise stock +values at arbitrary times, and indicate a rectangular region across +which differential values are shown. + +Note that the window as you see it may differ slightly from that shown +below due to cosmetic thematic variations in the appearance of +workstation windowing systems. + +@image{detailed-analysis,,,View of window with numbered place markers,png} + +@enumerate + +@item +Click on these arrow buttons to cycle through the companies in the +current market in alphabetical order. Once the last company is reached +the cycle will wrap around to the first one, and similarly if cycling +backwards from the first item. + +@item +This panel serves both to indicate which company's data are being +displayed in the chart, and to allow the user to select an arbitrary +company in the current market: start typing here and a drop-down box +will appear with possible completions of company names. Once selected, +press the ENTER key to load that company's data. + +@item +This drop-down box will show all open positions you have in the present +market (if any). Selecting one will: load the data for that company, +move the shares scale to indicate the number of shares you are holding, +and will indicate the position in (time, value) space at which the +position was opened, with a green vertical bar and tide line. + +@item +When a new chart is loaded this panel shows the latest price per share +of the commodity. It can be edited, and then on pressing the enter key +the new price will appear at the current time on the graph, allowing the +user to manually input an up-to-date price. He would especially want to +do this prior to clicking the ‘BUY’ button as this price will then be +entered as the purchase price of the newly opened position. + +@item +This button will show either `BUY' or `SELL'. If there is an open +position on the stock being displayed, pressing the button will update +the position as being closed at that time, at the price per share shown +at (4). Otherwise, pressing the button will create a record of an open +position, using the unit price at (4) and the number of shares at (9). + +@item +This axis shows the real value of the shares, taking into account the +number of shares indicated at (9); it will change dynamically as that +slider is moved. + +@item +Dragging the mouse (holding the primary button down while moving) over a +chart will cause a rectangular region to be highlighted, with +information around the edge to indicate the increments in the various +quantities that the lengths of the edges represent. To remove this +informative device, click anywhere on the chart. + +@item +When the mouse cursor is over the chart, cross-hairs will provide +precise information about times and stock values at that position. Note +that two numbers are generally displayed: to the right is the unit price +of the stock, in pence/cents, and to the left is the full value of a +position taking into account the number of shares at (9), in +pounds/dollars/euro. Also note the precise time displayed just above +the bottom axis of the chart. + +@item +Here you will see an indicated number of shares, used to work out the +full value of any (potential) positions that might be opened, and to +work out the profit line at (14). In the case that there is a position +open on this stock, the slider itself will not be present but will +represent the number of shares in this position (the slider can be +restored by selecting `NONE' at (3)). + +When a new position is to be recorded, this slider should be used to set +the precise number of shares in the purchase, before pressing the buy +button at (5). + +@item +This slider alters the temporal span of the chart, allowing for the +selection of a long-term or short-term view of the stock's value's +evolution. + +@item +This slider selects the size of the window used to calculate the mean +data line (red line on the chart): a larger window means the line will +be smoother. + +The size of the window is indicated at (15). + +@item +This slider controls the width of the envelope (yellow area), so that +the user can determine a useful measure of the variability of the data +and the degree of extremity of the current position. + +@item +This is a very dynamic scale showing the calendar time at which prices +were obtained. If weeks are being indicated, the dates shown will be +those of the Mondays. + +@item +The dark gray tide is the only one whose meaning is not obvious. It +represents the profit line for a position in this stock. Taking into +account the cost of trading (set in the preferences) this is the point +above the current value at which those trading costs become offset by +the increased value of the stock (taking the number of shares, set at +(9), into account). If there is an open position, the line will +represent the real profit line for that position. Otherwise it +represents the profit point if the stock were purchased at the latest +price. + +@item +This vertical line represents the size of the moving-average window used +to produce the smoothed time-series show in red on the chart. The +distance to the right-hand edge of the chart is half the window width: +practically it means that the data-average line to the right of this +vertical line is computed over a shorter window than the rest of the +graph, making the line more erratic there, increasing so as the +extremity of the chart is reached. + + +@end enumerate + + +@node Preferences, Menus, Detailed analysis, Getting Started +@section The Preferences Dialog + +The preferences dialog allows the user to set parameters which affect +the whole of the @code{trader-desk} application. Note that there is no +`SAVE' button here; pressing the enter key or dismissing the box will +make the settings entered take effect immediately. + +@image{preferences,,,The preferences dialog box,png} + +The items are + +@enumerate + +@item Fixed cost of trading + +This should represent the fixed cost of buying @emph{and} selling shares +(respectively, opening and closing a position). It is expressed in +pounds, but you may need to take this as dollars or whatever your major +currency unit is. + +@item Proportional cost of trading + +This should represent the proportional cost (if any) of buying +@emph{and} selling shares, expressed as a percentage. It is usually +used to account for stamp duty, but may also apply to agent or market +fees. + +@end enumerate + +@node Menus, , Preferences, Getting Started +@section The Menus + +@itemize +@item +File -> Ingest new market + +This will bring up a dialog box allowing for the selection of a market +whose data are not currently being tracked by this application. Once +selected, the progress of downloading historical data for that market +will be indicated in the displays, but expect this to take some not +inconsiderable amount of time. + +@item +File -> Preferences + +Select this item to bring up the preferences dialog, allowing for the +specification of parameters which affect the overall operation of this +application. + +@item +File -> Quit + +Select this to exit the program. + +@item +Display + +Selecting this menu item will produce a list of all markets for which +historical data are currently being held. Select one of those items to +see a market overview: thumbnail charts for all components in that +market. + +@item +Market -> Update latest data + +Selecting this will cause (after a short delay) the download of the +latest prices for all the components in the current market. Note that +this option will be inactive if the display is not showing a market +overview. + +@item +Market -> Update close data + +This action will cause a fetch of all market closing prices for all +components which are not currently in the database. This is a slow +process, but progress will be indicated through the real-time update of +the market overview, and a scroll bar in a small pop-up dialog box. + +@item +Help -> About + +This option will produce an informational dialog box, displaying the +current state and credibility of this program. + +@end itemize + + +@node Algorithms, Developer, User Guide, Top +@chapter Hints for Algorithm Developers + +As previously alluded to, we don't supply any trading algorithms with +this product. Here we give some general hints to the top-level approach +to developing algorithms. + +[This section of the manual is in an early stage of development and is +probably not entirely useful at this time.] + +@section Comparing algorithms + +[This section of the manual is in an early stage of development and is +probably not entirely useful at this time.] + +The basic idea is that you think of two possible approaches to making +trading decisions based on all the current data for which you have +historical records. Run the two algorithms over the history of data you +have (i.e., pretend that you have been trading stocks and shares for all +that time), and compare the amount of money that each algorithm makes or +loses. Choose the most favourable one. + +Now take a close look at how the best algorithm performed, given the +data, and try to think of a way to improve the algorithm. Now iterate +back to the paragraph above and compare the new algorithm with the +previous best. Either accept the new algorithm, and try to improve +that, or reject it and go back to the drawing board. + +@section Iterate, find the best algorithm + +[This section of the manual is in an early stage of development and is +probably not entirely useful at this time.] + +The next level of sophistication is to let the computer perform the hunt +for the best algorithm. This requires that the algorithm uses one or +more parameters as part of its definition. For example, you might +decide to buy the stock for which the average difference between the +previous @code{n} prices is biggest, out of all the available +companies on the market. This example has the single parameter, +@code{n}, which can take on any of the values 2, 3, 4, .... Choose a +starting value, such as @code{n}=5. Now get the computer to compare +the returns with @code{n}=4 and @code{n}=6, and see if either are +better than @code{n}=5. If not, choose @code{n}=5 as the best +algorithm. If @code{n}=4 was best, have the computer next try +@code{n}=3, or if @code{n}=6 was best, try @code{n}=7. +Etcetera. + +If your algorithm has more than one parameter, it may become expensive +to try all combinations of parameter variations about the vicinity of +your starting point. In this case you will have to try a smaller number +of nearby points chosen at random, using a computer's pseudo-random +number generation facilities. + +@section Using data + +The more information you have available to guide your dealing choices +the better. This program provides you with a daily history of closing +prices, for an entire market. However, if you can also draw on other +data in real time, you should factor those into your decision algorithm. +Unfortunately, you will always be at a disadvantage compared to +professional traders with regards to news sources, as they pay stupid +money to get the news delivered to them first. Or perhaps you can think +of a way to make old news work against the big boys... a bit of reverse +psychology, perhaps? + +@menu +* Bayes:: +@end menu + +@node Bayes, , Algorithms, Algorithms +@section The Bayesian Programme + +@subsection Bayes' Theorem + +[This section of the manual is in an early stage of development and is +probably not entirely useful at this time.] + +The modern way to think in science is to realize that, going in to any +experiment, we already have a good idea of what the outcome is likely to +be, and that the effect of performing some investigation is to provide +data which allow us to update, or refine, our initial idea based on the +results. We also take the view that we never know the true outcome of +our experiments. + +@cindex Bayes' Theorem +It starts with the simple idea that the probability of two events +happening is the probability that the first event happens multiplied by +the probability of the second event happening knowing that the first +event already happened: P(A and B) = P(A)P(B|A). But the argument could +equally well have been made the other way around, and so P(A)P(B|A) = +P(B)P(A|B). Now re-arrange this to give P(B|A) = P(B)P(A|B)/P(A). Now +consider that the event A is the outcome of some experiment, and the +event B is the probability that a model parameter has some specific +value. Then the above equation states that the parameter value given +the experimental result is equal to the parameter value probability +before the experiment multiplied by the probability that the +experimental result came from a model with the original parameter +probability and divided by the probability of the experimental result. +In other words, it is a recipe for updating original knowledge of a +parameter probability with the result of an experiment to measure that +parameter. + +[Apply to market modelling.] + +@subsection The Metropolis Algorithm + +@node Developer, Time Line, Algorithms, Top +@chapter Notes for Software Developers + +[This section of the manual is in an early stage of development and is +probably not entirely useful at this time.] + +@menu +* GIT:: +* Development Style:: +* Analytic Tools:: +* Trading Robots:: +@end menu + +@c @node GIT, Development Style, Developer, Developer +@c @section Sources from GIT + +@c @cindex git +@c @cindex gtkmm +@c @cindex gcc +@c @cindex bootstrap +@c The procedure is essentially the same as for the user instructions +@c above--including the setting up of a suitable database--, except for +@c the extra boot-strapping step, which will require a suite of developer +@c build facilites (autoconf @emph{et al.}) to be available on your system. The +@c development has been done on Ubuntu-1610, Debian stable and Guix, +@c though the latter two require some choice package selections to be +@c made (GTKmm and GCC need to be 2.x and 4.9.x, respectively). + +@c @example +@c git clone .../trader-desk +@c ./bootstrap.sh; +@c [Edit trader-desk/config.h] +@c ./configure --prefix=... --with-database-password= --without-boost +@c make +@c make install +@c @end example + + +@node Development Style, Analytic Tools, Developer, Developer +@section Development philosophy + +[This section of the manual is in an early stage of development and is +probably not entirely useful at this time.] + +@subsection Open source + +While we have nothing against the @emph{publish early, publish often} +philosophy, it is not for us at this time. While the project is in the +early stages of development we are taking things carefully, slowly and +methodically and following our own @ref{Time Line}, publishing only at +planned islands of stability. + +We believe that this is most conducive to creating the project we want, +and allows meaningful testing to take place without wasting too much of +people's time. + +The intention is that the project will ultimately be owned, and a part +of, the GNU project. The intention is also that the hand-over will not +occur before completion of the initial development and establishment of +the code as being of production quality. In the meantime, we keep +ownership to ourselves, but all releases will be made under the +GPLv3+. + +@subsection Programming + +[This section of the manual is in an early stage of development and is +probably not entirely useful at this time.] + +@cindex guile +We are massive fans of Guile and the Guile-as-extension-language +paradigm, and given that the ultimate aim of this project is to provide +a base for the development and deployment of artificially intelligent +robots for making trading decisions, it would seem natural that most of +the project would be written in Guile Scheme with only some essentially +low-level core parts written in C. + +However, it is the intention that this application allows robots to run +at least an order of magnitude faster than real-time (so that they can +assimilate the entire history of the market and make timely +recommendations for activity), and allows users to explore the market +data and results of analysis with buttery-smooth interaction so that the +mechanics of the software and data management do not interfere with the +exploration and research of ideas. + +@cindex c++ +Thus the application is being developed purely in C++ (version 14 no +less, and fully version 17 by the time the project reaches production +release). This language provides the performance and facilities we +desire, provides the mechanisms to imbue innate structure to the +software, and provides the means to specify clean, verifiable, +interfaces for external modules. + +As to the programming style and project presentation we stick to the GNU +coding standards and maintainer guidelines as far as practically +possible (allowing for the fact that the standards only truly apply to +software written in C). Build management is achieved through Autotools, +and package management is offered in the form of GUIX scripts. + +@subsubsection External dependencies + +We aim to keep external dependencies to a minimum, but these are the +ones we currently use which are readily available to most GNU +systems. + +@cindex gettext +@itemize @bullet +@item +@code{gettext}: for linguistic localization of the package; + +@cindex mariadb +@item +A running @code{MariaDB} database and client library: to store locally +and to query historical market data; + +@cindex gtkmm +@item +@code{GTKmm}: the graphical widget toolkit on which the GUI is built (we +are currently stuck at 2.4; the move to version 3 will be expedited as +soon as we get the chance); + +@cindex curl +@cindex libcurl +@item +@code{libcurl} on some TLS implementation: to access services on the +Internet. + +@end itemize + +@cindex curlpp +@cindex quote library +@cindex bootstrap +Additionally, we use @code{CURLpp} (to access libcurl through a C++ +interface) and the @code{quote} library (for access to the Yahoo! +Finance API) which are seldom so readily available; these are packaged +into distribution tarballs with the project sources, @c and a GIT bootstrap +@c script is provided to pull the sources into a development project tree +wherein they will be built and integrated through the Autotools +mechanisms. + +@subsection Data sources + +At the time of writing the only freely accessible and reliable source of +market price data, both current and historical, is Yahoo! Finance. It +is however absolutely not the intention of this package to nail itself +to Yahoo!, and even though there are currently no alternatives the +interface to the price server is to be fully abstracted to allow for new +possibilities in the future. + +Unfortunately, Yahoo! does not provide a service which tells the +components (companies) which make up a market, and their symbols are +incompatible with those from other services (there is no standard, +strangely enough). Our solution is to provide a service at +@url{https://rdmp.org/trader-desk} to provide the information, and which +we are currently maintaining, by hand, ourselves (although the process +will become more automated as we gain experience). Again, it is the +intention that this project's interface to the RDMP service will be +fully abstracted, so that other back-ends can conceivably be developed +in the future. + +@node Analytic Tools, Trading Robots, Development Style, Developer +@section Creating Analytic Tools + +[This section of the manual is in an early stage of development and is +probably not entirely useful at this time.] + +`Analytic tools' are interactive devices that allow a user to explore +the data of a particular component of the market. Examples of tools +currently implemented: moving average and standard deviation-based +envelope display, the position analyzer which puts the break-even +tide mark onto all the plots, and the delta-region analyzer which allows +the user to delineate a region on a chart and see the temporal and stock +value changes which that area represents. + +These tools must all be derived from the Trader_Desk::Analyzer class. +The collection of tools forms a stack inside the application, and each +time a chart is to be rendered on-screen the stack will be called, in +order, to render the analysis results for the eyes of the user. + +Currently there is no way to package tools so that they can be +incorporated at run-time; that will (hopefully) come with the next +pre-release. To hack a new tool in, read the +@code{trader-desk/analyzer.h}--the interface you need to implement--, +and @code{trader-desk/moving-average-analyzer.h} sources which are +heavily documented and provide a good example which can be used as a +starting point for your own development. Then edit +@code{trader-desk/analyzer.cc} to cause your new object type to be +realized within the application (this and @code{trader-desk/makefile.am} +are the only original distribution source files you should need to +touch). + + +@node Trading Robots, , Analytic Tools, Developer +@section Developing Trading Robots + +[This section of the manual is in an early stage of development and is +probably not entirely useful at this time.] + +Contrary to the analytic tools described in the previous section, +which work on data of a single company, a trading robot is a tool +which works on the data of an entire market. As such, it is in a +position to work out optimal trading strategies and make informed +suggestions for the user's trading activities. + +At this point in time there actually are no hooks for where such code +would be called out from the program, nor are there any well-defined +interfaces for such beasts. To get started you should familiarize +yourself with the @code{Trader_Desk::Chart_Grid} object and its member +types: @code{Trader_Desk::Chart_Data} and +@code{Trader_Desk::Time_Series}; these hold the data you have to play +with. The sky's the limit and the rest is the extent of your +ingenuity. + +Proper support for such robots will be along in the next pre-release +(version 0.2). + + +@node Time Line, Copying This Manual, Developer, Top +@chapter Development Time Line; The State of the Onion + +Currently at version 0.1, the package is in alpha-development state and +not at all well tested in the wild (well, it works for me...). + +It is anticipated that the following developments will take place before +each release. Note that there are no implied dates associated with the +releases; you should assume several months will pass between each +release. + +@table @samp + +@item 0.1 + +Solid operation on limited data sets, and limited scope for extension +(other than to hack the code). + +@item 0.2 + +@itemize @bullet + +@item +Move to GTKmm 3.x and GCC 6+; + +@item +Re-factoring of the libraries into functional units; + +@item +development of APIs for analytical tools and robot trader extensions; + +@item +abstraction of database and market data server back-ends; + +@item +have full set of market data available. + +@end itemize + +Hopefully the code quality can be upgraded to beta status at this time. + +@item 0.3 + +@itemize +@item +Choice of MySQL/MariaDB or Postgres as database back-end (maybe also +sqlite, ndb, nosql?); + +@item +display of historical positions (stock holdings) and API for +retrospective analysis; + +@item +indication of weekends and bank holidays with option to contract them on +the charts. + +@item +Hopefully the code can be considered fully production quality and made +available to the masses as a full GNU product. + +@end itemize + +@item 0.4 + +Abstraction of control interface, with implementations as command-line +utility (usable in scripts) and web-centric service. + +@item 1.0 + +Development complete; we will stay around to maintain the project, but +otherwise it will be left in perpetuity to the FSF as part of the GNU +project. + +@end table + +Of course, any serious bugs and issues which are reported will also be +dealt with during the release cycle. + +As we are currently in alpha development and many changes to libraries +and APIs are in the pipeline, we do not anticipate accommodating major +development efforts from outside. Minor patches to fix specific, +definite problems in the code will be welcomed, however, as would +proposals for future endeavours. + +Contact with the developers about any of the above issues can only be +made through @url{https://rdmp.org/trader-desk/contact}; you may also +make a request here to be added to a mailing list which will inform you +about new releases (this is very low volume). + + +@node Copying This Manual, Index, Time Line, Top +@appendix Copying This Manual + +@c Get fdl.texi from http://www.gnu.org/licenses/fdl.html +@include fdl.texi + +@node Index, , Copying This Manual, Top +@unnumbered Index + +@printindex cp + +@bye + +@c trader-desk.texi ends here diff --git a/makefile.am b/makefile.am new file mode 100644 index 0000000..55f5a47 --- /dev/null +++ b/makefile.am @@ -0,0 +1,58 @@ +# Copyright (c) 2020 Dale Mellor +# +# This file is part of the trader-desk package. +# +# The trader-desk package is free software: you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# The trader-desk package is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + + +SUBDIRS = trader-desk . po + +ACLOCAL_AMFLAGS = --install -I m4 + +if database_data + DTA = data.sql.xz +else + DTA = +endif +dist_pkgdata_DATA = trader-desk.png ${DTA} + +pkgconfigdir = ${libdir}/pkgconfig +pkgconfig_DATA = trader-desk.pc + +DISTFILES = ABOUT-NLS + +MAINTAINERCLEANFILES = ABOUT-NLS \ + aclocal.m4 \ + build-aux/compile \ + build-aux/config.guess \ + build-aux/config.rpath \ + build-aux/config.sub \ + build-aux/depcomp \ + build-aux/install-sh \ + build-aux/ltmain.sh \ + build-aux/missing \ + config.status \ + config.log \ + configure \ + makefile \ + makefile.in \ + po/en@boldquot.po \ + po/en@quot.po \ + po/Makefile.in.in \ + po/Makevars.template \ + po/trader-desk.pot + +# Don't know why we need to do this. +dist-hook: + cp -rp aclocal.m4 AUTHORS build-aux ChangeLog configure configure.ac COPYING INSTALL libtool makefile.am makefile.in NEWS README trader-desk.png $(top_distdir) diff --git a/po/.gitignore b/po/.gitignore new file mode 100644 index 0000000..df790af --- /dev/null +++ b/po/.gitignore @@ -0,0 +1,19 @@ +*.gmo +ChangeLog +en@* +Makefile +Makefile.in +Makefile.in.in +Makevars.template +messages.mo +POTFILES +Rules-quot +boldquot.sed +en@boldquot.header +en@quot.header +insert-header.sin +quot.sed +remove-potcdate.sed +remove-potcdate.sin +stamp-po +trader-desk.pot diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..e68c9c0 --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1,2 @@ +en@quot +en@boldquot diff --git a/po/Makevars b/po/Makevars new file mode 100644 index 0000000..6795e01 --- /dev/null +++ b/po/Makevars @@ -0,0 +1,98 @@ +# Copyright (c) 2017 Dale Mellor +# +# +# This file is part of the trader-desk package. +# +# The trader-desk package is free software: you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# The trader-desk package is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + + +# Makefile variables for PO directory in any package using GNU gettext. + + +# Usually the message domain is the same as the package name. +DOMAIN = $(PACKAGE) + +# These two variables depend on the location of this directory. +subdir = po +top_builddir = .. + +# These options get passed to xgettext. +XGETTEXT_OPTIONS = --keyword=_ --keyword=N_ + +# This is the copyright holder that gets inserted into the header of the +# $(DOMAIN).pot file. Set this to the copyright holder of the surrounding +# package. (Note that the msgstr strings, extracted from the package's +# sources, belong to the copyright holder of the package.) Translators are +# expected to transfer the copyright for their translations to this person +# or entity, or to disclaim their copyright. The empty string stands for +# the public domain; in this case the translators are expected to disclaim +# their copyright. +COPYRIGHT_HOLDER = Dale Mellor + +# This tells whether or not to prepend "GNU " prefix to the package +# name that gets inserted into the header of the $(DOMAIN).pot file. +# Possible values are "yes", "no", or empty. If it is empty, try to +# detect it automatically by scanning the files in $(top_srcdir) for +# "GNU packagename" string. +PACKAGE_GNU = no + +# This is the email address or URL to which the translators shall report +# bugs in the untranslated strings: +# - Strings which are not entire sentences, see the maintainer guidelines +# in the GNU gettext documentation, section 'Preparing Strings'. +# - Strings which use unclear terms or require additional context to be +# understood. +# - Strings which make invalid assumptions about notation of date, time or +# money. +# - Pluralisation problems. +# - Incorrect English spelling. +# - Incorrect formatting. +# It can be your email address, or a mailing list address where translators +# can write to without being subscribed, or the URL of a web page through +# which the translators can contact you. +MSGID_BUGS_ADDRESS = + +# This is the list of locale categories, beyond LC_MESSAGES, for which the +# message catalogs shall be used. It is usually empty. +EXTRA_LOCALE_CATEGORIES = + +# This tells whether the $(DOMAIN).pot file contains messages with an 'msgctxt' +# context. Possible values are "yes" and "no". Set this to yes if the +# package uses functions taking also a message context, like pgettext(), or +# if in $(XGETTEXT_OPTIONS) you define keywords with a context argument. +USE_MSGCTXT = no + +# These options get passed to msgmerge. +# Useful options are in particular: +# --previous to keep previous msgids of translated messages, +# --quiet to reduce the verbosity. +MSGMERGE_OPTIONS = + +# These options get passed to msginit. +# If you want to disable line wrapping when writing PO files, add +# --no-wrap to MSGMERGE_OPTIONS, XGETTEXT_OPTIONS, and +# MSGINIT_OPTIONS. +MSGINIT_OPTIONS = + +# This tells whether or not to regenerate a PO file when $(DOMAIN).pot +# has changed. Possible values are "yes" and "no". Set this to no if +# the POT file is checked in the repository and the version control +# program ignores timestamps. +PO_DEPENDS_ON_POT = yes + +# This tells whether or not to forcibly update $(DOMAIN).pot and +# regenerate PO files on "make dist". Possible values are "yes" and +# "no". Set this to no if the POT file and PO files are maintained +# externally. +DIST_DEPENDS_ON_UPDATE_PO = yes diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..fa00d8c --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,27 @@ +# Copyright (c) 2020 Dale Mellor +# +# This file is part of the trader-desk package. +# +# The trader-desk package is free software: you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# The trader-desk package is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + + +# List of source files which contain translatable strings. + +trader-desk/chart.cc +trader-desk/company-name-entry.cc +trader-desk/date-range-scale.cc +trader-desk/moving-average-analyzer.h +trader-desk/sd-envelope-analyzer.h +trader-desk/shares-scale.cc +trader-desk/trader-desk.cc diff --git a/po/en_GB.po b/po/en_GB.po new file mode 100644 index 0000000..e3c82a3 --- /dev/null +++ b/po/en_GB.po @@ -0,0 +1,179 @@ +# English translations for trader-desk package. +# Copyright (C) 2020 Dale Mellor +# This file is distributed under the same license as the trader-desk package. +# +msgid "" +msgstr "" +"Project-Id-Version: trader-desk 0.1\n" +"POT-Creation-Date: 2017-02-10 12:31+0000\n" +"PO-Revision-Date: 2017-02-10 12:35+0000\n" +"Last-Translator: Dale Mellor\n" +"Language-Team: English (British)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: trader-desk/chart.cc:257 +msgctxt "Label" +msgid "Position value (pounds)" +msgstr "Position value (£)" + +#: trader-desk/company-name-entry.cc:44 +msgctxt "Label" +msgid "Company name: " +msgstr "Company name: " + +#: trader-desk/date-range-scale.cc:9 +#, c-format +msgctxt "Label" +msgid "Date range = %.0f days" +msgstr "Date range = %.0f days" + +#: trader-desk/moving-average-analyzer.h:29 +#, c-format +msgctxt "Label" +msgid "Mean window = %.0f days" +msgstr "Mean window = %.0f days" + +#: trader-desk/positions-widget.cc:14 +msgctxt "Label" +msgid "Positions" +msgstr "Positions" + +#: trader-desk/positions-widget.cc:24 +msgctxt "Label" +msgid "None" +msgstr "None" + +#: trader-desk/sd-envelope-analyzer.h:32 +#, c-format +msgctxt "Label Abbrev:Standard deviation" +msgid "Envelope width = %.2f x std. dev." +msgstr "Envelope width = %.2f x std. dev." + +#: trader-desk/shares-scale.cc:12 +#, c-format +msgctxt "Label" +msgid "Number of shares = %.0f" +msgstr "Number of shares = %.0f" + +#: trader-desk/trader-desk.cc:27 +msgctxt "Window title" +msgid "Updating database" +msgstr "Updating database" + +#: trader-desk/trader-desk.cc:36 +msgctxt "Information" +msgid "trader-desk: Fetching data from Internet" +msgstr "trader-desk: Fetching data from Internet" + +#: trader-desk/trader-desk.cc:55 +msgctxt "Label" +msgid "Fixed cost of trading" +msgstr "Fixed cost of trading" + +#: trader-desk/trader-desk.cc:59 +msgctxt "Units:Worded:Monetary" +msgid "pounds" +msgstr "pounds" + +#: trader-desk/trader-desk.cc:62 +msgctxt "Label" +msgid "Proportional cost of trading" +msgstr "Proportional cost of trading" + +#: trader-desk/trader-desk.cc:65 +msgctxt "Units:Worded" +msgid "percent" +msgstr "percent" + +#: trader-desk/trader-desk.cc:76 +msgctxt "Window title" +msgid "Trader-Desk: Preferences" +msgstr "Trader-Desk: Preferences" + +#: trader-desk/trader-desk.cc:155 trader-desk/trader-desk.cc:167 +msgctxt "Instruction" +msgid "Select market" +msgstr "Select market" + +#: trader-desk/trader-desk.cc:175 +msgctxt "Join-A" +msgid "Sorry, there are no" +msgstr "Sorry, there are no" + +#: trader-desk/trader-desk.cc:179 +msgctxt "Join-A" +msgid "new markets." +msgstr "new markets." + +#: trader-desk/trader-desk.cc:182 trader-desk/trader-desk.cc:232 +msgctxt "Instruction" +msgid "Cancel" +msgstr "Cancel" + +#: trader-desk/trader-desk.cc:208 +msgctxt "Label" +msgid "Market" +msgstr "Market" + +#: trader-desk/trader-desk.cc:224 +msgctxt "Instruction, Join-B" +msgid "Double-click a market name" +msgstr "Double-click a market name" + +#: trader-desk/trader-desk.cc:229 +msgctxt "Instruction, Join-B" +msgid "to ingest its data" +msgstr "to ingest its data" + +#: trader-desk/trader-desk.cc:350 trader-desk/trader-desk.cc:386 +msgid "No Internet Connection" +msgstr "No Internet Connection" + +#: trader-desk/trader-desk.cc:446 +msgctxt "Menu" +msgid "_File" +msgstr "_File" + +#: trader-desk/trader-desk.cc:448 +msgctxt "Menu" +msgid "_Preferences" +msgstr "_Preferences" + +#: trader-desk/trader-desk.cc:453 +msgctxt "Menu" +msgid "_Display" +msgstr "_Display" + +#: trader-desk/trader-desk.cc:455 +msgctxt "Menu" +msgid "_Market" +msgstr "_Market" + +#: trader-desk/trader-desk.cc:457 +msgctxt "Menu" +msgid "_Help" +msgstr "_Help" + +#: trader-desk/trader-desk.cc:458 +msgctxt "Menu" +msgid "_About" +msgstr "_About" + +#: trader-desk/trader-desk.cc:501 +msgctxt "Menu" +msgid "Update _latest data" +msgstr "Update _latest data" + +#: trader-desk/trader-desk.cc:506 +msgctxt "Menu" +msgid "Update _close data" +msgstr "Update _close data" + +#: trader-desk/trader-desk.cc:511 +msgctxt "Menu" +msgid "_Ingest new market" +msgstr "_Ingest new market" diff --git a/setup-hint.sh b/setup-hint.sh new file mode 100644 index 0000000..cca192d --- /dev/null +++ b/setup-hint.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +# Get an up to date system with all the packages we require. + +sudo apt update +sudo apt upgrade -y +sudo apt install -y git build-essential libmariadbclient-dev \ + mariadb-server autoconf autopoint \ + libssl-dev libtool libgtkmm-3.0-dev \ + libcurl4-openssl-dev cmake texinfo xauth \ + gettext ed + + +# Set up the database to make it possible for anybody (in particular the +# trader-desk application) to use the database root account. + +sudo mysql mysql <<-\EOF + update mysql.user set password='', plugin=''; + flush privileges; + exit + EOF + + +# Download, build and install gcc 9.3. We use the latest C++20 standards, +# and need the best compiler we can get. + +sudo bash -c 'cat > /etc/ld.so.conf.d/01-local.conf' <<-\EOF + /usr/local/lib64 + /usr/local/lib + EOF +sudo ldconfig + +mkdir ${HOME}/sources; cd ${HOME}/sources +wget ftp://ftp.gnu.org/gnu/gcc/gcc-9.3.0/gcc-9.3.0.tar.xz +wget ftp://ftp.gnu.org/gnu/gmp/gmp-6.2.0.tar.xz +wget ftp://ftp.gnu.org/gnu/mpc/mpc-1.1.0.tar.gz +wget ftp://ftp.gnu.org/gnu/mpfr/mpfr-4.0.2.tar.xz +tar xf gcc-9.3.0.tar.xz +cd gcc-9.3.0 +tar xf ../gmp-6.2.0.tar.xz +ln -s gmp-6.2.0 gmp +tar xf ../mpc-1.1.0.tar.gz +ln -s mpc-1.1.0 mpc +tar xf ../mpfr-4.0.2.tar.xz +ln -s mpfr-4.0.2 mpfr +./configure --enable-languages=c,c++ --disable-bootstrap --disable-multilib +make -j2 # Takes a long time. +sudo make install +cd ${HOME}/sources; rm -rf gcc-9.3.0 + + +# Make a couple of third-party packages that don’t come with the Debian +# system. + +cmake_build() { cd $1 + mkdir build + cd build + cmake -DBUILD_SHARED_LIBS=TRUE .. + make -j2 + sudo make install +} + + +cd ${HOME}/sources +git clone https://github.com/fmtlib/fmt.git +( cmake_build fmt ) + + +cd ${HOME}/sources +git clone https://github.com/jpbarrette/curlpp.git +( cmake_build curlpp ) + +sudo ed /usr/local/lib/pkgconfig/curlpp.pc <<-\EOF + 1,$s@-Llib@-L${prefix}/lib@ + w + q + EOF + + +# Now build the DMBCS packages which make up the trader-desk application. + +autotools_build() { cd $1 + autoreconf --install + ./configure + make -j2 + sudo make install +} + +cd ${HOME} + +git clone https://rdmp.org/dmbcs/market-data-api.git dmbcs-market-data-api +( autotools_build dmbcs-market-data-api ) + +git clone https://rdmp.org/dmbcs/trader-desk.git dmbcs-trader-desk +( autotools_build dmbcs-trader-desk ) + +sudo ldconfig + +mkdir ${HOME}/.config + + +# Take heed of this message! + +cat < + + +namespace DMBCS::Trader_Desk { + + + + unique_ptr Alpha_Vantage__Monitor::clocks; + + + + void Alpha_Vantage__Monitor::make_clock_widgets (Preferences& P, + Gtk::HBox& C) + { + initialize_clocks (P); + Clocks& c {*clocks}; + + C.pack_start (c.starter); + C.pack_start (c.count_down); + C.pack_start (c.spacer); + C.pack_start (c.strikes_); + C.pack_start (c.finisher); + } + + + + void Alpha_Vantage__Monitor::remove_clock_widgets (Gtk::Container& C) + { + Clocks& c {*clocks}; + C.remove (c.starter); + C.remove (c.count_down); + C.remove (c.spacer); + C.remove (c.strikes_); + C.remove (c.finisher); + clocks.reset (); + } + + + + using system_clock = chrono::system_clock; + +void Alpha_Vantage__Monitor::Clocks::hit () + { + try + { + db.quick() << "insert ignore into alphavantage_ticks (time) value (" + << (system_clock::to_time_t (system_clock::now ())) + << ")"; + } + catch (Mysql::DB_Connection::Exception&) + { + db.reconnect (db.current_preferences); + } + + last_time = chrono::steady_clock::now (); + } + + + + static int seconds_since (const chrono::steady_clock::time_point& T) + { return chrono::duration_cast + (chrono::steady_clock::now () - T) + .count (); } + + using C = Alpha_Vantage__Monitor::Clocks; + +const C& C::update () +{ + const time_t now {system_clock::to_time_t (system_clock::now ())}; + + try + { + auto I {db.instruction ()}; + I << "delete from alphavantage_ticks where time<" + << (now - 24 * 60 * 60); + I.execute (); + strikes + = db.scalar_result (0, "select count(*) from alphavantage_ticks"); + strikes_.set_text (to_string (strikes)); + count_down.set_text (to_string (max (0, + 12 - seconds_since (last_time)))); + } + catch (Mysql::DB_Connection::Exception&) {} + + return *this; +} + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/alpha-vantage--monitor.h b/trader-desk/alpha-vantage--monitor.h new file mode 100644 index 0000000..db4442c --- /dev/null +++ b/trader-desk/alpha-vantage--monitor.h @@ -0,0 +1,90 @@ +#ifndef DMBCS__TRADER_DESK__ALPHA_VANTAGE__MONITOR__H +#define DMBCS__TRADER_DESK__ALPHA_VANTAGE__MONITOR__H + + +#include + + +namespace DMBCS::Trader_Desk { + + + struct Alpha_Vantage__Monitor + { + using Error = Alpha_Vantage::Error; + using Throttled = Alpha_Vantage::Throttled; + using Bad_API_Key = Alpha_Vantage::Bad_API_Key; + using TO_DO = Alpha_Vantage::TO_DO; + + struct Clocks + { + DB db; + chrono::steady_clock::time_point last_time; + int strikes {0}; + + Gtk::Label starter {"AlphaVantage charge: "}; + Gtk::Label count_down {"0"}; + Gtk::Label spacer {" "}; + Gtk::Label strikes_ {"0"}; + Gtk::Label finisher {"/500"}; + + + explicit Clocks (Preferences& P) : db {P} + { update (); } + + void hit (); + const Clocks& update (); + }; + + + + static unique_ptr clocks; + + + + static void initialize_clocks (Preferences& P) + { + if (! Alpha_Vantage__Monitor::clocks) + Alpha_Vantage__Monitor::clocks = make_unique (P); + } + + + static void make_clock_widgets (Preferences&, Gtk::HBox& container); + + static void remove_clock_widgets (Gtk::Container&); + + + static TO_DO get_closing_prices /* override */ + (const Update_Closing_Prices::Company& C, + const string& market_component_extension, + Preferences& P, + const function injector) + { + initialize_clocks (P); + const TO_DO ret {Alpha_Vantage::get_closing_prices + (C, market_component_extension, P, injector)}; + if (ret == TO_DO::FINISHED) clocks->hit (); + return ret; + } + + + + static Update_Latest_Prices::Data get_latest_data /* override */ + (const Update_Latest_Prices::Company& C, + const string& market_component_extension, + Preferences& P) + { + initialize_clocks (P); + auto ret {Alpha_Vantage::get_latest_data + (C, market_component_extension, P)}; + clocks->hit (); + return ret; + } + + + } ; /* End of class Alpha_Vantage__Monitor. */ + + + } /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__ALPHA_VANTAGE__MONITOR__H. */ diff --git a/trader-desk/alpha-vantage.cc b/trader-desk/alpha-vantage.cc new file mode 100644 index 0000000..221a7fd --- /dev/null +++ b/trader-desk/alpha-vantage.cc @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include /* For time utilities. */ +#include +#include +#include +#include + + +namespace DMBCS::Trader_Desk { + + +static bool throttle () + { + namespace C = chrono; + using clock = C::system_clock; + + static clock::time_point LAST_CALL; + + const C::duration wait {clock::now () - LAST_CALL}; + if (wait < C::seconds {12}) + { + usleep (min (number (C::seconds {12} - wait), + number (C::milliseconds {500}))); + return 1; + } + + LAST_CALL = clock::now (); + return 0; + } + + + +static void throw_error (const string& query, const string& json) + { + static const regex re {"\"(Note|Error Message)\": +\"([^\"]*)"}; + smatch match; + if (! regex_search (json, match, re)) + throw Alpha_Vantage::Error + {"AlphaVantage returned garbled error message"}; + if (match [1] == "Note") throw Alpha_Vantage::Throttled {match [2]}; + static const regex re2 {"apikey.*(invalid|missing)", regex::icase}; + if (regex_search (match [2].str (), re2)) + throw Alpha_Vantage::Bad_API_Key {match [2]}; + throw Alpha_Vantage::Error + {match [2].str () + + "\n\n[The query was ‘" + query + "’.]"}; + } + + + +static string get_curl_response (const string& query) + { + namespace Curl = cURLpp; + namespace Opt = Curl::Options; + + string response; + Curl::Easy request; + + request.setOpt (Opt::Url {query}); + + request.setOpt (Opt::WriteFunction + {[&response] (char* buffer, size_t size, size_t n) + { response += string {buffer, + buffer + size * n}; + return size * n; }}); + + request.setOpt (Opt::SslVerifyPeer {0}); + + request.perform (); + + return response; + } + + + + static time_t to_time_t (const Update_Closing_Prices::Data& D) + { return t (D.year, D.month, D.day); } + + + inline string maybe_dot (const string& X) + { return X.length () ? '.' + X : X; } + + + + static string get_timeseries_csv + (const Update_Closing_Prices::Company& company, + string market_component_extension, + const string& api_key, + const bool over_100) + { + const string query + {fmt::format ("https://www.alphavantage.co/query" + "?function=TIME_SERIES_DAILY_ADJUSTED" + "&symbol={}" + "&apikey={}" + "&datatype=csv" + "&outputsize={}", + company.symbol + maybe_dot (market_component_extension), + api_key, + over_100 ? "full" : "compact") }; + + const string ret {get_curl_response (query)}; + + if (ret [0] == '{') throw_error (query, ret); + + return ret; + } + + static vector fields (const string& csv, const char separator) + { + vector ret; + for (size_t cursor {0}; ; ) + { + const size_t next_cursor {csv.find (separator, cursor)}; + ret.push_back (csv.substr (cursor, next_cursor - cursor)); + if (next_cursor == csv.npos) return ret; + cursor = next_cursor + 1; + } + } + + static istream& operator>> (istream& I, Update_Closing_Prices::Data& D) + { + static char comma; + static double dividend_amount, split_coefficient; + + I >> D.year >> comma >> D.month >> comma >> D.day >> comma + >> D.open >> comma >> D.high >> comma + >> D.low >> comma >> D.close >> comma + >> D.adj_close >> comma >> D.volume >> comma + >> dividend_amount >> comma >> split_coefficient; + + return I; + } + + static vector parse_csv + (const string& results, const int company_seqid) + { + istringstream O {results.substr (results.find ('\n'))}; + vector data; + + for (;;) { Update_Closing_Prices::Data datum; + datum.company_seqid = company_seqid; + O >> datum; + if (! O.good ()) return data; + data.push_back (move (datum)); } + } + + static int days_ago (const time_t T) + { + using Clock = chrono::system_clock; + return number (Clock::now () + - Clock::from_time_t (T)) + / 24; + } + +auto Alpha_Vantage::get_closing_prices + (const Update_Closing_Prices::Company& company, + const string& market_component_extension, + const Preferences& P, + const function injector) + -> TO_DO + { + if (throttle ()) return MORE_WORK; + + vector data + {parse_csv (get_timeseries_csv + (company, + market_component_extension, + P.market_data_service_key, + days_ago (company.last_close_date) > 100), + company.seqid)}; + + auto i { find_if (data.rbegin (), data.rend (), + [L = company.last_close_date] + (const Update_Closing_Prices::Data& D) + { return to_time_t (D) > L; })}; + + if (i != data.rbegin()) --i; + + for (auto j {i}; j != data.rend (); ++j) + injector (*j); + + return FINISHED; + } + + + + static string get_snap_csv (const Update_Latest_Prices::Company& company, + const string& market_component_extension, + const string& api_key) + { + const auto query + { fmt::format ("https://www.alphavantage.co/query" + "?function=GLOBAL_QUOTE" + "&symbol={}" + "&datatype=csv" + "&apikey={}", + company.symbol + maybe_dot (market_component_extension), + api_key) }; + + const string ret {get_curl_response (query)}; + if (ret [0] == '{') throw_error (query, ret); + return ret; + } + + inline double extract_price_csv (const string& X) + { + return strtod (fields (fields (X, '\n') [1], ',') [4].data (), + nullptr); + } + + + +Update_Latest_Prices::Data Alpha_Vantage::get_latest_data + (const Update_Latest_Prices::Company& company, + const string& market_component_extension, + const Preferences& P) + { + while (throttle ()) ; + + return + { .company_seqid = company.seqid, + .time = chrono::system_clock::now (), + .price = extract_price_csv + (get_snap_csv (company, + market_component_extension, + P.market_data_service_key)) }; + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/alpha-vantage.h b/trader-desk/alpha-vantage.h new file mode 100644 index 0000000..6d5afa3 --- /dev/null +++ b/trader-desk/alpha-vantage.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__ALPHA_VANTAGE__H +#define DMBCS__TRADER_DESK__ALPHA_VANTAGE__H + + +#include +#include + + +namespace DMBCS::Trader_Desk { + + + /* This is a struct rather than a namespace because we want to be able + * to derive from it a version with a proper API key, and want to + * alias it as a type in ‘update_*_prices.cc’. + * + * The reason for this seemingly unnecessary abstraction is that in + * the past we use Yahoo! Finance for this purpose, but they pulled + * their API. We now want to keep things flexible so that other data + * sources might be used in future. */ + +struct Alpha_Vantage + { + struct Error : runtime_error { using runtime_error::runtime_error; }; + struct Throttled : Error { using Error::Error; }; + struct Bad_API_Key : Error { using Error::Error; }; + + + enum TO_DO : bool {FINISHED = 0, MORE_WORK = 1}; + static TO_DO get_closing_prices /* override */ + (const Update_Closing_Prices::Company&, + const string& market_component_extension, + const Preferences& P, + const function injector); + + + static Update_Latest_Prices::Data get_latest_data /* override */ + (const Update_Latest_Prices::Company&, + const string& market_component_extension, + const Preferences&); + + + } ; /* End of class Alpha_Vantage. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__ALPHA_VANTAGE__H. */ diff --git a/trader-desk/analyzer.cc b/trader-desk/analyzer.cc new file mode 100644 index 0000000..7e8c16f --- /dev/null +++ b/trader-desk/analyzer.cc @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include + + +/** \file + * + * Implementation of the \c Analyzer_Stack constructor, a factory which + * knows about all the detailed analyzers. If a new analyzer is to be + * added to the system, this file will need modifying to accomodate it; + * it is the *only* place that needs to know anything about specific + * analyzers. */ + + +namespace DMBCS::Trader_Desk { + + + Analyzer_Stack::Analyzer_Stack (Chart_Data& chart_data, Preferences&) + { + analyzers.emplace_back (new SD_Envelope_Analyzer {chart_data}); + analyzers.emplace_back (new Delta_Analyzer {chart_data}); + + for (auto &a : analyzers) + a -> signal_redraw_needed () + . connect ([this] { redraw_needed_signal.emit (); }); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/analyzer.h b/trader-desk/analyzer.h new file mode 100644 index 0000000..da58f3e --- /dev/null +++ b/trader-desk/analyzer.h @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__ANALYZER__H +#define DMBCS__TRADER_DESK__ANALYZER__H + + +#include +#include +#include +#include + + +/** \file + * + * Declaration of the \c Analyzer pure interface, and of the \c + * Analyzer_Stack class. */ + + +namespace DMBCS::Trader_Desk { + + + /** Abstract idea of an object which can draw anything it wants onto a + * chart, hopefully to provide some useful illumination of the data to + * the user. Additionally a control widget can be given which will + * appear at the right-hand side of the screen. */ + + struct Analyzer + { + /** Obligatory null destructor for a purely virtual base class. */ + virtual ~Analyzer () = default; + + /** The returned object, if not empty, will be stacked up at the right + * edge of the window. This is optional functionality for analyzers, + * so we provide a default implementation which adds no controls. */ + virtual vector make_control_widgets () + { return {}; } + + + /** Draw whatever into the \a chart_context. The tide_marks are the + * points in time at which tide-lines need adding, if they are to + * show on the \c hand_analysis_widget. */ + virtual void graph_draw_hook + (Chart_Context &canvas, + Tide_Mark::List ¬es, + unsigned number_shares, + vector const &) = 0; + + /** An opportunity for the analyzer to increase the area in which + * time-series are plotted, if this is necessary to display all of + * the auxiliary data, for example. Most analyzers donʼt need this, + * so we provide a null default. */ + virtual void stretch_outline (Time_Series::Range &) {} + + /** Emitted internally to signal to the world that analytical results + * have changed (and probably need re-rendering on-screen). */ + virtual sigc::signal &signal_redraw_needed () = 0; + + /** Allow an analyzer to respond to mouse presses on the chart canvas. + * + * As it stands, the system only allows for one of the analyzers to + * respond to this, and that privilege is currently taken by the \c + * Delta_Analyzer. */ + virtual bool button_down (int const /*x*/, int const /*y*/) { return 0; } + + /** Allow an analyzer to respond to mouse button releases. */ + virtual bool button_up (int const /*x*/, int const /*y*/) { return 0; } + + /** Allow an analyzer to respond to mouse drags on the chart canvas. */ + virtual bool button_move (int const /*x*/, int const /*y*/) { return 0; } + + + }; /* End of class Analyzer. */ + + + + /** Whether there is one analyzer in the system or twenty, this class + * makes it appear to the rest of the application as if there were just + * one, thus simplifying matters. It is simply a wrapper around a + * container of analyzers. This also removes any dependencies of the + * application on the specifics of the analyzers keeping the + * abstraction purely abstract: the \c Analyzer_Stack constructor is a + * shrink-wrapped analyzer factory. */ + + struct Analyzer_Stack : Analyzer + { + /** The individual analyzers that we provide a home for. */ + vector > analyzers; + + /** If any analyzer indicates the need for a graphics re-draw, we will + * fire this signal which the application needs to listen out for. */ + sigc::signal redraw_needed_signal; + + /** If not \c nullptr, this analyzer is working with the mouse. */ + Analyzer *mouse_user {nullptr}; + + + /** The class constructor is actually a comprehensive analyzer + * factory, and returns a fully populated object ready to run the + * full gamut of analysis at the userʼs behest. */ + explicit Analyzer_Stack (Chart_Data &, Preferences&); + + + /** Provide a container-load of widgets which the various analyzers + * need to function. These are not at all life-time managed and the + * application must take responsibility to destroy the objects when + * necessary. The application is expected to allocate space + * horizontally at the right-hand edge of the data charts. */ + vector make_control_widgets () override + { + vector ret; + for (auto &a : analyzers) + { + auto const w = a->make_control_widgets (); + ret.insert (end (ret), begin (w), end (w)); + } + return ret; + } + + + /** Give every analyzer an opportunity to draw onto the chart + * canvas. */ + void graph_draw_hook + (Chart_Context &c, + Tide_Mark::List &t, + unsigned number_shares, + vector const &p) override + { + for (auto &a : analyzers) + a->graph_draw_hook (c, t, number_shares, p); + } + + + /** Give every analyzer a go at stretching the size of the chart + * canvas. */ + void stretch_outline (Time_Series::Range &r) override + { for (auto &a : analyzers) a->stretch_outline (r); } + + + /** Return our re-draw signal. */ + sigc::signal &signal_redraw_needed () override + { return redraw_needed_signal; } + + + /** Give each analyzer notification of a button press event until one + * responds and acts on the event. */ + bool button_down (int const x, int const y) override + { + for (auto &a : analyzers) + if (a->button_down (x, y)) + { + mouse_user = a.get (); + return 1; + } + + return 0; + } + + + /** Give the ‘active’ analyzer information about a mouse button-up + * event. */ + bool button_up (int const x, int const y) override + { return mouse_user && mouse_user->button_up (x, y); } + + + /** Give the ‘active’ analyzer information about a mouse move + * event. */ + bool button_move (int const x, int const y) override + { return mouse_user && mouse_user->button_move (x, y); } + + + }; /* End of class Analyzer_Stack. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__ANALYZER__H. */ diff --git a/trader-desk/application--ingest-market.cc b/trader-desk/application--ingest-market.cc new file mode 100644 index 0000000..2b31423 --- /dev/null +++ b/trader-desk/application--ingest-market.cc @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include + + +/** \file + * + * Implementation of the \c Application::ingest_new_market + * mega-method. */ + + +namespace DMBCS::Trader_Desk { + + + void Application::ingest_new_market () + { + try + { + DB db {user_prefs}; + Markets markets {db}; + + try { + update_market_meta_data (markets, db); + } + catch (Market_Data_Api::Bad_Communication const &e) + { + Gtk::MessageDialog {*window, e.what (), 0, Gtk::MESSAGE_WARNING} + .run (); + return; + } + + Gtk::Dialog dialog (pgettext ("Instruction", "Select market"), + *window, + Gtk::DIALOG_MODAL); + + auto add_label = [&dialog] (string const &m) + { + Gtk::Label *const t = new Gtk::Label; + t->set_markup (m); + dialog . get_vbox () -> pack_start (*Gtk::manage (t), + Gtk::PACK_SHRINK); + }; + + add_label (pgettext ("Instruction", "Select market")); + + if (find_if (begin (markets), end (markets), + [] (Markets::value_type const &m) + { return ! m.second.tracked; }) + == markets.end ()) + { + add_label (string {""} + + pgettext ("Join-A", "Sorry, there are no") + + ""); + + add_label (string {""} + + pgettext ("Join-A", "new markets.") + + ""); + + dialog . add_button (pgettext ("Instruction", "Cancel"), + Gtk::RESPONSE_CANCEL); + dialog . set_size_request (300, 250); + dialog . show_all (); + + dialog . run (); + } + + else + { + Gtk::TreeModelColumn name; + Gtk::TreeModelColumn data; + Gtk::TreeModel::ColumnRecord columns; + columns.add (name); + columns.add (data); + + auto list = Gtk::ListStore::create (columns); + for (auto const &u : markets) + if (! u.second.tracked) + { + auto b = list->append (); + (*b) [name] = u.second.world_data.name; + (*b) [data] = &u.second; + } + + Gtk::TreeView view {list}; + view . append_column (pgettext ("Label", "Market"), name); + + Gtk::TreeModel::Path selected_row; + + + view . signal_row_activated () + . connect ([&selected_row, &dialog] + (Gtk::TreeModel::Path const &path, + Gtk::TreeViewColumn *const) + { selected_row = path; + dialog.response (Gtk::RESPONSE_OK); }); + + dialog . get_vbox () -> pack_start (view); + + add_label (string {""} + + pgettext ("Instruction, Join-B", + "Double-click a market name") + + ""); + + add_label (string {""} + + pgettext ("Instruction, Join-B", + "to ingest its data") + + ""); + + dialog . add_button (pgettext ("Instruction", "Cancel"), + Gtk::RESPONSE_CANCEL); + dialog . set_size_request (300, 250); + dialog . show_all (); + + if (Gtk::RESPONSE_OK == dialog . run ()) + { + dialog.hide (); + + auto market_data = *(*list->get_iter (selected_row)) [data]; + + start_tracking (markets, db, market_data.world_data.symbol); + + /* Check that the seqid is not in market_grids. */ + + auto a = find_if (begin (market_grids), + end (market_grids), + [seqid = market_data.seqid] + (unique_ptr const &g) + { return g->market.seqid == seqid; }); + + if (a == end (market_grids)) + { + /* !!!! How is this getting destroyed? */ + auto *const g {new Chart_Grid {user_prefs, market_data}}; + + if (market_grids.empty ()) notebook.remove_page (1); + + market_grids.emplace_back (g); + + g->selection_signal + .connect ([this, g] + { hand_analysis->subsume_selected (*g); + close_data_menu->set_sensitive (0); + last_data_menu->set_sensitive (1); + notebook.set_current_page (0); }); + + notebook.append_page (*g); + display_grid (market_grids.size () - 1); + notebook.show_all (); + + /* Now fix up the menus. */ + { + auto const n = market_grids.size () - 1; + ostringstream a; a << "display-grid-" << n; + + actions->add (Gtk::Action::create + (a.str (), market_data.world_data.name), + [this, n] { display_grid (n); }); + + ui_manager->add_ui_from_string + ("" + " " + " " + " " + " " + " " + ""); + } + + /* !!!! Maybe think of re-setting the connection to + * the database here. */ + + update_closing_prices (); + } + } + } + } + + catch (Market_Data_Api::No_Network &e) + { + Gtk::MessageDialog {*window, e.what (), 0, Gtk::MESSAGE_WARNING} + .run (); + } + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/application--update-closing-prices.cc b/trader-desk/application--update-closing-prices.cc new file mode 100644 index 0000000..6b42fcf --- /dev/null +++ b/trader-desk/application--update-closing-prices.cc @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include "alpha-vantage--monitor.h" +#include "application.h" +#include "update-closing-prices.h" +#include + + +/** \file + * + * Implementation of the \c Application::update_closing_prices + * mega-method. */ + + +namespace DMBCS::Trader_Desk { + + +struct Progress_Dialog : Gtk::Dialog + { + Gtk::ProgressBar progress_bar; + Gtk::Label company_name; + + explicit Progress_Dialog (Gtk::Window &parent); + }; + + + +Progress_Dialog::Progress_Dialog (Gtk::Window &parent) + : Gtk::Dialog (pgettext ("Window title", "Updating database"), parent, 1) + { + company_name.set_ellipsize (Pango::ELLIPSIZE_END); + company_name.set_alignment (Gtk::ALIGN_START); + get_vbox ()->pack_start + (*Gtk::make_managed + (pgettext ("Information", + "trader-desk: Fetching data from Internet"))); + get_vbox ()->pack_start (company_name, Gtk::PACK_EXPAND_WIDGET); + get_vbox ()->pack_start (progress_bar, Gtk::PACK_SHRINK); + add_button (Gtk::Stock::STOP, 0); + get_vbox ()->set_spacing (10); + set_default_size (400, 10); + show_all (); + } + + + +extern "C" int queue_draw (Gtk::Widget *const w) + { w->queue_draw (); return 0; } + + + + struct no_connection_args + { Gtk::Window* window; string message; }; + +extern "C" int run_no_internet_connection_message + (const no_connection_args *const A) + { + Gtk::MessageDialog {*A->window, A->message, 0, Gtk::MESSAGE_WARNING} + .run (); + delete A; + return 0; + } + + + +extern "C" int delete_widget (Gtk::Widget *const w) + { delete w; return 0; } + + + + struct pulse_progress_bar_args + { Progress_Dialog* dialog; double fraction; string company_name; }; + +extern "C" int pulse_progress_bar (const pulse_progress_bar_args *const A) + { + A -> dialog -> progress_bar . set_fraction (A->fraction); + A -> dialog -> company_name . set_text (A->company_name); + delete A; + return 0; + } + + + + struct preferences_error_args { Application* app; string message; }; + +extern "C" int preferences_error_ (const preferences_error_args *const A) + { + A->app->preferences_error (A->message); + delete A; + return 0; + } + + + +static void grid_injector (Chart_Grid& grid, + Chart** current_chart, + const Update_Closing_Prices::Data& data) + { + if (! *current_chart) + *current_chart = grid.find_chart (data.company_seqid); + + if (*current_chart) + (*current_chart) -> data + . new_event + ({chrono::system_clock::from_time_t + (t (data.year, data.month, data.day)) + + (*current_chart)->data.prices.market_close_time, + data.close}, + Chart_Data::NO_SIGNAL); + } + +void Application::update_closing_prices () + { + const size_t a {(size_t) notebook.get_current_page ()}; + + if (a < 1 || a > market_grids.size ()) return; + + Chart_Grid& grid {*market_grids [a - 1]}; + + try { + DB db {user_prefs}; + grid.regenerate (db, user_prefs); + } + catch (const Market_Data_Api::Bad_Communication& e) + { + Gtk::MessageDialog {*window, e.what (), 0, Gtk::MESSAGE_WARNING} + .run (); + return; + } + + for (auto& c : grid.chart) c->data.unaccurate = 1; + + grid.queue_draw (); + + std::thread + {[this, progress_dialog = new Progress_Dialog (*window), &grid] + { + sigc::connection call_id; + + try + { + DB db {user_prefs}; + Update_Closing_Prices::Work update {.market = grid.market}; + + call_id = progress_dialog->signal_response () + .connect ([&update] (int) + { update.stop = true; }); + + /** We donʼt want to hunt for the correct chart every time a + * new datum is reported to us, so only do the search when + * this is \c nullptr and re-use the last search result + * (stored here) otherwise. */ + Chart* current_chart {nullptr}; + + do_update + (update, + db, + user_prefs, + + /* Progress callback. */ + [progress_dialog, &grid] + (const double& x, + const Update_Closing_Prices::Company& company) + { gdk_threads_add_idle + ((int(*)(void*)) pulse_progress_bar, + new pulse_progress_bar_args + {progress_dialog, x, company.name}); }, + + /* Datum injector. */ + [&db, &grid, ¤t_chart] + (Update_Closing_Prices::Data const &data) + { if (data.close == 0) return; + sql_injector (db, data); + grid_injector (grid, ¤t_chart, data); }, + + /* Company_done. */ + [&grid, ¤t_chart] (int const &company_seqid) + { if (! current_chart) + current_chart = grid.find_chart (company_seqid); + + current_chart->data.extremes + = current_chart->data + .prices + .get_range (chrono::hours (50*24)); + + current_chart->data.unaccurate = 0; + gdk_threads_add_idle ((int(*)(void*))queue_draw, + current_chart); + current_chart = nullptr; }); + } + + catch (const Price_Server::Bad_API_Key& E) + { + gdk_threads_add_idle + ((int(*)(void*)) preferences_error_, + new preferences_error_args {this, E.what ()}); + } + + catch (const Update_Closing_Prices::No_Connection& E) + { + gdk_threads_add_idle + ((int(*)(void*)) run_no_internet_connection_message, + new no_connection_args {window, E.what ()}); + } + + call_id.disconnect (); + gdk_threads_add_idle ((int(*)(void*)) delete_widget, + progress_dialog); + }} + .detach (); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/application.cc b/trader-desk/application.cc new file mode 100644 index 0000000..8f259ee --- /dev/null +++ b/trader-desk/application.cc @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +namespace DMBCS::Trader_Desk { + + + void Application::display_grid (size_t const &g) + { + if (notebook.get_current_page () == 0) + { + hand_analysis->chart.data.update_extremes (Chart_Grid::DEFAULT_SPAN); + hand_analysis->chart.data.return_subsumed (); + } + + notebook.set_current_page (g + 1); + + close_data_menu->set_sensitive (1); + last_data_menu->set_sensitive (0); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/application.h b/trader-desk/application.h new file mode 100644 index 0000000..127d15a --- /dev/null +++ b/trader-desk/application.h @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2017 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__APPLICATION__H +#define DMBCS__TRADER_DESK__APPLICATION__H + + +#include +#include + + +/** \file + * + * Declaration of the \c Application class. */ + + +namespace DMBCS::Trader_Desk { + + + /** The glue that binds everything together, really half the + * implementation of the top-level \c Window defined in + * trader-desk.cc. + * + * [This is still a work in progress. Ultimately we want this \c + * Application class to be independent of the (GTK) GUI front-end, so + * need to change the split in functionality between this class and the + * \c Window class accordingly.] */ + + struct Application + { + /* The following are established outside of this class, and it is + * expected that this be done as soon as the class is constructed. */ + + /** *The* set of user preferences. May change at infrequent but + * random times. */ + Preferences user_prefs; + + /** The most top-level window of this application. */ + Gtk::Window *window {nullptr}; + + /** A function we can push to Gtk::Idle when we need to recourse to + * the preferences dialog due to a problem in the preferences. */ + function preferences_error; + + + + /** The part of the menu which is specific to the markets whose data + * we have loaded. */ + Glib::RefPtr last_data_menu; + Glib::RefPtr close_data_menu; + + /** All of the actions available to the menu. */ + Glib::RefPtr actions; + + /** The menu manager. */ + Glib::RefPtr ui_manager; + + /** The notebook which appears, without tabs, as the main widget in + * the \c window. */ + Gtk::Notebook notebook; + + /** All of the data, organized into a list of grids of charts. */ + vector> market_grids; + + /** The widget which shows detailed analysis of a particular companyʼs + * data, and allows for much interaction with the user. */ + unique_ptr hand_analysis; + + + + /** The application is not in a good state until the constructor has + * run, user_prefs have been updated with fixed-up values (by virtue + * of the wizard), and create_database() is run. + * + * !!!! This is far from ideal. */ + explicit Application (Preferences&& P) : user_prefs {move (P)} + {} + + + /** This method is used to *change* the grid being displayed, + * according to their \a position amongst the notebook pages, + * i.e. counting from one upwards. */ + void display_grid (size_t const &position); + + + /** Mega-method which does all the work (including operating the + * display machinery) to get a new market working in the system. */ + void ingest_new_market (); + + + /** Mega-method which does all the work to bring a marketʼs data up to + * date. */ + void update_closing_prices (); + + + } ; /* End of class Application. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__APPLICATION__H. */ diff --git a/trader-desk/chart-context.cc b/trader-desk/chart-context.cc new file mode 100644 index 0000000..1bfd0d0 --- /dev/null +++ b/trader-desk/chart-context.cc @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + + +namespace DMBCS::Trader_Desk { + + + + double Chart_Context::x (Time_Point const &t) const + { + return left_border + + (width - left_border - right_border) + * (t - outline.start_time).count () + / (double) ((outline.end_time - outline.start_time).count ()); + } + + + Time_Point Chart_Context::date (int const &x) const + { + return outline.start_time + + chrono::duration_cast + ((x - left_border) + * (outline.end_time - outline.start_time) + / (double) (width - left_border - right_border)); + } + + + double Chart_Context::y (Currency_Value const &value) const + { + return top_border + (height - bottom_border - top_border) + * (1.0 - (value - outline.min_value) + / (outline.max_value - outline.min_value)); + } + + + + Currency_Value Chart_Context::value (int const &y) const + { + return outline.max_value + - (outline.max_value - outline.min_value) + * (y - top_border) + / (double) (height - top_border - bottom_border); + } + + + + void Chart_Context::draw_time_series (Time_Series const &series, + Colour const &colour, + double const &alpha) const + { + if (series.size () < 2) + return; + + set_source_rgb (colour, alpha); + + auto i = begin (series); + + move_to (*i++); + + while (i != end (series) && i->time >= outline.start_time) + line_to (*i++); + + cairo->stroke (); + } + + + + void Chart_Context::add (Text &text, + string const &message, + Colour const &colour, + Text::Place const &position) + { + pango->set_text (message); + + Pango::Rectangle const r {pango->get_pixel_logical_extents ()}; + + text += Text::Item {message, + colour, + position, + {(double) r.get_width (), (double) r.get_height ()}}; + } + + + + void Chart_Context::render (Text &text) + { + for (auto const &t : text.arrange ({left_border, width - right_border, + top_border, height - bottom_border})) + { + cairo->rectangle (t.placement.x, t.placement.y, + t.size.x, t.size.y); + + set_source_rgb (Colour::MARK_LABEL_BACK, 0.5); + + cairo->fill (); + + pango->set_text (t.text); + + cairo->move_to (t.placement.x, t.placement.y); + + pango->add_to_cairo_context (cairo); + + set_source_rgb (t.colour, 0.7); + + cairo->fill (); + } + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/chart-context.h b/trader-desk/chart-context.h new file mode 100644 index 0000000..85f783a --- /dev/null +++ b/trader-desk/chart-context.h @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__CHART_CONTEXT__H +#define DMBCS__TRADER_DESK__CHART_CONTEXT__H + + +#include +#include +#include +#include + + +/** \file + * + * Declaration of the \c Chart_Context class. */ + + +namespace DMBCS::Trader_Desk { + + + /** A wrapper around a Cairo context: a canvas on which to draw things. + * Methods are provided for drawing lines and filling areas represented + * relative to real times and prices, and for laying down and rendering + * small items of text. + * + * The class is really just an implementation detail of \c Chart + * (though references are passed out and used in several other + * classes), and is instantiated only transiently in that class; there + * is not a proper constructor but provision for the \c Chart class to + * get an uninitialized object and for that class to then perform full + * initialization. */ + + struct Chart_Context + { + /** The `canvas' on which the chart is painted. */ + Cairo::RefPtr cairo; + + /** An object which helps with painting text onto the above canvas. */ + Glib::RefPtr pango; + + /** The number of pixels between the left edge of the window and the + * side of the chart. */ + double left_border; + + /** The number of pixels between the right-hand side of the chart and + * the window edge. */ + double right_border; + + /** The number of pixels above the chart. */ + double top_border; + + /** The number of pixels below the chart. */ + double bottom_border; + + /** The width of the chart, in pixels. */ + double width; + + /** The height of the chart, in pixels. */ + double height; + + /** The (date, price) extremes of the chart. */ + Time_Series::Range outline; + + /** The collection of text strings which will be placed on the chart + * when it is rendered. */ + Text text; + + + /** The application has a single (transient) instance of this class, + * created and initialized (constructed) within Chart::on_expose. We + * provide that method with the means to make an uninitialized + * object, and then prohibit any copying or moving of that object. */ + Chart_Context () = default; + + Chart_Context (Chart_Context const &) = delete; + Chart_Context &operator= (Chart_Context const &) = delete; + Chart_Context (Chart_Context &&) = delete; + Chart_Context &operator= (Chart_Context &&) = delete; + + + + /** Plot the chart. */ + void draw_time_series (Time_Series const &series, + Colour const &colour, + double const &alpha) const; + + + /** Move the `pen' to the position on the chart corresponding to the + * \a event. */ + void move_to (Event const &event) const + { cairo->move_to (x (event.time), y (event.price)); } + + /** Draw a line on the chart to the position corresponding to \a + * event. */ + void line_to (Event const &event) const + { cairo->line_to (x (event.time), y (event.price)); } + + + /** Set the colour for the subsequent drawing operations. */ + void set_source_rgb (Colour const &c, double const &alpha) const + { cairo->set_source_rgba (c.red, c.green, c.blue, alpha); } + + /** Ditto. */ + void set_source_rgb (Colour const &c) const + { cairo->set_source_rgb (c.red, c.green, c.blue); } + + + /* While this class is designed to present the application with an + * interface expressed entirely in terms of stock market \c Event + * points, when we are placing text onto the charts it is necessary + * to expose the raw coordinates, for reasons involving the + * displacement of the text position to allow for size and anchor + * orientation. */ + + Time_Point date (int const &x) const; + Currency_Value value (int const &y) const; + + double x (Time_Point const &t) const; + double y (Currency_Value const &value) const; + + Text::Place x (Event const &e) const + { return {x (e.time), y (e.price)}; } + + + + /** Add the \a message into the \a text object. */ + void add (Text &text, + string const &message, + Colour const &, + Text::Place const &); + + /** Render all the messages which have previously been added to the \a + * text object (these will be re-arranged so that there are no + * overlaps). */ + void render (Text &); + + + }; /* End of class Chart_Context. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__CHART_CONTEXT__H. */ diff --git a/trader-desk/chart-data.cc b/trader-desk/chart-data.cc new file mode 100644 index 0000000..771f0d1 --- /dev/null +++ b/trader-desk/chart-data.cc @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include + + +namespace DMBCS::Trader_Desk { + + + constexpr const bool Chart_Data::NO_SIGNAL; + constexpr const Currency_Value Chart_Data::NO_POSITION; + + +void Chart_Data::timeseries__change_span (DB& db, const Duration& window) +try + { + const auto start {TODAY_MARK - window}; + + bool test; + {lock_guard l {prices_mutex}; + test = prices.empty () ? 1 : start < prices.back ().time; + } + + if (test) + { + reap_prefetch (); + if (last_fetch_time == Time_Point {} || start < last_fetch_time) + { + prices.extend_range (db, company_seqid, window); + last_fetch_time = start; + } + } + + const Time_Series::Range hold {extremes}; + update_extremes (window); + /* We are always in the GTK thread. */ + if (hold != extremes) changed_signal.emit (); + } +catch (Mysql::DB_Connection::Exception&) {} + + + +static void new_timeseries (Chart_Data *const CD, + DB& db, + const Duration& window, + const Duration& market_close_time) + { + CD->kill_prefetch (); + CD->reap_prefetch (); + + const auto t {chrono::system_clock::now ()}; + + const auto immediate_window + {min (chrono::duration_cast (window), + chrono::hours {24*50})}; + + CD->prices + = Time_Series::from_database + (db, CD->company_seqid, t, immediate_window, market_close_time); + + CD->last_fetch_time = t - immediate_window; + + CD->update_extremes (window); + CD->changed_signal.emit (); + + if (window != immediate_window) + CD->prefetch_ (db.current_preferences, {window}); + } + +void Chart_Data::new_company (DB& db, + const int company_seqid_, + const string& name, + const Duration& window, + const Duration& market_close_time) + { + reap_prefetch (); + + return_subsumed (); + subsumed_object = nullptr; + + company_seqid = company_seqid_; + company_name = name; + + prices = Time_Series {market_close_time}; + extremes = Time_Series::Range {}; + extremes.start_time = chrono::system_clock::now () - window; + + last_fetch_time = Time_Point {}; + new_timeseries (this, db, window, market_close_time); + new_company_signal.emit (); + } + + + +extern "C" int emit_changed_signal (gpointer signal) + { + ((sigc::signal*) signal)->emit (); + return 1; + } + + + +static void do_prefetch + (DB& db, Chart_Data& CD, const vector span) + { + CD.prefetch_thread_stop = false; + + for (const Duration& s : span) + while (CD.last_fetch_time > TODAY_MARK - s) + { + const auto this_time + {max (CD.last_fetch_time - chrono::hours {24}*500, + TODAY_MARK - s)}; + + {lock_guard l {CD.prices_mutex}; + CD.prefetch_series = new Time_Series {CD.prices}; + } + + CD.prefetch_series->extend_range + (db, CD.company_seqid, TODAY_MARK - this_time); + + bool need_refresh {0}; + + {lock_guard l {CD.prices_mutex}; + CD.prices = move (*CD.prefetch_series); + CD.prefetch_series = nullptr; + need_refresh = (CD.last_fetch_time > CD.extremes.start_time); + CD.last_fetch_time = this_time; + } + + if (CD.prefetch_thread_stop) return; + + if (need_refresh) + { + CD.update_extreme_prices (); + + /* !!!! Unfortunate that this appears here--and the gtkmm + * header--(we have nothing to do with the graphics + * plane). Funny that it doesn't work at the point where + * we actually enter the graphics plane, but I guess + * other parts of the system react to this signal and + * they in turn will trigger the graphics plane. We are + * going to have to investigate all the points where + * signals are emitted! (Maybe it is just occurrences of + * this one particular signal?) */ + + /* ALWAYS outside the GTK thread. */ + gdk_threads_add_idle (emit_changed_signal, &CD.changed_signal); + } + } + } + +void Chart_Data::prefetch_ (Preferences& P, + const vector& span) + { + if (! prefetch_thread) + prefetch_thread.reset (new thread ([this, span, &P] + { DB db {P}; + do_prefetch (db, *this, span); })); + } + + + +void Chart_Data::reap_prefetch () + { + if (! prefetch_thread) return; + + prefetch_thread->join (); + prefetch_thread.reset (); + } + + + +void Chart_Data::update_extreme_prices () + { + lock_guard l {prices_mutex}; + + const auto extremes_ + {prices.get_range (extremes.end_time - extremes.start_time)}; + + extremes.min_value = extremes_.min_value; + extremes.max_value = extremes_.max_value; + } + + + +void Chart_Data::note_current_price (DB& db, Currency_Value const &value) + { + {lock_guard l {prices_mutex}; + + latest_price = {chrono::system_clock::now (), value}; + + /* Expensive! */ + prices.insert (begin (prices), latest_price); + + extremes.end_time = latest_price.time; + } + + update_extreme_prices (); + + db.quick () + << "update company set last_price=" << latest_price.price + << ", last_price_date=from_unixtime(" + << number (latest_price.time.time_since_epoch ()) + << ") where seqid=" << company_seqid; + + /* Always in GTK thread. */ + changed_signal.emit (); + } + + + +void Chart_Data::subsume (Chart_Data *const c) + { + return_subsumed (); + + subsumed_object = c; + { + lock_guard l {c->prices_mutex}; + lock_guard m {prices_mutex}; + + extremes = c->extremes; + prices = c->prices; + last_fetch_time = c->last_fetch_time; + } + + company_seqid = c->company_seqid; + company_name = c->company_name; + latest_price = c->latest_price; + + changed_signal.emit (); + new_company_signal.emit (); + } + + + +void Chart_Data::return_subsumed () + { + if (! subsumed_object) return; + + kill_prefetch (); + reap_prefetch (); + + { + lock_guard l {subsumed_object->prices_mutex}; + lock_guard m {prices_mutex}; + + subsumed_object->extremes = extremes; + subsumed_object->latest_price = latest_price; + subsumed_object->prices = prices; + subsumed_object->last_fetch_time = last_fetch_time; + } + + subsumed_object = nullptr; + } + + + +void Chart_Data::new_event (const Event& e, const bool& no_signal) + { + { + lock_guard l {prices_mutex}; + prices.insert_event (latest_price = e); + } + + if (e.time <= extremes.start_time) return; + + extremes.end_time = max (extremes.end_time, e.time); + + extremes.min_value = min (extremes.min_value, e.price); + extremes.max_value = max (extremes.max_value, e.price); + + if (! no_signal) { unaccurate = 0; + changed_signal.emit (); } + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/chart-data.h b/trader-desk/chart-data.h new file mode 100644 index 0000000..c6ebac3 --- /dev/null +++ b/trader-desk/chart-data.h @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__CHART_DATA__H +#define DMBCS__TRADER_DESK__CHART_DATA__H + + +#include +#include +#include +#include + + +namespace DMBCS::Trader_Desk { + + + /** This class looks after the data which form the graph across a + * chart. It has several features. + * + * 1) It holds more data than we need, on the off-chance it might be + * wanted later. + * + * 2) It actually can get as much data as are available in the database + * in a background thread, so that they are available without delay. + * + * 3) One \c Chart_Data object is able to take control (subsume) the + * data of another \c Chart_Data object. This is required as the + * usual object creation/passing paradigms do not work when objects + * are also widgets visible on-screen. */ + + struct Chart_Data + { + /** A placebo value to indicate that no actual position on the chart + * is used. */ + static constexpr Currency_Value const NO_POSITION {-1.0}; + + + /** The database's unique identifier for this company. */ + int company_seqid; + + /** A human-readable name string. */ + string company_name; + + /** Access to the prices needs this mutex. */ + mutex prices_mutex; + + /** The actual data we hold. There may be more here than we actually + * need at the present time, i.e. they may go further back in time + * than current analyses demand. */ + Time_Series prices {chrono::seconds {0}}; + + /** The range of data we are interested in. Will be a subset of the + * range of \c prices. */ + Time_Series::Range extremes; + + /** A thread to background-fetch more data. */ + unique_ptr prefetch_thread; + + /** A holding place for the background thread to do its work. */ + Time_Series *prefetch_series {nullptr}; + + /** When this goes \c TRUE, that is taken as a signal to a background + * thread to abandon its operations. */ + bool prefetch_thread_stop {false}; + + /** If true we consider the entire time-series to be inaccurate, and + * show it pink. Usually used when we are in the process of updating + * the latest known prices. */ + bool unaccurate {0}; + + /** We can't rely on the prices time-series to tell us the earliest + * datum requested from the database, because the database might not + * have gone back that far, and we don't want to keep requesting data + * that don't exist. Hence we keep this record here. */ + Time_Point last_fetch_time; + + /* !! managed entirely outside the class. */ + /** The number of shares for which analytical data are computed and + * shown. */ + unsigned number_shares {0}; + + /** The point in the data space at which a current position was + * opened. */ + Event open_position {0, NO_POSITION}; + + /** If the user hand-enters a price point, we store that here. */ + Event latest_price {0, NO_POSITION}; + + /** If we took the data from another object, we store the source here + * so that it can subsequently be returned. */ + Chart_Data *subsumed_object {nullptr}; + + /** We emit this signal when any aspect of the data are changed. */ + sigc::signal changed_signal; + + /** This signal is emitted after the data have been completely + * subsumed by those for another company. */ + sigc::signal new_company_signal; + + + + /** If the null constructor is used, then a call of new_company is the + * only action that will make any sense; or we could subsume the data + * of another \c Chart_Data object. */ + Chart_Data () = default; + + + /** The sole working constructor. Sets the object up ready for + * action, but does not actually load in any data at this point. */ + Chart_Data (int const s, string const &n, + Duration const &market_close_time) + : company_seqid {s}, + company_name {n}, + prices {market_close_time} + {} + + + /** The destructor simply cleans up all of its resources. */ + ~Chart_Data () + { + reap_prefetch (); + } + + + /** Put a flag up to instruct a running background data pre-fetch + * thread to abandon its work and stop. */ + void kill_prefetch () + { + prefetch_thread_stop = true; + } + + + + /** Kick off a background thread to get data as far back in time as \a + * span. */ + void prefetch_ (Preferences&, const vector& span); + + + /** Wait if necessary for the background thread to finish, and then + * reap the new data into the existing \c prices time-series. */ + void reap_prefetch (); + + + /** Take over the data contained in \a c. This is specifically for + * the case when \a c is a widget on the market thumbnail page, and + * we want to analyze the data in detail in the hand-analysis + * page. */ + void subsume (Chart_Data *const c); + + + /** Give the subsumed data back to the original source, leaving us + * bereft until we subsume someone else's data. */ + void return_subsumed (); + + + /** Assume that the range of prices inside the current range of dates + * has changed, and update the recorded upper and lower bounds. */ + void update_extreme_prices (); + + + /** Re-compute the \c range for the given \a window back from the + * current time. */ + void update_extremes (Duration const &window) + { + lock_guard l {prices_mutex}; + extremes = prices.get_range (window); + } + + + /** Add an event to the time-series corresponding to the \a value at + * the current time. This information is also stored on the company + * record in the database. */ + void note_current_price (DB&, Currency_Value const &value); + + + /** Causes the span of our data to be changed in real time, i.e. not + * in the background thread. Usually, because of the work of the + * background thread, this function will not take very long to + * complete the operation. */ + void timeseries__change_span (DB&, Duration const &window); + + + /** Re-initialize the class to hold the data of company with database + * identifier \a company_seqid. The data filling a period \a window + * to the start of the current day will be obtained as quickly as + * possible, but note that this might not happen as soon as the + * function returns; a large window will be broken up and the data + * progressively retrieved in a background thread. */ + void new_company (DB&, + const int company_seqid, + const string& name, + const Duration& window, + const Duration& market_close_time); + + + /** Flag for the following method. */ + static constexpr bool const NO_SIGNAL {1}; + + /** Insert the new datum \a e into the \c prices time-series, and + * update the extremes if the new point is more recent than the start + * of the current extremes (the usual case). A \c changed_signal + * will be emitted unless \c NO_SIGNAL is passed as the second + * argument. */ + void new_event (Event const &e, bool const &no_signal = 0); + + + }; /* End of class Chart_Data. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__CHART_DATA__H. */ diff --git a/trader-desk/chart-grid.cc b/trader-desk/chart-grid.cc new file mode 100644 index 0000000..f3b23fe --- /dev/null +++ b/trader-desk/chart-grid.cc @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2017 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +/** \file + * + * Implementation of the \c Chart_Grid class. */ + + +namespace DMBCS::Trader_Desk { + + + constexpr chrono::hours const Chart_Grid::DEFAULT_SPAN; + + +Chart_Grid::Chart_Grid (Preferences& P, + const Market_Meta_Data& m) + : user_prefs {P}, market {m} + { + add_events (Gdk::BUTTON_RELEASE_MASK); + add (table); + DB db {user_prefs}; + regenerate (db, P, 1 /* force */); + } + + + +Chart* Chart_Grid::find_chart (const int& company_seqid) + { + auto const c = find_if (begin (chart), end (chart), + [company_seqid] (unique_ptr const &x) + { return x->data.company_seqid + == company_seqid; }); + + return c != end (chart) ? c->get () : nullptr; + } + + + +void Chart_Grid::regenerate (DB& db, Preferences& P, const bool force) + { + if (force || update_components (market, + db, + (Gtk::Window*) get_toplevel ())) + { + table.resize (1, 1); + chart.clear (); + + auto sql = db.row_query (); + + sql << " select seqid, rtrim(name) " + << " from company " + << " where market=" << market.seqid + << " order by name asc"; + + sql.execute (); + + int number_columns = int (ceil (sqrt (sql.number_rows ()))); + + chart.reserve (sql.number_rows ()); + + int row {0}; + int column {0}; + + for (; sql; ++sql) + { + const auto seqid {sql.next_entry ()}; + + chart.emplace_back (new Chart {Chart::Style::THUMB, P}); + + /* We fix up the zero duration when the widget is exposed, so + * that we donʼt delay getting the application started by + * pre-loading tons of data. */ + chart.back ()->data.new_company (db, + seqid, + sql.next_entry (), + chrono::hours {50*24}, + market.world_data.close_time); + + table.attach (*chart.back (), column, column + 1, row, row + 1); + + if (0 == (column = (column+1) % number_columns)) ++row; + } + + show_all (); + } + } + + + +bool Chart_Grid::on_draw (const Cairo::RefPtr& C) + { + unique_ptr db; + + for (auto &c : chart) + { + if (c->data.extremes.start_time == c->data.extremes.end_time) + { + if (! db) db = make_unique (user_prefs); + c->data.timeseries__change_span (*db, DEFAULT_SPAN); + } + else + table.propagate_draw (*c, C); + } + + return 1; + } + + + +bool Chart_Grid::on_button_release_event (GdkEventButton *const event) + { + if (chart.empty ()) return 0; + + const Gtk::Allocation alloc {chart.front ()->get_allocation ()}; + + guint rows, columns; + table.get_size (rows, columns); + + const int index = int (event->x) / alloc.get_width () + + columns * (int (event->y) / alloc.get_height ()); + + if (index >= 0 && index < int (chart.size ())) + { + selection = chart [index].get (); + selection_signal.emit (); + } + + return 1; + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/chart-grid.h b/trader-desk/chart-grid.h new file mode 100644 index 0000000..d8649e7 --- /dev/null +++ b/trader-desk/chart-grid.h @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__CHART_GRID__H +#define DMBCS__TRADER_DESK__CHART_GRID__H + + +#include +#include + + +/** \file + * + * Declaration of the \c Chart_Grid class. */ + + +namespace DMBCS::Trader_Desk { + + + /** Widget which manages a whole bunch of charts, for all companies in a + * market, and displays them all at once in a grid on the screen. */ + + struct Chart_Grid : Gtk::EventBox + { + Preferences& user_prefs; + + /** The duration displayed in each thumbnail. */ + static constexpr chrono::hours const DEFAULT_SPAN {50 * 24}; + + /** Object packed with nothing but \c Chart widgets. */ + Gtk::Table table; + + /** All the \c Chart's we are managing. */ + vector > chart; + + /** Pointer into the \c chart list: the one we last clicked on. */ + Chart *selection {nullptr}; + + /** We emit this whenever the user clicks on a chart in the grid. */ + sigc::signal selection_signal; + + /** The market we hold and display charts for. */ + Market_Meta_Data market; + + + /** Sole constructor, gives us a fully operational object. */ + Chart_Grid (Preferences&, const Market_Meta_Data&); + + /** Find the chart corresponding to the company with the database \a + * seqid, or return \c nullptr. */ + Chart *find_chart (int const &seqid); + + /** Completely re-construct this object based on the data currently in + * the database. If \a force is TRUE, then this object will be + * constructed according to the information in the database; if \a + * force is FALSE then the database may be updated with new + * information about the market components, and if a change is made + * in these then this object will be refreshed. */ + void regenerate (DB &, Preferences&, bool const force = 0); + + /** Make sure all the individual charts have their data loaded before + * we attempt to render them on-screen. */ + bool on_draw (const Cairo::RefPtr&) override; + + /** Called when the user selects an individual chart. We update our + * state and emit the \c selection_signal. */ + bool on_button_release_event (GdkEventButton *const) override; + + + }; /* End of class Chart_Grid. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__CHART_GRID__H. */ diff --git a/trader-desk/chart.cc b/trader-desk/chart.cc new file mode 100644 index 0000000..d35e146 --- /dev/null +++ b/trader-desk/chart.cc @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include +#include + + +/** \file + * + * Implementation of the \c Chart class. */ + + +namespace DMBCS::Trader_Desk { + + + Chart::Chart (uint32_t const features_, Preferences& P) + : features (features_) + { + data . changed_signal . connect ([this] { queue_draw (); }); + + /* Big enough for at least a thumb; we will take up more space if we're + * offered it. */ + set_size_request (40, 25); + + if (features & Feature::CROSS_HAIRS) + add_events (Gdk::POINTER_MOTION_MASK | Gdk::LEAVE_NOTIFY_MASK + | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK); + + if (features & Feature::ANALYZERS) + analyzer = make_unique (data, P); + } + + + + bool Chart::on_motion_notify_event (GdkEventMotion *const motion) + { + if (features & Feature::CROSS_HAIRS) + { + if (analyzer) + analyzer->button_move (motion->x, motion->y); + + pointer_x = (int) motion->x; + pointer_y = (int) motion->y; + + queue_draw (); + } + + return 1; + } + + + + bool Chart::on_leave_notify_event (GdkEventCrossing *const) + { + pointer_x = pointer_y = -1; + queue_draw (); + return 1; + } + + + + bool Chart::on_button_press_event (GdkEventButton *const event) + { + return features & Feature::CROSS_HAIRS + && analyzer + && analyzer->button_down (event->x, event->y); + } + + + + bool Chart::on_button_release_event (GdkEventButton *const event) + { + return features & Feature::CROSS_HAIRS + && analyzer + && analyzer->button_up (event->x, event->y); + } + + + + /* !! We really want to paint most of the chart into memory, and map it + * to the screen as required. But we need to re-assess all the + * tide-marks on the cursor line whenever the cursor moves. */ + + bool Chart::on_draw (const Cairo::RefPtr& cairo) + { + /* This object is constructed as we draw the various aspects of the + * chart, and then it is rendered on top of everything else right at + * the end of this method. */ + Tide_Mark::List tide_marks; + + + + /*************** Set up the canvas. **************************/ + + Chart_Context canvas; + + { + const Gtk::Allocation allocation {get_allocation ()}; + canvas.width = allocation.get_width (); + canvas.height = allocation.get_height (); + } + + canvas.left_border = features & (int) Feature::AXIS_LABELS ? 45 : 4; + canvas.bottom_border = features & (int) Feature::AXIS_LABELS ? 40 : 4; + canvas.top_border = 4; + canvas.right_border = 4; + + canvas.cairo = cairo; + + canvas.pango = Pango::Layout::create (canvas.cairo); + canvas.pango -> set_font_description (Pango::FontDescription ("Sans 7")); + + canvas.set_source_rgb (data.unaccurate ? Colour::NO_DATA_REGION + : Colour::CHART_BACKGROUND); + canvas.cairo -> paint (); + + canvas.outline = data.extremes; + + double const space = 0.05 * (canvas.outline.max_value + - canvas.outline.min_value); + + canvas.outline.min_value -= space; + canvas.outline.max_value += space; + + if (analyzer) + analyzer->stretch_outline (canvas.outline); + + auto cursor_mark + = Tide_Mark::price_marker + (canvas.date (pointer_x), + canvas.outline.contains ({ canvas.date (pointer_x), + canvas.value (pointer_y)}) + ? Colour::CURSOR_TIDES + : Colour::NO_DISPLAY); + + auto current_mark = Tide_Mark::price_marker (canvas.outline.end_time, + Colour::CHART_BACKGROUND); + + canvas.cairo->set_line_width (1.0); + + + + /************************ No-data region. *******************************/ + + { + auto const r = data.prices.empty () ? canvas.outline.end_time + : data.prices.back ().time; + + if (r > canvas.outline.start_time) + { + canvas.set_source_rgb (Colour::NO_DATA_REGION); + + canvas.move_to ({canvas.outline.start_time, + canvas.outline.min_value}); + + canvas.line_to ({canvas.outline.start_time, + canvas.outline.max_value}); + + canvas.line_to ({r, canvas.outline.max_value}); + + canvas.line_to ({r, canvas.outline.min_value}); + + canvas.cairo->fill (); + } + } + + + /*********** Let the analyzers draw themselves. *************/ + + if (analyzer) + analyzer->graph_draw_hook (canvas, + tide_marks, + data.number_shares, + {cursor_mark, current_mark}); + + + /******** X-axis *******/ + + if (features & Feature::AXIS_LABELS) + { + canvas.set_source_rgb (Colour::TIME_AXIS); + + char buffer [200]; + + auto const *disc = Date_Axis::discretization; + + time_t const t = chrono::system_clock::to_time_t + (data.extremes.end_time); + + for (; disc->format; ++disc) + { + /* If the spacing between ticks is less than one pixel, + * move on. */ + if (disc->real_interval.count () + / double ((data.extremes.end_time + - data.extremes.start_time).count ()) + * (canvas.width - canvas.left_border + - canvas.right_border) + < 1.0) + continue; + + /* Find the box needed to enclose a label showing the + * current time. */ + strftime (buffer, sizeof (buffer), + disc->format, + localtime (&t)); + + canvas.pango->set_text (buffer); + + Pango::Rectangle const rect + = canvas.pango->get_pixel_logical_extents (); + + /* If the spacing between ticks is more than the space + * required to display the current date-time, then this is + * the tick spacing we will use. */ + if (disc->real_interval.count () + / double ((data.extremes.end_time + - data.extremes.start_time).count ()) + * (canvas.width - canvas.left_border + - canvas.right_border) + > rect.get_width () + 4) + break; + } + + + + auto show_ticks = [&canvas, this] + (Date_Axis::Discretization const &disc, + uint32_t const &line_offset) + { + char buffer [200]; + + for (auto i = data.extremes.start_time; + i <= data.extremes.end_time; + i += disc.interval) + { + i = disc.round_down (i); + + if (i > data.extremes.start_time) + { + auto i_ = chrono::system_clock::to_time_t (i); + + strftime (buffer, sizeof (buffer), + disc.format, + localtime (&i_)); + + canvas.pango->set_text (buffer); + + canvas.move_to ({i, canvas.outline.min_value}); + + canvas.cairo->rel_move_to (0, line_offset + 1); + + canvas.pango->add_to_cairo_context (canvas.cairo); + + canvas.cairo->fill (); + } + } + }; + + + + if (disc->format) + { + show_ticks (*disc, 0); + + if ((disc + 1)->format) + { + show_ticks (*(disc + 1), 11); + + if ((disc + 2)->format) + show_ticks (*(disc + 2), 22); + } + } + } + + + /******** Y-axis ********/ + + canvas.pango + ->set_markup (string {""} + + pgettext ("Label", "Position value (pounds)") + + ""); + + const Pango::Rectangle extents + = canvas.pango->get_pixel_logical_extents (); + + const int font_height = extents.get_height (); + + if (features & (int) Feature::AXIS_LABELS) + { + canvas.cairo->save (); + canvas.cairo->rotate (- M_PI / 2.0); + canvas.cairo->move_to (- (canvas.height - extents.get_width ()) + / 2, + 1); + canvas.pango->add_to_cairo_context (canvas.cairo); + canvas.set_source_rgb (Colour::PRICE_AXIS); + canvas.cairo->fill (); + canvas.cairo->restore (); + } + + if (features & (int) Feature::AXIS_LABELS) + { + double const share_scale = data.number_shares / 100.0; + + double inc = 0.01; + + for (inc = 0.01; + (inc / share_scale) / (canvas.outline.max_value + - canvas.outline.min_value) + * (canvas.height - canvas.bottom_border + - canvas.top_border) + < font_height * 1.0; + inc *= 10.0) ; + + for (double b = inc * (floor (share_scale + * canvas.outline.min_value + / inc) + + 1); + b < share_scale * canvas.outline.max_value; + b += inc) + if (b / share_scale > canvas.outline.min_value) + { + ostringstream out; + out << setw (5) << b; + canvas.pango->set_text (out.str ()); + Pango::Rectangle const extents + = canvas.pango->get_pixel_logical_extents (); + canvas.move_to ({data.extremes.start_time, + b / share_scale}); + canvas.cairo->rel_move_to (- extents.get_width () - 2, + - extents.get_height () / 2); + canvas.pango->add_to_cairo_context (canvas.cairo); + canvas.set_source_rgb (Colour::PRICE_AXIS); + canvas.cairo->fill (); + } + } + + + if (data.prices.empty ()) + { + if (features & Feature::COMPANY_NAME) + canvas.add (canvas.text, + data.company_name, + Colour::COMPANY_NAME_TITLE, + { canvas.left_border, canvas.top_border }); + + canvas.render (canvas.text); + + return 1; + } + + + canvas.set_source_rgb (Colour::PRICE_AXIS); + canvas.move_to ({data.extremes.start_time, canvas.outline.max_value}); + canvas.line_to ({data.extremes.start_time, canvas.outline.min_value}); + canvas.cairo->stroke (); + + canvas.set_source_rgb (Colour::TIME_AXIS); + canvas.move_to ({data.extremes.start_time, canvas.outline.min_value}); + canvas.line_to ({data.extremes.end_time, canvas.outline.min_value}); + canvas.cairo->stroke (); + + { + lock_guard l {data.prices_mutex}; + + canvas.draw_time_series (data.prices, Colour::PRICE_GRAPH, 1.0); + + tide_marks.emplace_back (current_mark (data.prices.front ().price, + Colour::PRICE_TIDES)); + + tide_marks.emplace_back (cursor_mark (data.prices.interpolated_value + (canvas.date (pointer_x)), + Colour::PRICE_TIDES)); + } + + if (features & Feature::CROSS_HAIRS) + tide_marks.emplace_back (cursor_mark (canvas.value (pointer_y), + Colour::CURSOR_TIDES)); + + + /**** Company name ****/ + + if (features & Feature::COMPANY_NAME) + canvas.add (canvas.text, + data.company_name, + Colour::COMPANY_NAME_TITLE, + canvas.x ({data.extremes.start_time, + canvas.outline.max_value})); + + + /***** Tide marks. *****/ + + if (features & Feature::TIDE_MARKS) + { + set dates_shown; + + for (auto const &tide : tide_marks) + if (tide.temporal_colour != Colour::NO_DISPLAY + && tide.time >= canvas.outline.start_time + && tide.time <= canvas.outline.end_time) + { + canvas.cairo + ->set_source_rgb + (1.0 - 0.5 * (1.0 - tide.value_colour.red), + 1.0 - 0.5 * (1.0 - tide.value_colour.green), + 1.0 - 0.5 * (1.0 - tide.value_colour.blue)); + + canvas.move_to ({canvas.outline.start_time, tide.price}); + canvas.line_to ({canvas.outline.end_time, tide.price}); + + canvas.cairo->stroke (); + + ostringstream hold; + hold << tide.price; + + canvas.add (canvas.text, + hold.str (), + tide.value_colour, + {canvas.x (tide.time) + 20, + canvas.y (tide.price) - 13}); + + ostringstream hold_2; + hold_2 << data.number_shares * tide.price / 100.0; + + canvas.add (canvas.text, + hold_2.str (), + tide.value_colour, + {canvas.x (tide.time) - 60, + canvas.y (tide.price) - 13}); + + if (dates_shown.insert (tide.time).second) + { + canvas.set_source_rgb (tide.temporal_colour); + + canvas.move_to ({tide.time, canvas.outline.max_value}); + canvas.line_to ({tide.time, canvas.outline.min_value}); + + canvas.cairo->stroke (); + + struct tm tm; + time_t t = chrono::system_clock::to_time_t (tide.time); + localtime_r (&t, &tm); + + char buffer [200]; + strftime (buffer, sizeof (buffer), "%Y-%m-%d %H:%M", &tm); + + canvas.add (canvas.text, + buffer, + Colour::CURSOR_TIDES, + {canvas.x (tide.time) + 2, + canvas.height - canvas.bottom_border + - 12}); + } + } + } + + canvas.render (canvas.text); + + return 1; + + } /* End of on_draw method. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/chart.h b/trader-desk/chart.h new file mode 100644 index 0000000..8de5f40 --- /dev/null +++ b/trader-desk/chart.h @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__CHART__H +#define DMBCS__TRADER_DESK__CHART__H + + +#include +#include +#include + + +/** \file + * + * Declaration of the \c Chart class. */ + + +namespace DMBCS::Trader_Desk { + + + /** This is a GTK widget which displays a chart complete with all its + * trimmings. Most of the bulk of the implementation deals with the + * gooey details of actually rendering all the parts into the part of + * the screen occupied by the widget, but there is also some code to + * support a stack of analyzers, which will pass along mouse events to + * them if appropriate. */ + + + struct Chart : Gtk::DrawingArea + { + private: + /** Set of selectable ‘trimmings’ which may adorn the chart. */ + struct Feature + { + /** The company name near the top-left corner. */ + uint32_t static constexpr const COMPANY_NAME = 1 << 0; + + /** Titles and tick indicators outside the chart itself. */ + uint32_t static constexpr const AXIS_LABELS = 1 << 1; + + /** Live cursors which follow the mouse around and display numerical + * data at all points where the cross-hairs intersect an + * interesting point. */ + uint32_t static constexpr const CROSS_HAIRS = 1 << 2; + + /** Lines and numerical data highlighting interesting parts of a + * chart. */ + uint32_t static constexpr const TIDE_MARKS = 1 << 3; + + /** Display output of full analysis stack. */ + uint32_t static constexpr const ANALYZERS = 1 << 4; + }; + + public: + /** Combinations of the above features which are enabled for each + * ‘personality’ in which charts appear. */ + struct Style + { + /** The appearance of charts on the market-wide thumbnail + * display. */ + uint32_t static constexpr const THUMB = Feature::COMPANY_NAME; + + /** The appearance of the chart shown in the detailed ‘hand + * analysis’ widget. */ + uint32_t static constexpr const HAND_ANALYSIS = Feature::AXIS_LABELS + + Feature::CROSS_HAIRS + + Feature::TIDE_MARKS + + Feature::ANALYZERS; + }; + + + /** The actual data we are presenting; our actions are driven to a + * large extent from the data_changed signal emitted by this + * object. */ + Chart_Data data; + + /** An object which will embellish our chart with extra analytical + * tools and features. */ + unique_ptr analyzer; + + private: + /** The features we want to embellish our display with: bit-field of \c + * Feature's via \c Style. */ + uint32_t const features; + + /** The last known coordinates of the mouse cursor, when it was over + * our chart; used to convey mouse place into expose() method. */ + int pointer_x, pointer_y; + public: + + /** Sole constructor which partially initializes an object (note in + * particular that no company is specified here). */ + Chart (uint32_t const features_, Preferences&); + + /** A \c Chart is a very heavy object and we do not want to be copying + * or even moving these around. */ + Chart (Chart const &) = delete; + Chart (Chart&&) = delete; + void operator= (Chart const &) = delete; + void operator= (Chart &&) = delete; + + private: + /** Run all of the analyzers, and then render them, the chart, and all + * of its selected trimmings. */ + bool on_draw (const Cairo::RefPtr&) override; + + /** If cross-hairs are live, send the button event to the analyzer + * stack. */ + bool on_button_press_event (GdkEventButton *const) override; + + /** If cross-hairs are live, send the button event to the analyzer + * stack. */ + bool on_button_release_event (GdkEventButton *const) override; + + /** If cross-hairs are live, update all of them and get the chart + * re-drawn. Finally pass the motion event to the analyzers. */ + bool on_motion_notify_event (GdkEventMotion *const motion) override; + + /** If cross-hairs are live, remove them by re-drawing the chart + * without. */ + bool on_leave_notify_event (GdkEventCrossing *const) override; + + + }; /* End of class Chart. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__CHART__H. */ diff --git a/trader-desk/colour.cc b/trader-desk/colour.cc new file mode 100644 index 0000000..397d349 --- /dev/null +++ b/trader-desk/colour.cc @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +namespace DMBCS::Trader_Desk { + + + const Colour Colour::COMPANY_NAME_TITLE {0.0, 0.0, 0.0}; + const Colour Colour::CURSOR_TIDES {0.6, 0.6, 0.6}; + const Colour Colour::PRICE_TIDES {0.4, 0.4, 1.0}; + const Colour Colour::PROFIT_LINE {0.4, 0.4, 0.4}; + const Colour Colour::POSITION_TIDES {0.0, 0.8, 0.0}; + const Colour Colour::CHART_BACKGROUND {1.0, 1.0, 1.0}; + const Colour Colour::MEAN_TIDE {1.0, 0.5, 0.5}; + const Colour Colour::MEAN_GRAPH {1.0, 0.0, 0.0}; + const Colour Colour::SD_ENVELOPE {1.0, 1.0, 0.5}; + const Colour Colour::ENVELOPE_TIDES {0.7, 0.7, 0.0}; + const Colour Colour::PRICE_GRAPH {0.2, 0.2, 0.2}; + const Colour Colour::TIME_AXIS {0.0, 0.0, 1.0}; + const Colour Colour::PRICE_AXIS {0.0, 0.9, 0.0}; + const Colour Colour::MARK_LABEL_BACK {1.0, 1.0, 1.0}; + const Colour Colour::NO_DATA_REGION {1.0, 0.8, 0.8}; + const Colour Colour::POSITIVE_DELTA {0.5, 1.0, 0.5}; + const Colour Colour::NEGATIVE_DELTA {1.0, 0.5, 0.5}; + const Colour Colour::DELTA_VALUE {0.0, 0.0, 0.0}; + + const Colour Colour::NO_DISPLAY {-1.0, -1.0, -1.0}; + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/colour.h b/trader-desk/colour.h new file mode 100644 index 0000000..e39da9b --- /dev/null +++ b/trader-desk/colour.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__COLOUR__H +#define DMBCS__TRADER_DESK__COLOUR__H + + +#include + + +/** \file + * + * Declaration of the \c Colour class. */ + + +namespace DMBCS::Trader_Desk { + + + using namespace std; + + + /** A characteristic given to tide lines and text labels. */ + struct Colour + { + /** Component of colour, each in the range [0.0, 1.0]. */ + float red, green, blue; + + private: + /** Sole constructor, provide fully specified object. */ + constexpr Colour (float const &r, float const &g, float const &b) + : red {r}, green {g}, blue {b} + {} + + public: + static const Colour CURSOR_TIDES; + static const Colour COMPANY_NAME_TITLE; + static const Colour PRICE_TIDES; + static const Colour PROFIT_LINE; + static const Colour POSITION_TIDES; + static const Colour CHART_BACKGROUND; + static const Colour MEAN_TIDE; + static const Colour MEAN_GRAPH; + static const Colour SD_ENVELOPE; + static const Colour ENVELOPE_TIDES; + static const Colour PRICE_GRAPH; + static const Colour TIME_AXIS; + static const Colour PRICE_AXIS; + static const Colour MARK_LABEL_BACK; + static const Colour NO_DATA_REGION; + static const Colour POSITIVE_DELTA; + static const Colour NEGATIVE_DELTA; + static const Colour DELTA_VALUE; + + /** Extra-special value which indicates that this feature should not + * be drawn at all. */ + static const Colour NO_DISPLAY; + + + } ; /* End of class Colour. */ + + + /** Two colours compare equal if they look the same to a human observer. + * Note that comparing anything with \c NO_DISPLAY (apart from \c + * NO_DISPLAY itself) will fail. */ + inline constexpr bool operator== (const Colour &a, const Colour &b) + { + return abs (a.red - b.red) + abs (a.green - b.green) + + abs (a.blue - b.blue) + < 0.01; + } + + + /** The exact inverse of the above function. */ + inline constexpr bool operator!= (const Colour &a, const Colour &b) + { return ! (a == b); } + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__COLOUR__H. */ diff --git a/trader-desk/company-name-entry.cc b/trader-desk/company-name-entry.cc new file mode 100644 index 0000000..c4964d9 --- /dev/null +++ b/trader-desk/company-name-entry.cc @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +namespace DMBCS::Trader_Desk { + + + Company_Name_Entry::Company_Name_Entry (Chart_Data &cd) + : chart_data (cd), + previous_icon {Gtk::Stock::GO_BACK, Gtk::ICON_SIZE_SMALL_TOOLBAR}, + next_icon {Gtk::Stock::GO_FORWARD, Gtk::ICON_SIZE_SMALL_TOOLBAR}, + label {pgettext ("Label", "Company name: ")} + { + Gtk::TreeModel::ColumnRecord column_record; + column_record.add (name); + column_record.add (seqid); + + tree_model = Gtk::ListStore::create (column_record); + + auto completion = Gtk::EntryCompletion::create (); + completion->set_model (tree_model); + completion->set_text_column (name); + entry.set_completion (completion); + + entry.set_width_chars (30); + + previous_company_button . add (previous_icon); + next_company_button . add (next_icon); + + sub_box.pack_start (previous_company_button, Gtk::PACK_SHRINK); + sub_box.pack_start (next_company_button, Gtk::PACK_SHRINK); + + pack_start (sub_box, Gtk::PACK_SHRINK, 20); + pack_start (label, Gtk::PACK_SHRINK, 0); + pack_start (entry, Gtk::PACK_SHRINK, 0); + + previous_company_button.signal_clicked () + .connect ([this] { previous_company_required (); }); + + next_company_button.signal_clicked () + .connect ([this] { next_company_required (); }); + + chart_data . changed_signal + . connect ([this] + { if (chart_data.company_name != entry.get_text ()) + entry.set_text (chart_data.company_name); }); + + /* When the user types/selects a company name, then presses enter... */ + entry . signal_activate () . connect ([this] { do_name_select (); } ); + } + + + + void Company_Name_Entry::read_names (DB& db, + size_t const &market_id, + size_t const &company_id) + { + tree_model->clear (); + + auto sql {db.row_query ()}; + + sql << " select rtrim(name), seqid " + << " from company " + << " where market=" << market_id + << " order by name asc"; + + cursor = begin (tree_model->children ()); + + for (sql.execute (); sql; ++sql) + { + auto row = tree_model->append (); + (*row) [name] = sql.next_entry (); + (*row) [seqid] = sql.next_entry (); + + if ((*row) [seqid] == company_id) + cursor = row; + } + } + + + + void Company_Name_Entry::do_name_select () + { + cursor = find_if (begin (tree_model->children ()), + end (tree_model->children ()), + [this, text=entry.get_text ()] + (Gtk::TreeRow const &x) + { return x [name] == text; }); + + name_change . emit ((*cursor) [seqid]); + } + + + + void Company_Name_Entry::next_company_required () + { + if (cursor != tree_model->children ().end ()) + ++cursor; + + if (cursor == tree_model->children ().end ()) + cursor = begin (tree_model->children ()); + + entry.set_text ((*cursor) [name]); + + name_change.emit ((*cursor) [seqid]); + } + + + + void Company_Name_Entry::previous_company_required () + { + if (cursor == begin (tree_model->children ())) + cursor = end (tree_model->children ()); + + entry.set_text ((*--cursor) [name]); + + name_change.emit ((*cursor) [seqid]); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/company-name-entry.h b/trader-desk/company-name-entry.h new file mode 100644 index 0000000..8fd3e56 --- /dev/null +++ b/trader-desk/company-name-entry.h @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__COMPANY_NAME_ENTRY__H +#define DMBCS__TRADER_DESK__COMPANY_NAME_ENTRY__H + + +#include +#include + + +/** \file + * + * Declaration of the \c Company_Name_Entry class. */ + + +namespace DMBCS::Trader_Desk { + + + /** Drop-down box allowing display and selection of names of companies + * in a given market, plus a couple of little arrows to move backwards + * and forwards through the list. */ + + struct Company_Name_Entry : Gtk::HBox + { + /** The object whose company data we are interested in. */ + Chart_Data &chart_data; + + /** The actual on-screen widget. */ + Gtk::Entry entry; + + /** A place to pack the forwards/backwards arrows together, for + * aesthetic reasons. */ + Gtk::HBox sub_box; + + /** The image on the back button. */ + Gtk::Image previous_icon; + + /** The back button itself. */ + Gtk::Button previous_company_button; + + /** The image on the forward button. */ + Gtk::Image next_icon; + + /** The forward button itself. */ + Gtk::Button next_company_button; + + /** Static label in front of the selection box. */ + Gtk::Label label; + + /** Human-readable company names, part of \c tree_model. */ + Gtk::TreeModelColumn name; + + /** The database sequence ID of the companies, in correspondence with + * the \c names above, part of \c tree_model. */ + Gtk::TreeModelColumn seqid; + + /** The tree model holds a list of all company names and seqids, and + * is used for two purposes: to provide completions to the entry + * input box, and to provide us with an STL-like container of names, + * e.g. for iterating through. */ + Glib::RefPtr tree_model; + + /** Pointer to the currently selected (active) item in the \c + * tree_model. */ + Gtk::TreeModel::iterator cursor; + + /** We emit this signal when the name is changed (selected) by the + * user. */ + sigc::signal name_change; + + + /** Sole constructor which composes the entire composite widget and + * has everything ready for operation. */ + explicit Company_Name_Entry (Chart_Data &); + + /* The various copy and move operations are deleted by default since + * the base class doesn't support them. */ + + /** Refresh the list of company names available to scroll through, and + * in the drop-down box. */ + void read_names (DB&, size_t const &market_id, size_t const &company_id); + + /** The user has clicked the ‘next’ button. */ + void next_company_required (); + + /** The user has clicked the ‘previous’ button. */ + void previous_company_required (); + + /** The user has picked a new company name in the entry widget. */ + void do_name_select (); + + + }; /* End of class Company_Name_Entry. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__COMPANY_NAME_ENTRY__H. */ diff --git a/trader-desk/date-axis.cc b/trader-desk/date-axis.cc new file mode 100644 index 0000000..47ebb26 --- /dev/null +++ b/trader-desk/date-axis.cc @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +namespace DMBCS::Trader_Desk { namespace Date_Axis { + + + inline constexpr time_t unix (Duration const &d) + { + return number (d); + } + + + inline tm *squash (tm *const time, int count) + { + if (count-- > 0) time->tm_sec = 0; + if (count-- > 0) time->tm_min = 0; + if (count-- > 0) time->tm_hour = 0; + if (count-- > 0) time->tm_mday = 1; + if (count-- > 0) time->tm_mon = 0; + + return time; + } + + inline Duration round_count (Duration const &t, int const &count) + { + time_t t_ = unix (t); + + return chrono::seconds (mktime (squash (localtime (&t_), count))); + } + + + Duration round_year (Duration t) { return round_count (t, 5); } + Duration round_month (Duration t) { return round_count (t, 4); } + + + Duration round_week (Duration t) + { + time_t t_ = unix (t); + tm date = *localtime (&t_); + + squash (&date, 3); + + mktime (&date); + + /* Go back to the previous Monday (tm_mday = 1). */ + if (date.tm_wday == 0) + date.tm_mday -= 6; + else + date.tm_mday -= date.tm_wday - 1; + + return chrono::seconds (mktime (&date)); + } + + + Duration round_day (Duration t) { return round_count (t, 3); } + Duration round_hour (Duration t) { return round_count (t, 2); } + Duration round_minute (Duration t) { return round_count (t, 1); } + Duration round_second (Duration t) { return t; } + + +} } /* End of namespace DMBCS::Trader_Desk::Date_Axis. */ diff --git a/trader-desk/date-axis.h b/trader-desk/date-axis.h new file mode 100644 index 0000000..f6b8390 --- /dev/null +++ b/trader-desk/date-axis.h @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__DATE_AXIS__H +#define DMBCS__TRADER_DESK__DATE_AXIS__H + + +#include +#include + + +namespace DMBCS::Trader_Desk { + + + /** Really part of the implementation of \c Chart to help with drawing + * the date axis along the bottom edge. The class provides information + * about the possible spacings between tick marks, and methods which + * round any date down to the nearest tick. */ + + namespace Date_Axis + { + + /** A \c Discretization represents a level of detail to display on a + * time axis. The core concept is the real_interval between tick + * marks, but this is not always consistent (lengths of months, leap + * years), so we consider an increased \c interval which is bigger + * than any interval which might occur and smaller than any double + * interval, and then we use the system's calendar functions to \c + * round_down to the last appropriate real date. */ + + struct Discretization + { + /** Slightly bigger than the largest real interval between ticks + * (remember that months and years vary), but definitely smaller + * than two ticks. */ + Duration interval; + + /** The real interval between ticks. */ + Duration real_interval; + + /** A \c strftime format which provides appropriate labels on the + * ticks. A \c nullptr here represents a sentinel place-holder for + * the end of a list of discretizations. */ + char const *const format; + + /** Function which will round the \a Duration down to the nearest + * tick. */ + Duration (*round_down_) (Duration); + + /** Convenience wrapper around above function. */ + Duration round_down (Duration const &d) { return (*round_down_) (d); } + + /** As above, but round the \c Time_Point \a t down to the nearest + * tick. */ + Time_Point round_down (Time_Point t) const + { + return chrono::system_clock::from_time_t (0) + + (*round_down_) (t.time_since_epoch ()); + } + }; + + + /* The specialized rounding functions. Since we use the C library + * calendar functions, we always work in terms of duration (as + * seconds) since the Unix epoch. */ + + Duration round_year (Duration t); + Duration round_month (Duration t); + Duration round_week (Duration t); + Duration round_day (Duration t); + Duration round_hour (Duration t); + Duration round_minute (Duration t); + Duration round_second (Duration t); + + + namespace C = chrono; + + + /* Null-terminated array of discretization levels which we use, in + * increasing granularity. */ + + static constexpr const Discretization discretization [] + = { { C::seconds (1), + C::seconds (1), + "%H:%M:%S", &round_second }, + { C::seconds (70), + C::seconds (60), + "%H:%M", &round_minute }, + { C::seconds (6 * 60), + C::seconds (5 * 60), + "%H:%M", &round_minute }, + { C::seconds (70 * 60), + C::seconds (60 * 60), + "%H:00", &round_hour }, + { C::seconds (25 * 60 * 60), + C::seconds (24 * 60 * 60), + "%d", &round_day }, + { C::seconds (8 * 24 * 60 * 60), + C::seconds (7 * 24 * 60 * 60), + "%d", &round_week }, + { C::seconds (35 * 24 * 60 * 60), + C::seconds (31 * 24 * 60 * 60), + "%b", &round_month }, + { C::seconds (380 * 24 * 60 * 60), + C::seconds (365 * 24 * 60 * 60), + "%Y", &round_year }, + { C::seconds (0), C::seconds (0), nullptr, nullptr } }; + + + } /* End of namespace Date_Axis. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__DATE_AXIS__H. */ diff --git a/trader-desk/date-range-scale.cc b/trader-desk/date-range-scale.cc new file mode 100644 index 0000000..c0334d6 --- /dev/null +++ b/trader-desk/date-range-scale.cc @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +namespace DMBCS::Trader_Desk { + + +Date_Range_Scale::Date_Range_Scale (Chart_Data &d, Preferences& P) + : Exponential_Scale (d, pgettext ("Label", "Date range = %.0f days"), + Gdk::RGBA {"#0000ff"}, + Gdk::RGBA {"#aaaaff"}, + Gdk::RGBA {"#6666ff"}, + 1, 6 * 30, P.time_horizon * 365, + 50 /* Initial setting. */), + db {P} + { + d . changed_signal . connect ([this] { on_data_changed (); }); + + value_adjustment -> signal_value_changed () + . connect ([this] { on_value_changed (); }); + } + + + + void Date_Range_Scale::on_value_changed () + { + db.check_connection (); + data.timeseries__change_span + (db, chrono::hours {24} * int (value ()->get_value ())); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/date-range-scale.h b/trader-desk/date-range-scale.h new file mode 100644 index 0000000..c07d3d3 --- /dev/null +++ b/trader-desk/date-range-scale.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__DATE_RANGE_SCALE__H +#define DMBCS__TRADER_DESK__DATE_RANGE_SCALE__H + + +#include + + +namespace DMBCS::Trader_Desk { + + + /** A \c Scale which controls the visual span of a \c Time_Series \c + * Chart. */ + + struct Date_Range_Scale : Exponential_Scale + { + DB db; + + /** Sole constructor which provides a fully functioning + * object. */ + Date_Range_Scale (Chart_Data &, Preferences&); + + /** Called when the user slides the graphical slider. */ + void on_value_changed (); + + }; /* End of class Date_Range_Scale. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__DATE_RANGE_SCALE__H. */ diff --git a/trader-desk/db.cc b/trader-desk/db.cc new file mode 100644 index 0000000..1c0a96d --- /dev/null +++ b/trader-desk/db.cc @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include + + +namespace DMBCS::Trader_Desk { + + + DB::DB (Preferences& P) : DB_Connection {P}, + current_preferences {P} + { + + /* Not sure if there is some proper way to do this, but things + * currently donʼt work unless we drop the current connection to the + * database and make a new one. */ + /* reconnect (P); */ + } + + + + DB& DB::check_connection () + { + if (! database_equal (current_preferences, last_preferences)) + { + last_preferences = current_preferences; + DB_Connection::reconnect (current_preferences); + } + return *this; + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/db.h b/trader-desk/db.h new file mode 100644 index 0000000..ac0a619 --- /dev/null +++ b/trader-desk/db.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__DB__H +#define DMBCS__TRADER_DESK__DB__H + + +#include + + +/** \file + * + * Declaration of the DB class. */ + + +namespace DMBCS::Trader_Desk { + + + /** Wrapper around a database connection which ensures that the tables + * we need are in place. */ + /* Currently hard-wired around MySQL, but the intention is to + * abstract the database back-end. */ + + struct DB : Mysql::DB_Connection + { + Preferences& current_preferences; + Preferences last_preferences; + + + /** Establish a connection to the RDBMS, check for our database, + * create and pre-populate tables if necessary. May throw an \c + * Exception object. */ + DB (Preferences&); + + + DB& check_connection (); + + + } ; /* End of class DB. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__DB__H. */ diff --git a/trader-desk/delta-analyzer.cc b/trader-desk/delta-analyzer.cc new file mode 100644 index 0000000..e3c12be --- /dev/null +++ b/trader-desk/delta-analyzer.cc @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +namespace DMBCS::Trader_Desk { + + + Delta_Analyzer::Delta_Analyzer (Chart_Data &cd) + { + cd . new_company_signal + . connect ([this] { end_place = start_place; }); + } + + + + void Delta_Analyzer::graph_draw_hook + (Chart_Context &canvas, + Tide_Mark::List &, + unsigned number_shares, + vector const &) + { + if (end_place.x != start_place.x || end_place.y != start_place.y) + { + if (mouse_active) + { + delta_region.start = {canvas.date (start_place.x), + canvas.value (start_place.y)}; + + delta_region.end = {canvas.date (end_place.x), + canvas.value (end_place.y)}; + + if (start_place.x > end_place.x) + swap (delta_region.start, delta_region.end); + } + + delta_region.render (canvas, number_shares); + } + } + + + + bool Delta_Analyzer::button_down (int const x, int const y) + { + end_place = start_place = {x, y}; + mouse_active = 1; + return 1; + } + + + + bool Delta_Analyzer::button_move (int const x, int const y) + { + if (! mouse_active) return 0; + end_place = {x, y}; + return 1; + } + + + + bool Delta_Analyzer::button_up (int const, int const) + { + if (! mouse_active) return 0; + + mouse_active = 0; + + /* Needed to remove the box from the screen if the mouse was simply + * clicked and not moved. */ + redraw_needed . emit (); + + return 1; + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/delta-analyzer.h b/trader-desk/delta-analyzer.h new file mode 100644 index 0000000..b0aad49 --- /dev/null +++ b/trader-desk/delta-analyzer.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__DELTA_ANALYZER__H +#define DMBCS__TRADER_DESK__DELTA_ANALYZER__H + + +#include +#include +#include + + +namespace DMBCS::Trader_Desk { + + + struct Delta_Analyzer : Analyzer + { + /** Transparent structure to record mouse positions in the GTK + * window. */ + struct Place { int x, y; }; + + /** The mouse coordinates of the starting corner of the delta region + * (where the mouse button was first pressed). */ + Place start_place {-1, -1}; + + /** The mouse coordinates of the end corner of the delta region. If + * this equals \c start_place then the delta region is deemed not + * active. */ + Place end_place {-1, -1}; + + /** Indicates if the mouse is currently being used to define the + * bounds of the delta rectangle. */ + bool mouse_active {0}; + + /** Triggered internally if we need the system to re-draw the chart + * area. */ + sigc::signal redraw_needed; + + /** The object which we are controlling, which is responsible for + * actually displaying the delta region and its computed parameters + * on a chart canvas. */ + Delta_Region delta_region { Colour::POSITIVE_DELTA, + Colour::NEGATIVE_DELTA }; + + + /** Set up for operations, and watch the \a chart_data for company + * changes when we must ‘blank’ our on-screen presence. */ + explicit Delta_Analyzer (Chart_Data &chart_data); + + + /** If there is anything to draw then set up \c delta_region if + * necessary and call through that object to get the actual work of + * drawing the region done. */ + void graph_draw_hook (Chart_Context &context, + Tide_Mark::List &, + unsigned number_shares, + vector const &) override; + + + /** Provide our signal handler. */ + sigc::signal &signal_redraw_needed () override + { return redraw_needed; } + + + /** Note that the mouse is now active and set (both) the corners of + * the delta region to the position of the mouse. */ + bool button_down (int const x, int const y) override; + + + /** If the mouse is active, set the end point of the delta region + * definition to the mouse position. */ + bool button_move (int const x, int const y) override; + + + /** If the mouse is active, set it to inactive and force a re-draw in + * case the region no longer should be displayed, e.g. if the mouse + * was just clicked. */ + bool button_up (int const x, int const y) override; + + + } ; /* End of class Delta_Analyzer. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__DELTA_ANALYZER__H. */ diff --git a/trader-desk/delta-region.cc b/trader-desk/delta-region.cc new file mode 100644 index 0000000..2ac8e6b --- /dev/null +++ b/trader-desk/delta-region.cc @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include + + +/** \file + * + * Implementation of the \c Delta_Region class. */ + + +namespace DMBCS::Trader_Desk { + + + template + static string readable_time_span (DURATION const &d) + { + namespace C = chrono; + + ostringstream hold; + + hold << fixed << setprecision (2); + + if (d < C::hours {48}) + hold << (number (d) / 60.0) + << " hours"; + else if (d < C::hours {10 * 24}) + hold << (number (d) / 24.0) + << " days"; + else + hold << (number (d) / (7.0 * 24.0)) + << " weeks"; + + return hold.str (); + } + + + + void Delta_Region::render (Chart_Context &canvas, + unsigned const &number_shares) const + { + canvas.set_source_rgb (start.price > end.price ? negative_colour + : positive_colour); + + canvas.move_to (start); + canvas.line_to ({end.time, start.price}); + canvas.line_to (end); + canvas.line_to ({start.time, end.price}); + canvas.line_to (start); + canvas.line_to (end); + canvas.cairo->stroke (); + + canvas.set_source_rgb (start.price > end.price ? negative_colour + : positive_colour, + 0.2); + + canvas.move_to (start); + canvas.line_to ({end.time, start.price}); + canvas.line_to (end); + canvas.line_to ({start.time, end.price}); + canvas.cairo->fill (); + + ostringstream percentage; + percentage << fixed << setprecision (2) + << (end.price - start.price) / start.price * 100.0 << '%'; + + canvas.add (canvas.text, percentage.str (), Colour::DELTA_VALUE, + canvas.x ({end.time, (start.price + end.price) / 2.0})); + + ostringstream delta; + delta << fixed << setprecision (2) << end.price - start.price; + + canvas.add (canvas.text, delta.str (), Colour::DELTA_VALUE, + canvas.x ({end.time, (start.price + end.price) / 2.0})); + + ostringstream cost; + cost << fixed << setprecision (2) + << number_shares * (end.price - start.price) / 100.0; + + canvas.add (canvas.text, cost.str (), Colour::DELTA_VALUE, + {canvas.x (start.time) - 60, + canvas.y ((start.price + end.price) / 2.0)}); + + canvas.add (canvas.text, readable_time_span (end.time - start.time), + Colour::DELTA_VALUE, + { (canvas.x (start.time) + canvas.x (end.time)) / 2, + canvas.y (min (start.price, end.price))}); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/delta-region.h b/trader-desk/delta-region.h new file mode 100644 index 0000000..fcf40ef --- /dev/null +++ b/trader-desk/delta-region.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__DELTA_REGION__H +#define DMBCS__TRADER_DESK__DELTA_REGION__H + + +#include +#include + + +/** \file + * + * Declaration of the \c Delta_Region class. */ + + +namespace DMBCS::Trader_Desk { + + + /** A \c Delta_Region is a rectangle marked out in (time, price) space + * with the intention of displaying the differences (deltas) in time + * and price along the edges and giving an indication of the rate of + * change of the price with time. These are the quantities which + * represent a trader‘s gains or losses, actual or potential. + * + * The class is only half-autonomous, it holding the extreme values + * in (time, price) space but relying on the application to actually + * provide and manipulate these values as required (in practice this + * requirement is met by the \c Delta_Analyzer class). */ + + struct Delta_Region + { + /** The colour we use when \c start.price is _less_ than \c + * end.price. The border will be this solid colour, and the fill + * will be the same but with a large fraction of transparency. */ + Colour positive_colour; + + /** The alternative to the above colour which we use when \c + * start.price is _more_ than \c end.price. */ + Colour negative_colour; + + /** The position of the defining point on the left (earliest time) + * edge of the rectangle. */ + Event start; + + /** The position of the defining point on the right (latest time) + * edge of the rectangle. */ + Event end; + + + + /** Partial class constructor which establishes the colours that + * will be used to render any delta regions. + * + * It is left to the application to complete the specification of + * the object: viz the \c start and \c end points. */ + Delta_Region (Colour const &p, Colour const &n) + : positive_colour {p}, negative_colour {n} + {} + + + /** Partial class constructor which establishes a uni-colour + * realization of the object on a chart. + * + * It is left to the application to complete the specification of + * the object: viz the \c start and \c end points. */ + Delta_Region (Colour const &p) : Delta_Region {p, p} + {} + + + /** Show the region on the \a canvas along with summary data + * relating to the fact that the display refers to \a number_shares. + * + * Note that the application MUST set the \c start and \c end + * corners of the delta region before calling this method. */ + void render (Chart_Context &canvas, unsigned const &number_shares) const; + + + } ; /* End of class Delta_Region. */ + + + } /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__DELTA_REGION__H. */ diff --git a/trader-desk/hand-analysis-widget.cc b/trader-desk/hand-analysis-widget.cc new file mode 100644 index 0000000..8f8deab --- /dev/null +++ b/trader-desk/hand-analysis-widget.cc @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +namespace DMBCS::Trader_Desk { + + +Hand_Analysis_Widget::Hand_Analysis_Widget + (function gcfg, + Preferences& P) + : chart {Chart::Style::HAND_ANALYSIS, P}, + company_name {chart.data}, + trade {P, chart.data}, + date_range {chart.data, P}, + shares {chart.data}, + get_chart_from_grid {gcfg} + { + pack_start (address_bar, Gtk::PACK_SHRINK, 5); + pack_start (display_h_box, Gtk::PACK_EXPAND_WIDGET, 5); + + address_bar.pack_start (company_name, Gtk::PACK_EXPAND_PADDING, 0); + address_bar.pack_start (trade, Gtk::PACK_EXPAND_PADDING, 0); + + display_h_box.pack_start (chart, Gtk::PACK_EXPAND_WIDGET, 5); + display_h_box.pack_start (controls_h_box, Gtk::PACK_SHRINK, 0); + + controls_h_box.set_spacing (SCALE_SEPARATION); + + controls_h_box.pack_start (shares, Gtk::PACK_SHRINK, 0); + controls_h_box.pack_start (date_range, Gtk::PACK_SHRINK, 0); + + for (auto const &w : chart.analyzer->make_control_widgets ()) + controls_h_box.pack_start (*Gtk::manage (w), Gtk::PACK_SHRINK); + + chart . analyzer + -> signal_redraw_needed () + . connect ([this] { queue_draw (); }); + + company_name . name_change + . connect ([this, &P] (int const &seqid) + { get_chart_from_grid (chart.data, seqid); + new_chart (P); }); + + } /* End of method Hand_Analysis_Widget::Hand_Analysis_Widget. */ + + + +void Hand_Analysis_Widget::new_chart (Preferences& P) + { + chart.data.extremes.start_time + = chart.data.extremes.end_time + - chrono::hours (24 * (int) date_range.value ()->get_value ()); + + chart.data . prefetch_ + (P, + {chrono::hours {24 * (int)date_range.value ()->get_value ()}, + chrono::hours {10 * 365 * 24}}); + + chart.data.changed_signal.emit (); + } + + + +void Hand_Analysis_Widget::subsume_selected (Chart_Grid &grid) + { + DB db {grid.user_prefs}; + company_name.read_names (db, + grid.market.seqid, + grid.selection->data.company_seqid); + chart.data.subsume (&grid.selection->data); + new_chart (grid.user_prefs); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/hand-analysis-widget.h b/trader-desk/hand-analysis-widget.h new file mode 100644 index 0000000..9480909 --- /dev/null +++ b/trader-desk/hand-analysis-widget.h @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__HAND_ANALYSIS_WIDGET__H +#define DMBCS__TRADER_DESK__HAND_ANALYSIS_WIDGET__H + + +#include +#include +#include +#include + + +namespace DMBCS::Trader_Desk { + + + /* + +-----VBox-------------------------------------------------------------------------------------------+ + | +----address_bar--------------------------------------------------------------------------------+ | + | | +--company_name-------------+ +--positions---------------------+ +--trade_instruction----+ | | + | | | | | | | | | | + | | +---------------------------+ +--------------------------------+ +-----------------------+ | | + | +-----------------------------------------------------------------------------------------------+ | + | +----display_h_box------------------------------------------------------------------------------+ | + | | +----chart------------------------+ +----controls_h_box----------------------------------+ | | + | | | | | +-shares-+ +-date_range--+ +----+ | | | + | | | | | | | | | | | | | | + ... ... ... + | | | | | | | | | | | | | | + | | | | | +--------+ +-------------+ +---------------------+ | | | + | | +---------------------------------+ +----------------------------------------------------+ | | + | +-----------------------------------------------------------------------------------------------+ | + +----------------------------------------------------------------------------------------------------+ + */ + + /** This is a compound widget which entirely looks after itself under + * the GTK machinery and the cooperation between various components + * (almost all hang off the back of the chart object which throws a + * signal whenever anything changes). The composition--layout--of the + * widget is shown above. */ + + struct Hand_Analysis_Widget : Gtk::VBox + { + /** The gap between sliders in the \c controls_h_box, needed so that + * complicated controls can emulate the appearance of this top-level + * widget. */ + static constexpr int const SCALE_SEPARATION {10}; + + /** Layout widget as per diagram above. */ + Gtk::HBox display_h_box; + + /** Layout widget as per diagram above. */ + Gtk::HBox controls_h_box; + + /** Control widget as per diagram above. */ + Gtk::HBox address_bar; + + /** The chart which we are hand-analyzing. */ + Chart chart; + + /** Display and select the company whose data are in the \c chart. */ + Company_Name_Entry company_name; + + /** Buy/sell button and current price input. */ + Trade_Instruction trade; + + /** Select the range of dates over which data are shown. */ + Date_Range_Scale date_range; + + /** Select the number of shares in a hypothetical position (if we are + * in a position, this will be a static object which simply shows the + * number of shares). */ + Shares_Scale shares; + + /** Us calling this method, provided by the wider application, will + * get the \a Chart_Data populated with the data from a chart which + * corresponds to the company with \a seqid. */ + function get_chart_from_grid; + + + /** Sole constructor which gives us a fully operational object. The + * incoming function \a gcfg must provide the machinery we need in \c + * get_chart_from_grid. */ + Hand_Analysis_Widget (function gcfg, + Preferences&); + + + /** Find the selected chart in the \a grid, and then subsume that + * chart into our display: take a shadow copy of the data and show + * these in the graph. */ + void subsume_selected (Chart_Grid &grid); + + + private: + + /* Implementation detail: whenever we change to charting some new + * data we must arrange for the associated pre-fetch thread to run so + * that the amount of data available for display matches at least the + * time-period which the current chart spans. */ + void new_chart (Preferences&); + + + }; /* End of class Hand_Analysis_Widget. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__HAND_ANALYSIS_WIDGET__H. */ diff --git a/trader-desk/makefile.am b/trader-desk/makefile.am new file mode 100644 index 0000000..c130516 --- /dev/null +++ b/trader-desk/makefile.am @@ -0,0 +1,62 @@ +# Copyright (c) 2020 Dale Mellor +# +# This file is part of the trader-desk package. +# +# The trader-desk package is free software: you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# The trader-desk package is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + + +AM_CXXFLAGS = ${gtk_config_CFLAGS} -I${top_srcdir} \ + -Wno-deprecated-copy \ + -Wall -Wextra \ + -Wno-error=int-in-bool-context \ + -Werror \ + -DCURLPP_GLOBAL_H=1 \ + -DPKGDATADIR=\"${pkgdatadir}\" \ + -DLOCALEDIR=\"${localedir}\" \ + -DHAVE_MYSQL=${HAVE_MYSQL} \ + -DHAVE_MARIADB=${HAVE_MARIADB} \ + -D_GNU_SOURCE=1 \ + -std=c++2a -I${includedir} + +AM_LDFLAGS = ${gtk_config_LIBS} + +bin_PROGRAMS = trader-desk + +lib_LTLIBRARIES = libtrader-desk.la + +CLASSES = alpha-vantage alpha-vantage--monitor analyzer application \ + chart chart-context chart-data chart-grid \ + colour company-name-entry \ + date-axis date-range-scale db delta-analyzer delta-region \ + hand-analysis-widget \ + markets moving-average-analyzer mysql \ + preferences \ + scale sd-envelope-analyzer shares-scale \ + text time-series trade-instruction \ + update-closing-prices update-latest-prices \ + wizard + +pkginclude_HEADERS = ${CLASSES:=.h} tide-mark.h + +nodist_noinst_HEADERS = auto-config.h + +libtrader_desk_la_SOURCES = ${CLASSES:=.cc} \ + application--ingest-market.cc \ + application--update-closing-prices.cc + +LDADD = libtrader-desk.la ${LTLIBINTL} + +trader_desk_SOURCES = trader-desk.cc + +MAINTAINERCLEANFILES = makefile.in auto-config.h.in diff --git a/trader-desk/markets.cc b/trader-desk/markets.cc new file mode 100644 index 0000000..8d16791 --- /dev/null +++ b/trader-desk/markets.cc @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include /* Only for number<> (). */ + + +namespace DMBCS::Trader_Desk { + + +Markets::Markets (DB& db) + { + auto sql {db.row_query ()}; + + sql << "select seqid, symbol, name, component_extension, tracked, " + << " unix_timestamp(last_update), " + << " 60*hour(close_time)+minute(close_time) " + << " from market"; + + Market_Meta_Data m; + + try + { + for (sql.execute (); sql; ++sql) + { + sql >> m.seqid >> m.world_data.symbol + >> m.world_data.name >> m.world_data.component_extension + >> m.tracked; + + m.last_time = sql.next_entry ((time_t) 0); + + m.world_data.close_time = chrono::minutes {sql.next_entry (0)}; + + this->insert ({m.seqid, m}); + } + } + catch (Mysql::DB_Connection::Exception&) {} + } + + + + static Markets::iterator find_symbol (Markets& markets, + const string& symbol) + { + return find_if (markets.begin (), markets.end (), + [&symbol] (const Markets::value_type& d) + { return d.second.world_data.symbol == symbol; }); + } + +void update_market_meta_data (Markets& markets, DB& db) +try + { + if (Market_Data_Api::short_time + (db.scalar_result + ((time_t) 0, + "select unix_timestamp(last_markets_update) from global"))) + return; + + /* !!! Need to be prepared for this not to work (offline?) */ + for (const Market_Data_Api::Market& i : Market_Data_Api::get_markets ()) + if (find_symbol (markets, i.symbol) == markets.end ()) + { + auto s {db.instruction ()}; + + s << "insert into market " + << " set symbol='" << i.symbol << "', " + << " name=\"" << i.name << "\", " + << " component_extension='" + << i.component_extension << "', " + << " close_time=sec_to_time(" + << number (i.close_time) + << ") "; + + s.execute (); + + markets.insert ({(size_t) s.insert_id (), + {.world_data = i, + .seqid = (size_t) s.insert_id (), + .tracked = 0, + .last_time = 0}}); + } + + db.instruction ("update global " + "set last_markets_update=from_unixtime(%d)", + time (nullptr)); + } +catch (Mysql::DB_Connection::Exception&) {} + + + +bool update_components + (Markets& markets, DB& db, Gtk::Window *const window) + { + /* !!! This wants to be the time on the individual market, *not* the + * global time-stamp. */ + if (Market_Data_Api::short_time + (db.scalar_result + ((time_t) 0, + "select unix_timestamp(last_markets_update) from global"))) + return false; + + update_market_meta_data (markets, db); + + bool ret {false}; + + for (auto& m : markets) + if (m.second.tracked) + ret = ret || Trader_Desk::update_components + (m.second, db, window); + + db.instruction ("update global " + "set last_markets_update=from_unixtime(%d)", + time (nullptr)); + + return ret; + } + + + +void update_database + (const Market_Data_Api::Delta& d, DB& db, const int market_seqid) + { + const int company_seqid + {db.scalar_result (0, + "select seqid " + "from company " + "where symbol='%s' and market=%d", + d.symbol.c_str (), + market_seqid)}; + + if (company_seqid) + db.quick () << "update company " + << "set market=" << market_seqid + << " where seqid=" << company_seqid; + else + db.quick () << "insert into company " + << "set name=\"" << d.name << "\", " + << "symbol='" << d.symbol << "', " + << "market=" << market_seqid; + } + + + + static void replace (string& in, const char a, const string& text) + { + for (size_t cursor {0}; + (cursor = in.find (a, cursor)) != in.npos; + ++cursor) + in.replace (cursor, 1, text); + } + + static string htmlize (string in) + { + replace (in, '&', "&"); + replace (in, '<', "<" ); + replace (in, '>', ">" ); + + return in; + } + +bool update_components (Market_Meta_Data& market_data, + DB& db, + Gtk::Window *const window) + { + if (Market_Data_Api::short_time (market_data.last_time)) + return false; + + auto instructions {Market_Data_Api::get_component_delta + (market_data.world_data.symbol, + market_data.last_time)}; + + market_data.last_time = time (0); + + db.instruction ("update market " + " set last_update=from_unixtime(%d) " + " where seqid=%d", + market_data.last_time, market_data.seqid); + + if (instructions.empty ()) return false; + + + /* List added companies for the benefit of the user. */ + string additions; + + /* List removed companies for the benefit of the user. */ + string removals; + + /* The total number of additions and removals. */ + size_t line_count {0}; + + /* Whether or not a terminating ‘...’ has been added to the additions. */ + bool additions_terminated {false}; + + /* Whether or not a terminating ‘...’ has been added to the removals. */ + bool removals_terminated {false}; + + auto make_updater + = [&line_count] + (string& line, bool& terminated) + { + return [&line, &terminated, &line_count] + (string const &name, string const &symbol) + { + if (++line_count < 10) + line += " " + htmlize (name) + + " (" + symbol + ")\n"; + + else if (! terminated) + { + terminated = 1; + line += "... (more)...\n"; + } + }; + }; + + auto update_additions = make_updater (additions, additions_terminated); + auto update_removals = make_updater (removals, removals_terminated); + + + const Market_Data_Api::Delta* move_pending {nullptr}; + + + for (auto const &i : instructions) + { + if (move_pending) + { + if (i.action != i.SIDEWAYS) break; + + /* !!! This is a potentially dangerous operation if the same + * symbol is used in different markets. We will have to + * be careful how we run the server in this regard. */ + auto const company_seqid + = db.scalar_result (0, + "select seqid " + "from company " + "where symbol='%s'", + move_pending->symbol.c_str ()); + + if (! company_seqid) + db.instruction ("insert into company " + "set name=\"%s\", " + "symbol='%s', " + "market=%d", + i.name.c_str (), i.symbol.c_str (), + market_data.seqid); + + else + db.instruction ("update company " + "set name=\"%s\", " + "symbol='%s', " + "market=%d " + "where seqid=%d", + i.name.c_str (), i.symbol.c_str (), + market_data.seqid, company_seqid); + + update_additions (i.name, i.symbol); + + move_pending = nullptr; + } + + + else if (i.action == i.SIDEWAYS) + { + move_pending = &i; + } + + + else if (i.action == i.ADD) + { + update_database (i, db, market_data.seqid); + update_additions (i.name, i.symbol); + } + + else /* i.action == REMOVE */ + { + db.quick () << "update company " + << "set market=0 " + << "where symbol='" << i.symbol << "' " + << "and market=" << market_data.seqid; + + update_removals (i.name, i.symbol); + } + } + + if (! window) return true; + + string a = "MARKET MOVEMENTS\n\n" + + market_data.world_data.name + "\n"; + + if (additions.length ()) + a += "New entries\n" + additions; + + if (removals.length ()) + a += "Dropped entries\n" + removals; + + Gtk::MessageDialog {*window, a, 1/*use mark-up*/} . run (); + + return true; + } + + + +void start_tracking (Markets& markets, DB& db, const string& symbol) + { + const Markets::iterator m {find_symbol (markets, symbol)}; + + if (m == markets.end ()) return; + + m->second.tracked = 1; + + db.instruction ("update market set tracked=1 where seqid=%d", m->first); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/markets.h b/trader-desk/markets.h new file mode 100644 index 0000000..df32955 --- /dev/null +++ b/trader-desk/markets.h @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__MARKETS__H +#define DMBCS__TRADER_DESK__MARKETS__H + + +#include +#include "db.h" +#include + + +/** \file + * + * Declaration of the \c Market_Meta_Data class and the \c Markets + * class. */ + + +namespace DMBCS::Trader_Desk { + + + /** Really just a data-carrying structure (vessel) which fully describes + * a stock market. It is divided into two parts: data which describe + * the markets in the eyes of the outside world, and data which + * describe the state of the markets from our own, private, point of + * view. */ + struct Market_Meta_Data + { + /** The subset of data which relate to the state of affairs in the + * world beyond this application; these are obtained from an + * Internet server through the medium of the + * dmbcs-market-data-api library. */ + Market_Data_Api::Market world_data; + + /** The sequence ID of this market in our local database. */ + size_t seqid; + + /** Whether we are keeping a database of prices for this + * market. */ + bool tracked; + + /** The last time the component data for this market were + * synchronized with a market data server. */ + time_t last_time; + + }; /* End of class Market_Meta_Data. */ + + + + void update_database + (const Market_Data_Api::Delta&, DB&, const int market_seqid); + + + + /** Fetch the data for this market from the Internet and update the + * database if necessary. A progress indication may be provided in a + * dialog on top of the window. */ + bool update_components (Market_Meta_Data&, DB&, Gtk::Window *const); + + + + /** A self-building collection of all markets known to the on-line data + * server, indexed according to our local database sequence ID. */ + struct Markets : map + { + /** Sole constructor, which self-builds our \c map from information + * contained in the local \a database. */ + explicit Markets (DB& database); + + /** Constructing these objects is expensive; allow them to be moved + * around but not duplicated. */ + Markets () = delete; + Markets (Markets const &) = delete; + Markets (Markets &&) = default; + Markets &operator= (Markets const &) = delete; + Markets &operator= (Markets &&) = default; + + } ; /* End of class Markets. */ + + + /** Market the market with \a symbol as being tracked, both in memory + * (here) and in the database. Note that this does NOT cause any price + * data to be fetched from the server. */ + void start_tracking (Markets&, DB&, const string& symbol); + + /** Get information on all known markets from the server, and update our + * own information and the database. */ + void update_market_meta_data (Markets&, DB&); + + /** Update the meta-data with the above method, then update the + * components of each market we follow (note *not* the price + * information!) These actions will not be allowed to take place more + * than once per 12 hours. The return value indicates if anything has + * actually changed. If a window is given, provide a report on changes + * in a dialog box on the window. */ + bool update_components (Markets *const, Gtk::Window *const = nullptr); + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__MARKETS__H. */ diff --git a/trader-desk/moving-average-analyzer.cc b/trader-desk/moving-average-analyzer.cc new file mode 100644 index 0000000..f2b2d1e --- /dev/null +++ b/trader-desk/moving-average-analyzer.cc @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +/** \file + * + * Implementation of the \c Moving_Average_Analyzer class. */ + + +namespace DMBCS::Trader_Desk { + + + Moving_Average_Analyzer::Moving_Average_Analyzer (Chart_Data &cd) + : chart_data (cd), + mean_series {cd.prices.market_close_time} + { + chart_data . changed_signal . connect ([this] { compute (); }); + } + + + + vector Moving_Average_Analyzer::make_control_widgets () + { + auto *const scale + = new Scale {chart_data, + (double) (number (mean_window) / 24)}; + + scale -> value () + -> signal_value_changed () + . connect ([this, scale] { control_moved (scale); }); + + return {scale}; + } + + + + void Moving_Average_Analyzer::stretch_outline (Time_Series::Range &outline) + { + /* !!!! When we re-visit this, need to ensure that the mean + * time-series has been previously computed. */ + + auto const range = mean_series.get_range (); + + auto const margin = (std::max (outline.max_value, range.max_value) + - std::min (outline.min_value, range.min_value)) + * 0.05; + + outline.max_value = max (outline.max_value, range.max_value + margin); + + outline.min_value = min (outline.min_value, range.min_value - margin); + } + + + + void Moving_Average_Analyzer::graph_draw_hook + (Chart_Context &canvas, + Tide_Mark::List &marks, + unsigned, + vector const &markers) + { + canvas . draw_time_series (mean_series, Colour::MEAN_GRAPH, 0.5); + + + /* The vertical bar which shows the mid-point of the latest window. */ + canvas . set_source_rgb (Colour::MEAN_GRAPH); + + canvas . move_to ({canvas.outline.end_time - mean_window / 2, + canvas.outline.min_value}); + + canvas . line_to ({canvas.outline.end_time - mean_window / 2, + canvas.outline.max_value}); + + canvas . cairo -> stroke (); + + + /* Put a tide-mark at the mean value at all points in time at which a + * marker has been specified. */ + for (auto const &marker : markers) + marks.emplace_back (marker (mean_series.interpolated_value + (marker (0.0, Colour::MEAN_TIDE).time), + Colour::MEAN_TIDE)); + } + + + + void Moving_Average_Analyzer::control_moved (Scale const *const scale) + { + mean_window = chrono::hours ((int) scale->value ()->get_value () * 24); + compute (); + } + + + + void Moving_Average_Analyzer::compute () + { + { + lock_guard l {chart_data.prices_mutex}; + + mean_series = Time_Series::compute_moving_average + (chart_data.prices, + mean_window, + chart_data.extremes.start_time); + } + + redraw_needed_.emit (); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/moving-average-analyzer.h b/trader-desk/moving-average-analyzer.h new file mode 100644 index 0000000..95243c4 --- /dev/null +++ b/trader-desk/moving-average-analyzer.h @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__MOVING_AVERAGE_ANALYZER__H +#define DMBCS__TRADER_DESK__MOVING_AVERAGE_ANALYZER__H + + +#include +#include + + +/** \file + * + * Declaration of the \c Moving_Average_Analyzer class. */ + + +namespace DMBCS::Trader_Desk { + + + /** An \c Analyzer which computes and displays a smoothed version of a + * prices time-series. */ + + struct Moving_Average_Analyzer : Analyzer + { + + /** The widget which we proffer to control ourselves is nothing more + * than an exponential scale, calibrated to allow specification of + * averaging windows between one day and a year, with fine control + * out to 14 days. */ + + struct Scale : Exponential_Scale + { + Scale (Chart_Data &chart_data, double const &initial_value) + : Exponential_Scale (chart_data, + pgettext ("Label", + "Mean window = %.0f days"), + Gdk::RGBA {"#ff0000"}, + Gdk::RGBA {"#ffaaaa"}, + Gdk::RGBA {"#ff6666"}, + 1, 14, 365, + initial_value) + {} + }; + + + /** The data that we are to analyze. */ + Chart_Data &chart_data; + + /** The size of the window over which we compute means. */ + Duration mean_window {chrono::hours {14*24}}; + + /** The resulting time-series of local mean values. */ + Time_Series mean_series; + + /** Fired whenever the analysis of data produces new results, which will + * need rendering in the GUI. */ + sigc::signal redraw_needed_; + + + /** Called when the user slides the control on the \a + * exponential_scale, meaning to change the moving-average window. */ + void control_moved (Scale const *const); + + /** Called whenever we must re-compute the moving-average + * time-series, including when the \c chart_data change. */ + void compute (); + + + /** Sole constructor which registers the \a chart_data we are to + * analyze. */ + explicit Moving_Average_Analyzer (Chart_Data &); + + + /********************** Analyzer interface. ****************************/ + + + /** Make a single widget which controls the size of the moving average + * window. */ + vector make_control_widgets () override; + + + /** Draw the \c mean_series and add a \a tide label at all the \a + * marked points in time. */ + void graph_draw_hook (Chart_Context &, + Tide_Mark::List &tide, + unsigned number_shares, + vector const &marked) + override; + + + /** Make sure the \a range includes the entire mean series, with room + * to breathe. */ + void stretch_outline (Time_Series::Range &range) override; + + + /** Return our signal so that the application can connect and act when + * we need a re-draw to take place. */ + sigc::signal &signal_redraw_needed () override + { return redraw_needed_; } + + + }; /* End of class Moving_Average_Analyzer. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__MOVING_AVERAGE_ANALYZER__H. */ diff --git a/trader-desk/mysql.cc b/trader-desk/mysql.cc new file mode 100644 index 0000000..10cb787 --- /dev/null +++ b/trader-desk/mysql.cc @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include +#include + + +/** \file + * + * Implementation of class methods in \c Mysql namespace. */ + + +namespace DMBCS::Trader_Desk { namespace Mysql { + + + int Instruction::execute () + { + const string query {buffer->str ()}; + const int test {mysql_real_query (mysql, query.c_str(), query.length())}; + + if (test && ! no_error) + { + cerr << "MYSQL ERROR: " << mysql_error (mysql) << endl; + throw DB_Connection::Exception {mysql_error (mysql)}; + } + + return test; + } + + + + void DB_Connection::connect (const Preferences& P) + { + if (! mysql_real_connect (&mysql, + P.database_host.data (), + P.database_user.data (), + P.database_password.data (), + P.database_instance.data (), + P.database_port, + P.database_socket.data (), + 0/*flags*/)) + throw Exception {mysql_error (&mysql)}; + + initialized = 1; + } + + + + void DB_Connection::reconnect (const Preferences& P) + { + if (initialized) mysql_close (&mysql); + initialized = 0; + connect (P); + } + + + + DB_Connection::DB_Connection (const Preferences& P) + { + mysql_init (&mysql); + connect (P); + } + + + + static int _run_query (MYSQL *const mysql, + string const &_template, + va_list arguments) + { + int buffer_length = _template.length () + 500; + + char *buffer = new char [buffer_length]; + + int length; + + for (;;) + { + length = vsnprintf + (buffer, buffer_length, _template.c_str (), arguments); + + if (length == buffer_length) + { + buffer_length += 1000; + delete[] buffer; + buffer = new char [buffer_length]; + } + + else + break; + } + + int const test = mysql_real_query (mysql, buffer, length); + + delete[] buffer; + + return test; + } + + + + void DB_Connection::void_database_result (MYSQL *const mysql, + string const &template_, + va_list arguments) + { + if (_run_query (mysql, template_, arguments) != 0) + cerr << mysql_error (mysql) << endl; + } + + + + string DB_Connection::string_database_result (MYSQL *const mysql, + string const &template_, + va_list arguments) + { + if (_run_query (mysql, template_, arguments) != 0) + { + cerr << mysql_error (mysql) << endl; + return string {}; + } + + MYSQL_RES *const results = mysql_store_result (mysql); + + if (! results) + return string {}; + + MYSQL_ROW const row = mysql_fetch_row (results); + + string const ret = (row && row [0]) ? string {row [0]} : string {}; + + mysql_free_result (results); + + return ret; + } + + + + void DB_Connection::instruction (string const &template_, ...) + { + va_list args; va_start (args, template_); + void_database_result (&mysql, template_, args); + va_end (args); + } + + +} } /* End of namespace DMBCS::Trader_Desk::Mysql. */ diff --git a/trader-desk/mysql.h b/trader-desk/mysql.h new file mode 100644 index 0000000..983523f --- /dev/null +++ b/trader-desk/mysql.h @@ -0,0 +1,508 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__MYSQL__H +#define DMBCS__TRADER_DESK__MYSQL__H + + +#include +#include +#include +#include +#include +#include +#include +#include + +#if HAVE_MYSQL +# include +#endif + +#if HAVE_MARIADB +# include +#endif + + +/** \file + * + * Definition of \c Mysql::DB_Connection, \c Mysql::Instruction, \c + * Mysql::Quick_Instruction, \c Mysql::Simple_Query and \c + * Mysql::Row_Query, objects which capture between them all the specifics + * of operating a MySQL/MariaDB database. */ + +/* Note that it is the intention to base these classes on abstract ones, + * and thence to provide alternative database back-ends. */ + + +namespace DMBCS::Trader_Desk { + + + using namespace std; + + + /** This namespace encaptures everything which is specific to a + * MySQL/MariaDB database back-end (specifically \c libmysqlclient). + * Nothing outside should have any inkling of the fact that we are using + * such a database. */ + namespace Mysql { + + + /** Object which provides std::ostream-type features to develop an SQL + * query string (should be one which produces no useful results) and + * then allows for its execution. If this produces some unique \c + * seqid (e.g. by inserting into a table with an automatic index + * column), then that value can subsequently be obtained with the \c + * insert_id method. */ + + struct Instruction + { + /** The connection to the database which we will be using. */ + MYSQL *mysql; + + /** Flag to indicate if we should print any error messages (\c FALSE) + * or not. */ + bool const no_error; + + /** Place where we accumulate the query string. */ + unique_ptr buffer; + + + /** Flag for the constructor which allows to specify that no error + * messages should be printed. */ + enum : bool { NO_ERROR = true }; + + /** Sole constructor which takes a connection to the database and an + * optional flag (see above) to indicate no error messages should + * appear. */ + explicit Instruction (MYSQL *const m, bool const ne = false) + : mysql {m}, + no_error {ne}, + buffer {make_unique ()} + {} + + + /** We can't copy these objects as that would wreak havoc with the \c + * buffer, but we want to be able to move them so they can be passed + * back from factory functions. */ + Instruction (Instruction const &) = delete; + Instruction (Instruction &&) = default; + Instruction &operator= (Instruction const &) = delete; + Instruction &operator= (Instruction &&) = default; + + + /** This is the method which makes our class look like an + * std::ostream. */ + template + Instruction &operator<< (T const &i) { *buffer << i; return *this; } + + /** Send the query string, which should by now have been sent into the + * \c buffer, to the database and return the resulting status (zero + * is success) of the operation. */ + int execute (); + + /** If the query caused an auto-incrementing table column to be + * updated, this method will return the last value assigned. */ + int insert_id () { return mysql_insert_id (mysql); } + + } ; /* End of class Instruction. */ + + + + /** A \c Quick_Instruction is just a \c Instruction which + * executes the query on object destruction, allowing for one-line + * instructions to make modifications to the database, + * e.g. `Quick_Instruction {} << "update table ...";'. */ + struct Quick_Instruction : Instruction + { + /** All construction, move, no-copying construction is exactly as \c + * Instruction. */ + using Instruction::Instruction; + + Quick_Instruction (Quick_Instruction const &) = delete; + Quick_Instruction (Quick_Instruction &&m) = default; + Quick_Instruction &operator= (Quick_Instruction const &) = delete; + Quick_Instruction &operator= (Quick_Instruction &&) = delete; + + + /** Do the work in the destructor. */ + ~Quick_Instruction () { Instruction::execute (); } + + /* Make sure the user can't accidentally call the execution + * directly. */ + int execute () = delete; + }; + + + + /** A \c Simple_Query is like a \c Instruction which returns + * a single meaningful value. The usage pattern is to construct, + * assemble a query with the inherited \c operator<<, and then call \c + * return_scalar to get the solitary resulting value. */ + struct Simple_Query : Instruction + { + /** Construction, move, non-copy is exactly as for \c + * Instruction. */ + using Instruction::Instruction; + + Simple_Query (Simple_Query const &) = delete; + Simple_Query (Simple_Query &&) = default; + Simple_Query &operator= (Simple_Query const &) = delete; + Simple_Query &operator= (Simple_Query &&) = default; + + /** Execute the assembled query and return the result, cast to type \c + * T. The \a fallback both determines the return type and also the + * value that will be returned if the database fails to provide + * this. */ + template + T return_scalar (T const &fallback = T ()); + + /** Make sure the user can't accidentally call for the execution + * directly. */ + int execute () = delete; + } ; + + + + /** This class represents a database query which produces (selects) + * multiple rows of multiple columns of results. It is used exactly as + * a \c Instruction, but after calling the \c execute method the + * class provides iterators and other convenience access methods for + * retrieving the data. */ + class Row_Query : public Instruction + { + /** The result from the database query. Note that this is a resource + * we manage locally, so have to be careful with class move and + * destruction. */ + MYSQL_RES *result {nullptr}; + + /** An index into the rows of the \c result. */ + MYSQL_ROW row; + + /** An index into the columns of the \c row we are currently + * examining. */ + int next_index; + + + public: + + /** Construction, move and non-copy is the same as for the base + * classes, but we have to take care with the handling of our + * controlled resource: the \c result. */ + using Instruction::Instruction; + + Row_Query (Row_Query const &) = delete; + + Row_Query (Row_Query &&m) + : Instruction {move (m)}, + result {m.result}, row {m.row}, next_index {m.next_index} + { m.result = nullptr; } + + Row_Query &operator= (Row_Query const &) = delete; + + Row_Query &operator= (Row_Query &&m) + { + result = m.result; m.result = nullptr; + row = m.row; + next_index = m.next_index; + return *this; + } + + + ~Row_Query () { if (result) mysql_free_result (result); } + + + /** Perform the database query, and then obtain a result manager, + * fetch the first row of results, and set up the result indexers to + * indicate that the first column of the first row will be the next + * available result value. */ + Row_Query &execute () { Instruction::execute (); + result = mysql_store_result (mysql); + row = result ? mysql_fetch_row (result) : 0; + next_index = 0; + return *this; } + + + /** After \c execute has been called, return the number of rows of + * data that are available. */ + int number_rows () const { return mysql_num_rows (result); } + + + /** Skip over \a count columns in the current row. */ + Row_Query &skip_entry (const int count = 1) { next_index += count; + return *this; } + + + /** Read the next result value into \a ret, and advance the indexers + * to the next column. */ + template + Row_Query &operator>> (T &ret) + { + istringstream in (row [next_index++]); + in >> ret; + + return *this; + } + + + /** Return the next value cast to type \c T, returning \a fallback if + * the database did not provide a valid value, and advancing the + * index along to the next column so that subsequent calls to this + * method automatically iterate through the values of the row. This + * method can be use interchangeably with the previous one (\c + * operator>>), according to convenience. */ + template T next_entry (const T& fallback = {}) + { + if (row [next_index] == 0) { ++next_index; + return fallback; } + + T ret; + *this >> ret; + return ret; + } + + + /** Return \c TRUE if there are more rows to reap data for. */ + operator bool () const { return row; } + + + /** Iterate to the next row in the result data set. Combined with the + * above result this allows for the straight-forward implementation + * of \c for(;;) loops over all the rows in a result set. */ + void operator++ () { row = mysql_fetch_row (result); + next_index = 0; } + + + } ; /* End of class Row_Query. */ + + + + /** Essentially just a MYSQL object which automatically opens and closes + * a connection to the \c trader-desk database on object construction + * and destruction. + * + * The sole constructor may throw an exception if a database connection + * cannot be established. The copy constructor is implicitly deleted, + * the move constructors implicitly defined. + * + * Once the connection is established, the class provides factory + * methods for the above query object types, plus a couple of + * convenience functions which allow for one-line printf-style query + * specification and execution. + * + * Note that most of the connection parameters come through the + * config.h file, except the password which is passed directly on the + * compiler command line, via makefile.am and configure.ac; we do not + * store it in any source files, hence it doesn't get into GIT, but do + * beware that the raw string is present in built binaries and, no + * doubt, in infrastructure files in the package build directory. */ + + struct DB_Connection + { + /** Thrown if a connection to the RDBMS cannot be made. */ + struct Exception : runtime_error + { using runtime_error::runtime_error; }; + + /** The real connection object which we are wrapping. */ + MYSQL mysql; + bool initialized {0}; + + + /** Establish a connection to the RDBMS. May throw an \c Exception + * object. */ + explicit DB_Connection (const Preferences&); + + + /** We only want one of these for the application, so copy and move + * are irrelevant and are \c delete'd. */ + DB_Connection (DB_Connection const &) = delete; + DB_Connection (DB_Connection &&m) = delete; + DB_Connection &operator= (DB_Connection const &) = delete; + DB_Connection &operator= (DB_Connection &&m) = delete; + + + /** Close the connection to the database. */ + ~DB_Connection () { if (initialized) mysql_close (&mysql); } + + + /** Manufacture an \c Instruction. */ + Instruction instruction () { return Instruction {&mysql}; } + + /** Manufacture a \c Quick_Instruction. */ + Quick_Instruction quick () { return Quick_Instruction {&mysql};} + + /** Manufacture a \c Simple_Query. */ + Simple_Query simple_query () { return Simple_Query {&mysql}; } + + /** Manufacture a \c Row_Query. */ + Row_Query row_query () { return Row_Query {&mysql}; } + + + + /** Implementation of following (\c instruction) method. */ + static void void_database_result (MYSQL *const mysql, + string const &template_, + va_list arguments); + + /** Execute a one-shot SQL statement on the database, expressed + * through the printf-style \a template_ and arguments. There can be + * no return information, thus the query should be one which does not + * return any. */ + void instruction (string const &template_, ...); + + + + /** Implementation of following (\c scalar_result) method. */ + static string string_database_result (MYSQL *const mysql, + string const &template_, + va_list arguments); + + /** Make a query on the database which obtains a single-valued result. + * The \a fallback_value serves both to define the type of result + * returned, and sets the returned value in the case that the + * database is unable to furnish the information. The SQL query + * itself is composed of the printf()-type string \a template_ with + * substitutions from the following arguments. */ + template + T scalar_result (T const &fallback_value, + string const &template_, + ...); + + + /** Either make a connection to the database (\c mysql must *not* + * already be connected), or else throw an \c Exception. */ + void connect (const Preferences&); + + /** Close and re-make the connection to the database. */ + void reconnect (const Preferences&); + + + } ; /* End of class DB_Connection. */ + + + + /******************************************************************* + ****************** Template implementations ******************** + *******************************************************************/ + + + template + inline T Simple_Query::return_scalar (T const &fallback) + { + string const hold = return_scalar (string {"@@"}); + + if (hold.length () == 2 && hold == "@@") return fallback; + + T ret; istringstream i {hold}; i >> ret; + + return ret; + } + + + + template<> + inline string Simple_Query::return_scalar (string const &fallback) + { + if (Instruction::execute () != 0) return fallback; + + MYSQL_RES *const results = mysql_store_result (mysql); + + if (! results) return fallback; + + MYSQL_ROW const row = mysql_fetch_row (results); + + string const ret = row ? string {row [0]} : fallback; + + mysql_free_result (results); + + return ret; + } + + + + template <> + inline string Row_Query::next_entry (string const &fallback) + { + auto const &ret = row [next_index++]; + return ret ? ret : fallback; + } + + + + template <> + inline Row_Query &Row_Query::operator>> (string &ret) + { + ret = next_entry (string {}); + return *this; + } + + + + template <> + inline Row_Query &Row_Query::operator>> + (chrono::system_clock::time_point &ret) + { + time_t t; + (*this) >> t; + ret = chrono::system_clock::from_time_t (t); + return *this; + } + + + + template + inline T DB_Connection::scalar_result (T const &fallback_value, + string const &template_, + ...) + { + va_list args; va_start (args, template_); + string const value {string_database_result (&mysql, template_, args)}; + va_end (args); + + if (! value.length ()) + return fallback_value; + + istringstream i (value); + T ret; + i >> ret; + + return ret; + } + + + + template <> +inline string DB_Connection::scalar_result (const string& fallback_value, + const string& template_, + ...) + { + va_list args; va_start (args, template_); + const string value {string_database_result (&mysql, template_, args)}; + va_end (args); + + if (! value.length ()) return fallback_value; + + return value; + } + + +} } /* End of namespace DMBCS::Trader_Desk::Mysql. */ + + +#endif /* Defined DMBCS__TRADER_DESK__MYSQL__H. */ diff --git a/trader-desk/preferences.cc b/trader-desk/preferences.cc new file mode 100644 index 0000000..aab8e37 --- /dev/null +++ b/trader-desk/preferences.cc @@ -0,0 +1,360 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include "preferences.h" +#include "alpha-vantage.h" +#include +#include + + +namespace DMBCS::Trader_Desk { + + + +Preferences Preferences::defaults () + { + return + { + .trade_cost_offset = 0.0, + .trade_cost_factor = 0.0, + .database_host = "localhost", + .database_user = "trader_desk", + .database_password = "123456", + .database_instance = "trader_desk", + .database_socket = "/run/mysqld/mysqld.sock", + .database_port = 3306, + .market_meta_data_service = "https://rdmp.org:9443/trader-desk/", + .market_data_service = "https://www.alphavantage.co/query", + .market_data_service_key = "" + }; + } + + + +void dump (const Preferences& P) + { + dump (P, P.file_path.empty () + ? getenv ("HOME") + string {"/.config/trader-desk.conf"} + : P.file_path); + } + +void dump (const Preferences& P, const string& file) + { dump (P, ofstream {file}); } + +static Preferences dump (Preferences&& P, const string& file) + { dump (P, file); return P; } + +void dump (const Preferences& P, ostream&& O) + { + O << "trade_cost_offset: " << P.trade_cost_offset << "\n" + << "trade_cost_factor: " << P.trade_cost_factor << "\n" + << "database_host: " << P.database_host << "\n" + << "database_user: " << P.database_user << "\n" + << "database_password: " << P.database_password << "\n" + << "database_instance: " << P.database_instance << "\n" + << "database_socket: " << P.database_socket << "\n" + << "database_port: " << P.database_port << "\n" + << "market_meta_data_service: " << P.market_meta_data_service << "\n" + << "market_data_service: " << P.market_data_service << "\n" + << "market_data_service_key: " << P.market_data_service_key << "\n"; + } + + + +string Preferences::default_file_name () + { return getenv ("HOME") + string {"/.config/trader-desk.conf"}; } + +Preferences Preferences::from_default_file () + { return from_file (default_file_name ()); } + +Preferences Preferences::from_file (const string& file) + { + Preferences ret + {[&file] { if (ifstream F {file}; F.good ()) + return from_file (move (F)); + return dump (defaults (), file); } () }; + ret.file_path = file; + return ret; + } + + static string read_line (istream& I) + { + static regex R {" *[^:\n]*: *(.*)$"}; + smatch M; + string line; + while (getline (I, line), I.good ()) + if (regex_match (line, M, R)) return M [1]; + return {}; + } + +Preferences Preferences::from_file (istream&& I) + { + Preferences ret; + ret.trade_cost_offset = strtod (read_line (I).data (), nullptr); + ret.trade_cost_factor = strtod (read_line (I).data (), nullptr); + ret.database_host = read_line (I); + ret.database_user = read_line (I); + ret.database_password = read_line (I); + ret.database_instance = read_line (I); + ret.database_socket = read_line (I); + ret.database_port = atoi (read_line (I).data ()); + ret.market_meta_data_service = read_line (I); + ret.market_data_service = read_line (I); + ret.market_data_service_key = read_line (I); + return ret; + } + + + +bool database_equal (const Preferences& A, const Preferences& B) + { + return A.database_host == B.database_host + && A.database_user == B.database_user + && A.database_password == B.database_password + && A.database_instance == B.database_instance + && A.database_socket == B.database_socket + && A.database_port == B.database_port; + } + + + +static void commit_prefs (Preferences_Dialog& D, Preferences& P) +{ + /* Pounds to pence. */ + P.trade_cost_offset = D.trade_cost_offset.get_value () * 100; + /* Percent to fraction. */ + P.trade_cost_factor = D.trade_cost_factor.get_value () / 100.0; + + P.database_host = D.database_host.get_text (); + P.database_user = D.database_user.get_text (); + P.database_password = D.database_password.get_text (); + P.database_instance = D.database_instance.get_text (); + P.database_port = atoi (D.database_port.get_text ().data ()); + P.database_socket = D.database_socket.get_text (); + + P.market_meta_data_service = D.market_meta_data_service.get_text (); + P.market_data_service = D.market_data_service.get_text (); + P.market_data_service_key = D.market_data_service_key.get_text (); + + dump (P); +} + + + static void set_margins (Gtk::Widget *const W, const int size) + { + W->set_margin_start (size); + W->set_margin_end (size); + W->set_margin_top (size); + W->set_margin_bottom (size); + } + +Preferences_Dialog::Preferences_Dialog + (Gtk::Window &parent, + Preferences& P, + const string& alpha_vantage__message) + : Gtk::Dialog (pgettext ("Window title", "Trader-Desk: Preferences"), + parent), + preferences {P} + { + get_vbox ()->set_spacing (20); + + + /************* Trade costs **************************/ + + trade_cost_offset.set_increments (1.0, 10.0); + trade_cost_offset.set_range (0.0, 1000.0); + /* Pence to pounds. */ + trade_cost_offset.set_value (preferences.trade_cost_offset / 100); + + trade_cost_factor.set_increments (1.0, 10.0); + trade_cost_factor.set_range (0.0, 100.0); + /* Fraction to percent. */ + trade_cost_factor.set_value (preferences.trade_cost_factor * 100.0); + + { + auto *const frame { Gtk::make_managed + (pgettext ("Label", "Trading costs"))}; + get_vbox ()->pack_start (*frame, Gtk::PACK_SHRINK); + + auto *const grid {Gtk::make_managed ()}; + frame->add (*grid); + + grid->set_column_spacing (20); + set_margins (grid, 10); + grid->set_margin_left (100); + + auto grid_line + { [grid] (const int row, const string& label, + Gtk::Widget& control, const string& units) + { + grid->attach (*Gtk::make_managed (label, Gtk::ALIGN_END), + 0, row); + grid->attach (control, 1, row); + grid->attach (*Gtk::make_managed + (units, Gtk::ALIGN_START), + 2, row); + } + }; + + grid_line (0, pgettext ("Label", "Fixed cost of trading"), + trade_cost_offset, + pgettext ("Units:Worded:Monetary", "pounds")); + + grid_line (1, pgettext ("Label", "Proportional cost of trading"), + trade_cost_factor, + pgettext ("Units:Worded", "percent")); + } + + + Gtk::Grid *const database_ {Gtk::make_managed ()}; + { + Gtk::Frame *const df {Gtk::make_managed + (pgettext ("Label", "Local database"))}; + get_vbox ()->pack_start (*df, Gtk::PACK_SHRINK); + df->add (*database_); + database_->set_column_spacing (20); + set_margins (database_, 10); + database_->set_margin_left (100); + } + + + auto create_text_input + { + [this, &D = *database_] + (const int row, const string& label, Gtk::Entry& E, + const string& initial_value) + { + D.attach (*Gtk::make_managed (label, Gtk::ALIGN_END), + 0, row); + D.attach (E, 1, row); + E.set_text (initial_value); + E.set_input_purpose (Gtk::INPUT_PURPOSE_FREE_FORM); + E.set_hexpand (1); + } + }; + + + create_text_input (0, pgettext ("Label", "Database host"), + database_host, preferences.database_host); + + create_text_input (1, pgettext ("Label", "Database user"), + database_user, preferences.database_user); + + create_text_input (2, pgettext ("Label", "Database password"), + database_password, preferences.database_password); + database_password.set_input_purpose (Gtk::INPUT_PURPOSE_PASSWORD); + database_password.set_visibility (0); + database_password.set_invisible_char ('*'); + + create_text_input (3, pgettext ("Label", "Database instance"), + database_instance, preferences.database_instance); + + database_->attach (*Gtk::make_managed + (pgettext ("Label", "Database port"), Gtk::ALIGN_END), + 0, 4); + database_port.set_text (to_string (preferences.database_port)); + database_port.set_input_purpose (Gtk::INPUT_PURPOSE_DIGITS); + database_->attach (database_port, 1, 4); + + create_text_input (5, pgettext ("Label", "Database socket"), + database_socket, preferences.database_socket); + + + + + Gtk::Grid *const servers_ {Gtk::make_managed ()}; + { + Gtk::Frame *const F {Gtk::make_managed + (pgettext ("Label", "Internet services"))}; + get_vbox ()->pack_start (*F, Gtk::PACK_SHRINK); + F->add (*servers_); + servers_->set_column_spacing (20); + set_margins (servers_, 10); + servers_->set_margin_left (100); + } + + + + + servers_->attach (*Gtk::make_managed + (pgettext ("Label", "Market meta-data service"), + Gtk::ALIGN_END), + 0, 0); + market_meta_data_service.set_hexpand (1); + market_meta_data_service.set_text (preferences.market_meta_data_service); + market_meta_data_service.set_width_chars + (max (preferences.market_meta_data_service.length (), 40)); + servers_->attach (market_meta_data_service, 1, 0); + + servers_->attach (*Gtk::make_managed + (pgettext ("Label", "Market data service"), + Gtk::ALIGN_END), + 0, 1); + market_data_service.set_hexpand (1); + market_data_service.set_text (preferences.market_data_service); + servers_->attach (market_data_service, 1, 1); + + servers_->attach (*Gtk::make_managed + (pgettext ("Label", "Market data service API key"), + Gtk::ALIGN_END), + 0, 2); + market_data_service_key.set_hexpand (1); + market_data_service_key.set_input_purpose (Gtk::INPUT_PURPOSE_PASSWORD); + market_data_service_key.set_visibility (0); + market_data_service_key.set_invisible_char ('*'); + market_data_service_key.set_text (preferences.market_data_service_key); + servers_->attach (market_data_service_key, 1, 2); + + if (alpha_vantage__message.length ()) + { + Gtk::Frame *const F {Gtk::make_managed + ("Message from AlphaVantage service:")}; + servers_->attach (*F, 0, 3, 2, 1); + F->set_margin_top (18); + Gtk::TextView *const T {Gtk::make_managed ()}; + F->add (*T); + T->set_wrap_mode (Gtk::WRAP_WORD); + T->get_buffer () + ->insert_markup (T->get_buffer ()->end (), + "" + + alpha_vantage__message + ""); + } + + /* The action buttons. */ + { + Gtk::HBox *const H {Gtk::make_managed ()}; + get_vbox ()->pack_start (*H, Gtk::PACK_SHRINK); + Gtk::Button *const B1 {Gtk::make_managed + (pgettext ("Label", "Commit changes"))}; + H->pack_start (*B1, Gtk::PACK_EXPAND_PADDING); + B1->signal_clicked () + .connect ([this] { commit_prefs (*this, this->preferences); + close (); }); + Gtk::Button *const B2 {Gtk::make_managed + (pgettext ("Label", "Cancel"))}; + H->pack_start (*B2, Gtk::PACK_EXPAND_PADDING); + B2 -> signal_clicked () . connect ([this] { close (); }); + } + + show_all (); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/preferences.h b/trader-desk/preferences.h new file mode 100644 index 0000000..f6d3d12 --- /dev/null +++ b/trader-desk/preferences.h @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__PREFERENCES__H +#define DMBCS__TRADER_DESK__PREFERENCES__H + + +#include +#include + + +/** \file + * + * Declaration of the \c Preferences and \c Preferences_Dialog + * classes. */ + + +namespace DMBCS::Trader_Desk { + + + using namespace std; + + + /** There are a number of parameters which affect the whole of the + * trader-desk application, and this flat structure simply contains + * those parameters. */ + + struct Preferences + { + double trade_cost_offset; + double trade_cost_factor; + + /* MariaDB connection parameters. */ + string database_host; + string database_user; + string database_password; + string database_instance; + string database_socket; + uint16_t database_port; + + /* The RDMP HTTP end-point. */ + string market_meta_data_service; + /* The AlphaVantage HTTP end-point... */ + string market_data_service; + /* ... and private key. */ + string market_data_service_key; + + /* Not really a user preference at this time. */ + static constexpr const int time_horizon {10}; + + /* If set, the file from whence these parameters came. */ + string file_path {}; + + static string default_file_name (); + + static Preferences from_file (istream&&); + static Preferences from_file (const string& file_path); + static Preferences from_default_file (); + + static Preferences defaults (); + }; + + + void dump (const Preferences&, ostream&&); + void dump (const Preferences&, const string& file_name); + void dump (const Preferences&); + + bool database_equal (const Preferences&, const Preferences&); + + + + /** There are a number of parameters which affect the whole of the + * trader-desk application, and this dialog box is designed to let the + * user edit those global parameters from the ‘Preferences’ item on the + * menu. + * + * From the applicationʼs point of view use of this object is very + * simple: use the sole constructor to instantiate and then call the \c + * run method. The dialog will take over all GUI operations until it + * is closed, and in the meantime will look after itself. + * + * In operation the dialog will directly modify the static data in \c + * Position_Analyzer and update the database as soon as the user makes + * a change to any entry. */ + + struct Preferences_Dialog : Gtk::Dialog + { + Preferences& preferences; + + Gtk::SpinButton trade_cost_offset {1.0, 2}; + Gtk::SpinButton trade_cost_factor {1.0, 2}; + + /* MariaDB connection parameters. */ + Gtk::Entry database_host; + Gtk::Entry database_user; + Gtk::Entry database_password; + Gtk::Entry database_instance; + Gtk::Entry database_port; + Gtk::Entry database_socket; + + /* The RDMP HTTP end-point. */ + Gtk::Entry market_meta_data_service; + /* The AlphaVantage HTTP end-point... */ + Gtk::Entry market_data_service; + /* ... and private key. */ + Gtk::Entry market_data_service_key; + + + Preferences_Dialog (Gtk::Window &, Preferences&, + const string& alpha_vantage_message = {}); + + + } ; /* End of class Preferences. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__PREFERENCES__H. */ diff --git a/trader-desk/scale.cc b/trader-desk/scale.cc new file mode 100644 index 0000000..77e5dea --- /dev/null +++ b/trader-desk/scale.cc @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include + + +/** \file + * + * Implementation of the \c Scale and \c Exponential_Scale classes. */ + + +namespace DMBCS::Trader_Desk { + + + Scale::Scale (Chart_Data &d, + string const &label_, + Gdk::RGBA const normal, + Gdk::RGBA const active, + Gdk::RGBA const prelight, + double const min, + double const max, + double const initial_value) + : data (d), + label_format (label_), + scale (min, max, (max-min)/100) + { + label.set_angle (90.0); + + pack_start (label, Gtk::PACK_SHRINK, 0); + pack_start (scale, Gtk::PACK_SHRINK, 0); + + scale.get_adjustment () -> set_value (initial_value); + + scale.set_draw_value (0); + + scale.override_background_color (normal, Gtk::STATE_FLAG_NORMAL); + scale.override_background_color (active, Gtk::STATE_FLAG_ACTIVE); + scale.override_background_color (prelight, Gtk::STATE_FLAG_PRELIGHT); + + scale . signal_value_changed () + . connect ([this] { on_slider_changed (); }); + + data . changed_signal + . connect ([this] { on_data_changed (); }); + } + + + + void Scale::setup_label () + { + char buffer [label_format.length () + 20]; + sprintf (buffer, label_format.c_str (), value ()->get_value ()); + label.set_text (buffer); + } + + + + struct Exponential_Mapping + { + double a, b, c; + + explicit Exponential_Mapping (Exponential_Scale const &e) + { + c = e.value_adjustment->get_lower (); + + double const l = e.mid_value - c; + double const L = e.value_adjustment->get_upper () - c; + + a = sq (L / l - 1); + b = l * l / (L - 2 * l); + } + + double to_linear (double const &x) const + { return log ((x - c) / b + 1) / log (a); } + + double from_linear (double const &x) const + { return c + b * (pow (a, x) - 1); } + + static constexpr double sq (double const &x) {return x*x;} + }; + + + + void Exponential_Scale::on_slider_changed () + { + value_adjustment + ->set_value (Exponential_Mapping {*this} + .from_linear (scale.get_adjustment ()->get_value ())); + + value_adjustment->value_changed (); + } + + + + void Exponential_Scale::set_value (double const x) + { + scale.get_adjustment () + -> set_value (Exponential_Mapping {*this} . to_linear (x)); + + value_adjustment->set_value (x); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/scale.h b/trader-desk/scale.h new file mode 100644 index 0000000..3063ad6 --- /dev/null +++ b/trader-desk/scale.h @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__SCALE__H +#define DMBCS__TRADER_DESK__SCALE__H + + +#include +#include + + +/** \file + * + * Declaration of \c Scale, \c Linear_Scale, and \c Exponential_Scale + * classes. */ + + +namespace DMBCS::Trader_Desk { + + + /** A \c Scale is a prototypical vertical slider control which gets + * stacked to the right of a detail (\c Hand_Analysis_Widget) chart + * allowing the user some control over the display of information on + * top of that chart. + * + * Note that the rest of the program interacts with these objects via + * the \c Adjustments, in particular listening for the \c value_changed + * signals on those objects. */ + + class Scale : public Gtk::HBox + { + protected: + + /** The data which the user is currently observing, which should be + * regarded as a read-only object which may influence the operation + * of the \c Scale slider (although the--extremely special--\c + * Date_Range_Scale does cause changes to happen in the \c data). */ + Chart_Data &data; + + /** A graphical component of the \c Scale widget: displays an + * identifying text string according to the \c label_format. */ + Gtk::Label label; + + /** A \c printf-style format string which may include a place-holder + * for a floating-point value; this is used as the identifying label + * on the slider and will display the slider's current value instead + * of the placeholder. */ + string const label_format; + + /** The actual GTK widget which implements our \c Scale. */ + Gtk::VScale scale; + + /** Called internally whenever the user moves the slider on the + * screen. This is the place to transform the slider's internal \c + * Adjustment into the adjustment we present to the application. */ + virtual void on_slider_changed () { setup_label (); } + + /** Called when an externally generated signal fires on the \c data + * object to indicate that some change has taken place in those + * data. */ + virtual void on_data_changed () {} + + + public: + + /** The sole constructor, which provides a fully populated object + * ready for operations (though often the range of values covered by + * the slider is not known until later). */ + Scale (Chart_Data &d, + string const &label_format, + Gdk::RGBA const normal, + Gdk::RGBA const active, + Gdk::RGBA const prelight, + double const min, double const max, + double const initial_value = -1.0); + + /* The underlying GTK widget machinery severely restricts any hope we + * may have of copying and moving this type of object. */ + + /** Obligatory destructor for a pure virtual interface. */ + virtual ~Scale () = default; + + /** Provide an adjustment. This may be the actual \c Adjustment under + * control of the slider, but sometimes it is another object managed + * by a derived class which apparently provides a more sophisticated + * level of control. */ + virtual Glib::RefPtr value () = 0; + + /* /\** Ditto above. *\/ */ + virtual Glib::RefPtr value () const = 0; + + /** Apply the `value' of the slider to the \c label_format string and + * put the result as the text displayed by the \c label. */ + void setup_label (); + + + }; /* End of class Scale. */ + + + + /** Most basic concrete \c Scale, basically a wrapper around the base + * class which returns the slider's own (directly controlled) + * adjustment as the value given to the user. Even though this is a + * concrete type, it should only be used as the base of a more + * specialized \c Scale which represents some useful quantity. */ + + struct Linear_Scale : Scale + { + /** Sole constructor which creates a fully populated object. */ + Linear_Scale (Chart_Data &d, + string const &label_, + Gdk::RGBA const &normal, + Gdk::RGBA const &active, + Gdk::RGBA const &prelight, + double const min, double const max, + double const initial_value) + : Scale (d, label_, normal, active, prelight, min, max, initial_value) + { + setup_label (); + } + + /** Simply return the slider's directly-controlled adjustment + * object. */ + Glib::RefPtr value () override { return scale.get_adjustment (); } + + /* /\** Ditto. *\/ */ + Glib::RefPtr value () const override + { return scale.get_adjustment (); } + }; + + + + /** A \c Scale which is exponential (or logarithmic, depending on which + * way you like to look at things), allowing for higher values to be + * more tightly packed together. + * + * The scale is parameterized at user level by the values of the + * end-points and (linear) mid-point, and in all other respects acts + * just like any other \C Scale object would with the exception that + * new values are assigned through the \c set_value method; the + * application should *not* try to set the value directly in the + * adjustment which is given out. + * + * Although this is a concrete class, it is anticipated that the + * application will only see further specializations derived from this + * one. */ + + struct Exponential_Scale : Scale + { + /** The value at the linear mid-point of the scale. */ + double mid_value; + + /** The \c Adjustment which we present to the user, exhibiting the + * exponentially varying values. */ + Glib::RefPtr value_adjustment; + + /** When the user moves the graphical slider. */ + void on_slider_changed () override; + + + /** The sole constructor, which fully specifies an \c + * Exponential_Scale object. */ + Exponential_Scale (Chart_Data &d, + string const &label, + Gdk::RGBA const normal, + Gdk::RGBA const active, + Gdk::RGBA const prelight, + double const lower, double const mid, double const upper, + double const initial_value) + : Scale (d, label, normal, active, prelight, 0.0, 1.0), + mid_value (mid), + value_adjustment {Gtk::Adjustment::create (mid, lower, upper)} + { + value_adjustment -> signal_value_changed () + . connect ([this] { setup_label (); }); + + set_value (initial_value); + } + + + /** The means by which the application imposes a value on the + * slider. */ + void set_value (double const x); + + + /** Give the logical, i.e. not the linear, value to the user. */ + Glib::RefPtr value () override { return value_adjustment; } + + + /* /\** Ditto, but for \c const objects. *\/ */ + Glib::RefPtr value () const override + { return value_adjustment; } + + + }; /* End of class Exponential_Scale. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__SCALE__H. */ diff --git a/trader-desk/sd-envelope-analyzer.cc b/trader-desk/sd-envelope-analyzer.cc new file mode 100644 index 0000000..06431b5 --- /dev/null +++ b/trader-desk/sd-envelope-analyzer.cc @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include + + +/** \file + * + * Implementation of the \c SD_Envelope_Analyzer class. */ + + +namespace DMBCS::Trader_Desk { + + + SD_Envelope_Analyzer::SD_Envelope_Analyzer (Chart_Data &cd) + : moving_average {cd} + { + moving_average . signal_redraw_needed () + . connect ([this] { data_changed (); }); + } + + + + vector SD_Envelope_Analyzer::make_control_widgets () + { + vector ret = moving_average.make_control_widgets (); + + Scale *const s = new Scale {moving_average.chart_data, + envelope_width}; + + s -> value () -> signal_value_changed () + . connect ([this, s] { control_moved (s); } ); + + ret.emplace_back (s); + + return ret; + } + + + + template inline X sq (X const &x) { return x*x; } + + + static Currency_Value standard_deviation_ (Time_Series const &t, + Time_Series const &mean, + Time_Point const &earliest_time) + { + if (mean.size () < 2) + return 0.0; + + auto const end_ = std::upper_bound + (begin (mean), end (mean), + earliest_time, + [] (Time_Point const &val, + Time_Series::value_type const &a) + { return a.time < val; }); + + return std::sqrt (std::inner_product (begin (mean), end_, + begin (t), + Currency_Value {0.0}, + plus {}, + [] (Event const &a, Event const &b) + { return sq (a.price - b.price); }) + / (mean.size () - 1)); + } + + + + void SD_Envelope_Analyzer::data_changed () + { + { + lock_guard l {moving_average.chart_data.prices_mutex}; + + standard_deviation + = standard_deviation_ (moving_average.chart_data.prices, + moving_average.mean_series, + moving_average.chart_data.extremes.start_time); + } + + redraw_needed_.emit (); + } + + + + void SD_Envelope_Analyzer::control_moved (Scale const *const scale) + { + if (abs (envelope_width - scale->value ()->get_value ()) > 1.0e-3) + { + envelope_width = scale->value ()->get_value (); + redraw_needed_.emit (); + } + } + + + + void SD_Envelope_Analyzer::stretch_outline (Time_Series::Range &outline) + { + auto const range = moving_average.mean_series.get_range (); + auto const margin = (outline.max_value - outline.min_value) * 0.05; + + outline.max_value = max (outline.max_value, + range.max_value + + envelope_width * standard_deviation + + margin); + + outline.min_value = min (outline.min_value, + range.min_value + - envelope_width * standard_deviation + - margin); + } + + + + void SD_Envelope_Analyzer::graph_draw_hook + (Chart_Context &context, + Tide_Mark::List &marks, + unsigned number_shares, + vector const &markers) + { + if (moving_average.mean_series.empty ()) + return; + + context.set_source_rgb (Colour::SD_ENVELOPE); + + context.move_to (moving_average.mean_series.front ()); + + auto const envelope = envelope_width * standard_deviation; + + auto i = begin (moving_average.mean_series); + + for (; + i != end (moving_average.mean_series) + && i->time >= context.outline.start_time; + ++i) + context.line_to ({i->time, i->price + envelope}); + + for (--i; i >= begin (moving_average.mean_series); --i) + context.line_to ({i->time, i->price - envelope}); + + context.cairo->fill (); + + for (auto const &t : markers) + { + auto const mean + = moving_average.mean_series + .interpolated_value (t (0.0, Colour::MEAN_TIDE).time); + + marks.emplace_back (t (mean - envelope, Colour::ENVELOPE_TIDES)); + marks.emplace_back (t (mean + envelope, Colour::ENVELOPE_TIDES)); + } + + moving_average . graph_draw_hook (context, marks, number_shares, markers); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/sd-envelope-analyzer.h b/trader-desk/sd-envelope-analyzer.h new file mode 100644 index 0000000..494b26c --- /dev/null +++ b/trader-desk/sd-envelope-analyzer.h @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__SD_ENVELOPE_ANALYZER__H +#define DMBCS__TRADER_DESK__SD_ENVELOPE_ANALYZER__H + + +#include + + +/** \file + * + * Declaration of the \c SD_Envelope_Analyzer class. */ + + +namespace DMBCS::Trader_Desk { + + + /** A chart analyzer which puts an envelope around the prices chart + * related to the standard deviation (SD) of the prices data around + * their mean: any proportion of this SD can be chosen by the user + * acting on a slider (\c Scale). + * + * This analyzer incorporates a \c Moving_Average_Analyzer within it, + * and provides a composite control to the application which allows for + * user adjustment of both the averaging window and relative width (as + * proportion of SD) of the envelope. */ + + class SD_Envelope_Analyzer : public Analyzer + { + + /** Simple linear scale which allows the user to choose an envelope + * width relative to SD between 0.1 and 10.0. */ + + struct Scale : Linear_Scale + { + Scale (Chart_Data &chart_data, double const &initial_value) + : Linear_Scale (chart_data, + pgettext ("Label Abbrev:Standard deviation", + "Envelope width = %.2f x std. dev."), + Gdk::RGBA {"#dddd00"}, + Gdk::RGBA {"#ffffaa"}, + Gdk::RGBA {"#ffff66"}, + 0.1, 10.0, initial_value) + {} + }; + + + /** The moving average object which we encapsulate (and leverage for + * our own functioning). */ + Moving_Average_Analyzer moving_average; + + /** The width of the envelope as a fraction of the \c + * standard_deviation. */ + double envelope_width {2}; + + /** The current standard deviation of the data in the prices + * time-series about the current mean time-series. */ + double standard_deviation {0}; + + /** We emit this signal whenever we re-compute the \c + * standard_deviation. */ + sigc::signal redraw_needed_; + + + /** Called when the \a scale slider indicates that the user has + * changed the position. */ + void control_moved (Scale const *const); + + /** Called when the \c moving_average object signals that the mean + * time-series has just been re-computed. */ + void data_changed (); + + + public: + + /** Sole constructor which sets up a fully populated and operational + * object. */ + explicit SD_Envelope_Analyzer (Chart_Data &cd); + + + /* Analyzer interface. */ + + /** Return a (newly allocated) composite object which includes sliders + * for ourself and the \c moving_average. */ + vector make_control_widgets () override; + + /** Make sure the vertical (price) limits of the \a Range are wide + * enough to display the full envelope. */ + void stretch_outline (Time_Series::Range &) override; + + /** Display the envelope and \c moving_average chart. */ + void graph_draw_hook (Chart_Context &context, + Tide_Mark::List &, + unsigned number_shares, + vector const &) override; + + /** Return the signal object on which we emit when anything + * changes. */ + sigc::signal &signal_redraw_needed () override + { return redraw_needed_; } + + + }; /* End of class SD_Envelope_Analyzer. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__SD_ENVELOPE_ANALYZER__H. */ diff --git a/trader-desk/shares-scale.cc b/trader-desk/shares-scale.cc new file mode 100644 index 0000000..06bc45b --- /dev/null +++ b/trader-desk/shares-scale.cc @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include + + +namespace DMBCS::Trader_Desk { + + + Shares_Scale::Shares_Scale (Chart_Data &d) + : Exponential_Scale (d, + pgettext ("Label", + "Number of shares = %.0f"), + Gdk::RGBA ("#00dd00"), + Gdk::RGBA ("#aaffaa"), + Gdk::RGBA ("#66ff66"), + 1, 1000, 10000, + 1) + { + d.changed_signal . connect ([this] { on_data_changed (); }); + + value_adjustment -> signal_value_changed () + . connect ([this] { on_value_changed (); }); + } + + + + void Shares_Scale::on_data_changed () + { + if (data.open_position.price < 0) + scale.show (); + else + scale.hide (); + + if (data.number_shares != unsigned (value ()->get_value ())) + { + set_value (data.number_shares); + scale.get_adjustment () -> value_changed (); + } + } + + + + void Shares_Scale::on_value_changed () + { + if (data.number_shares != (unsigned) (value ()->get_value () + 0.5)) + { + data.number_shares = (unsigned) (value ()->get_value () + 0.5); + data.update_extreme_prices (); + data.changed_signal.emit (); + } + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/shares-scale.h b/trader-desk/shares-scale.h new file mode 100644 index 0000000..68c4a02 --- /dev/null +++ b/trader-desk/shares-scale.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__SHARES_SCALE__H +#define DMBCS__TRADER_DESK__SHARES_SCALE__H + + +#include + + +/** \file + * + * Declaration of the \c Shares_Scale class. */ + + +namespace DMBCS::Trader_Desk { + + + /** Basic example of an \c Exponential_Scale which sets itself according + * to the \c Chart_Data, and conversely applies user input from the + * graphical slider to the \c Chart_Data. */ + + class Shares_Scale : public Exponential_Scale + { + /** Called when the user pushes the slider about. */ + void on_value_changed (); + + /** Called when the \c Chart_Data change. */ + void on_data_changed () override; + + public: + + /** Sole constructor which registers the \a Chart_Data which we watch + * over. */ + Shares_Scale (Chart_Data &d); + + + }; /* End of class Shares_Scale. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__SHARES_SCALE__H. */ diff --git a/trader-desk/text.cc b/trader-desk/text.cc new file mode 100644 index 0000000..929d878 --- /dev/null +++ b/trader-desk/text.cc @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include + + +namespace DMBCS::Trader_Desk { + + + inline double clamp (double const &x, double const &l, double const &h) + { + return max (min (x, h), l); + } + + + + static double boundary_stress (Text::Item const &i, + Text::Boundary const &b, + double const &critical) + { + double const ret + = i.size.y * (clamp (i.placement.x + i.size.x - b.right, + 0.0, i.size.x) + + clamp (b.left - i.placement.x, 0.0, i.size.x)) + + i.size.x * (clamp (b.top - i.placement.y, 0.0, i.size.y) + + clamp (i.placement.y + i.size.y - b.bottom, + 0.0, i.size.y)); + + return ret > -numeric_limits::min () ? 2 * ret + critical + : 0.0; + } + + + + static float overlap (Text::Item const &a, Text::Item const &b, + float const &critical_value) + { + static double const pad = 6.0; + + if (a.placement.x >= b.placement.x + b.size.x + pad + || a.placement.x + a.size.x <= b.placement.x - pad + || a.placement.y >= b.placement.y + b.size.y + || a.placement.y + a.size.y <= b.placement.y) + return 0; + + auto const right = min (a.placement.x + a.size.x, + b.placement.x + b.size.x) + pad; + auto const left = max (a.placement.x, b.placement.x) - pad; + auto const top = max (a.placement.y, b.placement.y); + auto const bottom = min (a.placement.y + a.size.y, + b.placement.y + b.size.y); + + return critical_value + sqrt ((right - left) * (bottom - top)); + } + + + + inline double squash (double const &x) + { + return 1.0 - exp (-x / 100.0); + } + + + inline double tension (Text::Item const &i) + { + return squash (hypot (i.native_position.x - i.placement.x, + i.native_position.y - i.placement.y)); + } + + + inline void bring_inside (Text::Item &i, Text::Boundary const &b) + { + i.placement = {clamp (i.placement.x, b.left, b.right - i.size.x), + clamp (i.placement.y, b.top, b.bottom - i.size.y)}; + } + + + + double Text::compute_tension (double const &critical_value, + Boundary const &boundary) const + { + double acc {0.0}; + + for (auto a = begin (items); a != end (items); ++a) + { + for (auto b = a + 1; b != end (items); ++b) + acc += overlap (*a, *b, critical_value); + + acc += tension (*a) + boundary_stress (*a, boundary, critical_value); + } + + return acc; + } + + + + void Text::shuffle (double shift_size, + double const &shift_limit, + Boundary const &boundary) + { + if (items.empty ()) return; + + for (; shift_size > shift_limit; shift_size /= 2.0) + { + for (auto &i : items) + bring_inside (i, boundary); + + auto const acceptable_limit = items.size (); + auto initial_tension = compute_tension (acceptable_limit, boundary); + + for (;;) + { + Item *hot_item {nullptr}; + enum { up, down, left, right} best_direction {up}; + double best_tension = numeric_limits::max (); + + { + auto test = [this, acceptable_limit, &best_tension, + &hot_item, &best_direction, boundary] + (Item &i, + Place const &shift, + decltype (best_direction) const &direction) + { + i.placement += shift; + auto const tension = compute_tension (acceptable_limit, + boundary); + if (tension < best_tension) + { + hot_item = &i; + best_direction = direction; + best_tension = tension; + } + }; + + for (auto &i : items) + { + test (i, Place {shift_size, 0}, right); + test (i, Place {- 2 * shift_size, 0}, left ); + test (i, Place {shift_size, shift_size}, up ); + test (i, Place {0, - 2 * shift_size}, down ); + + i.placement += Place {0, shift_size}; + } + } + + + if (! hot_item) return; + + + switch (best_direction) + { + case up: hot_item->placement += {0, shift_size}; break; + case down: hot_item->placement += {0, -shift_size}; break; + case left: hot_item->placement += {-shift_size, 0}; break; + case right: hot_item->placement += {shift_size, 0}; break; + } + + /* Has the tension decreased? If not we should give up, + * regardless of the acceptable limit. */ + + if (initial_tension - best_tension + < numeric_limits::epsilon ()) + break; + + if ((initial_tension = best_tension) < acceptable_limit) + break; + + } /* Loop back for further improvements. */ + + } /* End of loop over shift_size. */ + + } /* End of ‘shuffle’ function. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/text.h b/trader-desk/text.h new file mode 100644 index 0000000..d536dea --- /dev/null +++ b/trader-desk/text.h @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__TEXT__H +#define DMBCS__TRADER_DESK__TEXT__H + + +#include +#include +#include +#include +#include +#include + + +/** \file + * + * Declaration of the \c Text class. */ + + +namespace DMBCS::Trader_Desk { + + + /** The primary purpose of this class is to arrange short items of + * text in a rectangle of the screen in a way that avoids overlaps + * and keeps the items as close to their intended positions as + * possible. + * + * Thus the public interface is nice and simple: a method to add text + * items, \c operator+=, and a method to cause those items to be + * arranged optimally, \c arrange. + * + * The strategy is to move all items so that they fall inside the + * screen rectangle, and then for increasingly small deltas determine + * which movement, in any of the four cardinal directions, of which + * item leads to the best improvement in score: the total amount of + * area overlap of all the items plus a small number times the sum of + * the itemsʼ displacement from their starting values--we want to get + * rid of the overlaps at the expense of the displacements. The + * exercise continues until the overlaps are eliminated and delta is + * not more than the size of a pixel. */ + + struct Text + { + /** A structure to carry the details of a rectangular region on the + * screen. */ + struct Boundary { double left, right, top, bottom; }; + + + /** A structure to carry coordinates of a point on the screen, with + * some basic arithmetic where it serves our needs in the + * implementation of the \c Text class. */ + struct Place + { + double x {0.0}; + double y {0.0}; + + Place () = default; + + Place (double const &x_, double const &y_) : x (x_), y (y_) {} + + Place &operator+= (Place const &a) + { x += a.x; y += a.y; return *this; } + + Place &operator-= (Place const &a) + { x -= a.x; y -= a.y; return *this; } + }; + + + + /** An item of text to be placed on the screen. */ + + struct Item + { + /** The actual text string. */ + string text; + + /** The colour in which the text should be rendered (not actually + * needed in this class, but we carry the information along for + * the convenience of the application). */ + Colour colour; + + /** The place where we would like the text to appear. */ + Place native_position; + + /** The size of the box which encloses the text. */ + Place size; + + /** The computed place on the screen where the text should be + * printed so that it does not overlap with any other of the + * items. */ + Place placement; + + + /** Sole class constructor creates a fully populated object, + * except for the \c placement member which is set by subsequent + * optimization of a set of objects relative to each other. */ + Item (string const &t, Colour const &c, Place const &p, + Place const &s) + : text {t}, colour {c}, native_position {p}, size {s}, + placement {p} + {} + + }; /* End of class Item. */ + + + private: + + /* The actual collection of text items we accumulate and + * arrange. */ + vector items; + + + /* Compute a score, to be minimized, for the current arrangement of + * text \c Item \C placements. */ + double compute_tension (double const &critical_value, + Boundary const &boundary) const; + + + /* Randomly move some items of text around. */ + void shuffle (double shift_size, + double const &shift_limit, + Boundary const &boundary); + + + public: + + /** Return a vector of items whose \c placement's indicate the + * optimal positions to render each text item. */ + vector const &arrange (Boundary const &b) + { + for (auto &i : items) i.placement = i.native_position; + shuffle (10.0, 0.5, b); + return items; + } + + + /** Add a new text item to the list of items that need to be + * optimally arranged. */ + Text &operator+= (Item &&d) { items.emplace_back (move (d)); + return *this; } + + + } ; /* End of class Text. */ + + + } /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__TEXT__H. */ diff --git a/trader-desk/tide-mark.h b/trader-desk/tide-mark.h new file mode 100644 index 0000000..cdf694c --- /dev/null +++ b/trader-desk/tide-mark.h @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__TIDE_MARK__H +#define DMBCS__TRADER_DESK__TIDE_MARK__H + + +#include +#include + + +/** \file + * + * Definition and complete inline implementation of the \c Tide_Mark + * class. */ + + +namespace DMBCS::Trader_Desk { + + + /** A \c Tide_Mark is simply a point in (time, price)-space (i.e. an \c + * Event) through which we draw horizontal and vertical cross-hairs. + * The main requirement is to be able to pass a list of interesting + * points in time around the program--the current mouse position, + * current time, position-entry time--and then have each sub-system + * furnish a set of price marks; this is the purpose of the \c + * Price_Marker function type which is an object with time bound and + * which takes a price as an argument, with the end product being an + * actual \c Tide_Mark at that (time, price) point. + * + * Note further how the horizontal and vertical lines have distinctive + * colours. */ + + struct Tide_Mark : Event + { + /** The container type we (the application) use for containers of \c + * Tide_Mark's. */ + typedef vector List; + + /** The type of a function which manufactures a \c Tide_Mark using + * intrinsically predefined values for the time and time-tide + * colour. */ + typedef function + Price_Marker; + + /** Colour of price-wise tide-line. */ + Colour value_colour; + + /** Colour of time-wise tide-line. */ + Colour temporal_colour; + + + private: + + /* Construction must be done via the \c tide_mark function below. + * + * Rather than constructing these objects in one go, applications + * must first use \c price_marker() to manufacture us a function, + * which in turn manufactures a \c Tide_Mark. */ + constexpr Tide_Mark (Event const &e, Colour const &v, Colour const &t) + : Event {e}, value_colour {v}, temporal_colour {t} + {} + + + /* No reason to make copies, so guard against accidentally doing + * so. */ + Tide_Mark (Tide_Mark const &) = delete; + Tide_Mark &operator= (Tide_Mark const &) = delete; + + + public: + + /* Moves are necessary as these items are stored in \c vector's. */ + Tide_Mark (Tide_Mark &&) = default; + Tide_Mark &operator= (Tide_Mark &&) = default; + + + /** A factory method which produces a \c Price_Marker, which in turn + * produces \c Tide_Marks with event time and time-tide colours fixed + * at \a time and \a time_colour, respectively. */ + static Price_Marker price_marker (Time_Point const &time, + Colour const &time_colour) + { + return [time, time_colour] (Currency_Value const &v, Colour const &c) + { return Tide_Mark {{time, v}, c, time_colour}; }; + } + + + } ; /* End of class Tide_Mark. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__TIDE_MARK__H. */ diff --git a/trader-desk/time-series.cc b/trader-desk/time-series.cc new file mode 100644 index 0000000..3982971 --- /dev/null +++ b/trader-desk/time-series.cc @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include +#include +#include +#include + + +/** \file + * + * Implementation of the \c Time_Series class. */ + + +namespace DMBCS::Trader_Desk { + + + Time_Point const TODAY_MARK + { [] { + auto round_days = [] (time_t const &t) + { + struct tm tm; localtime_r (&t, &tm); + + tm.tm_sec = 0; + tm.tm_min = 0; + tm.tm_hour = 0; + + return mktime (&tm); + }; + + return chrono::system_clock::from_time_t + (round_days + (chrono::system_clock::to_time_t + (chrono::system_clock::now ()))); + } () + }; + + + + inline Duration::rep T (Time_Point const &t) + { + return number (t.time_since_epoch ()); + } + + + + void Time_Series::insert_event (Event const &e) + { + const auto t {find_if (begin (), end (), [t = e.time] (const Event& a) + { return a.time < t; })}; + + if (t == end ()) emplace_back (e); + else insert (t, e); + } + + + + void Time_Series::extend_range (DB &db, + int const seqid, + Duration const &window_size) + { + auto const earliest_date = (empty () ? TODAY_MARK : front ().time) + - window_size; + + auto const last_time = empty () ? TODAY_MARK + : back ().time - chrono::seconds (1); + + if (earliest_date <= last_time) + { + auto const x = from_database (db, + seqid, + last_time, + last_time - earliest_date, + market_close_time); + + insert (end (), std::begin (x), std::end (x)); + } + } + + + + Time_Series Time_Series::from_database (DB &db, + int const seqid, + Time_Point const &latest_date, + Duration const &window_size, + Duration const &market_close_time) + { + Time_Series ret {market_close_time}; + + + /* If the user has entered a recent price for this stock, we need to + * splice the value into the time-series we produce (presumably we are + * only interested in doing this if the date is later than any data we + * already have). */ + + time_t user_date {0}; + Currency_Value user_price {0.0}; + + { + auto sql = db.row_query (); + + sql << "select UNIX_TIMESTAMP(last_price_date), last_price " + << " from company " + << " where company.seqid=" << seqid; + + sql.execute (); + + if (sql) { + user_date = sql.next_entry (user_date); + user_price = sql.next_entry (user_price); + ++sql; + } + } + + if (user_date > T (latest_date)) user_date = 0; + + { + auto sql = db.row_query (); + + sql << " select unix_timestamp(date), close " + << " from prices " + << " where company=" << seqid + << " and date >= from_unixtime(" + << T (latest_date - window_size) << ") " + << " and date <= from_unixtime(" << T (latest_date) << ") " + << "order by date desc"; + + sql.execute (); + + if (! sql) return ret; + + auto user_time = chrono::system_clock::from_time_t (user_date); + + for (; sql; ++sql) + { + auto const date = sql.next_entry () + + ret.market_close_time; + + if (user_time > date) + { + ret.emplace_back (user_date, user_price); + user_time = chrono::system_clock::from_time_t (0); + } + + ret.emplace_back (date, sql.next_entry (Currency_Value {0.0})); + } + } + + return ret; + + } /* End of from_database method. */ + + + + auto Time_Series::interpolated_value (Time_Point const &date) const + -> Currency_Value + { + if (empty ()) + return 0; + + auto const t = lower_bound (rbegin (), rend (), + Event {date, 0.0}, + [] (value_type const &a, + value_type const &b) + { return a.time < b.time; }); + + if (t == rbegin ()) + return t->price; + + return (t-1)->price + (t->price - (t-1)->price) + * ((T (date) - T ((t-1)->time)) + / (double) (T (t->time) - T ((t-1)->time))); + } + + + + auto Time_Series::get_range (Duration const &date_range) const -> Range + { + Range ret; + + if (empty ()) + return ret; + + ret.end_time = front ().time; + + const_iterator const end_ + = date_range == chrono::seconds::max () + ? end () + : std::upper_bound (begin (), + end (), + ret.end_time - date_range, + [] (Time_Point const &val, value_type const &a) + { return a.time < val; }); + + auto const e + = minmax_element (begin (), + end_, + [] (value_type const &a, value_type const &b) + { return a.price < b.price; }); + + ret.min_value = e.first->price; + ret.max_value = e.second->price; + + if (end_ != begin ()) ret.start_time = (end_ - 1)->time; + else ret.start_time = end_->time; + + return ret; + } + + + + Time_Series Time_Series::compute_moving_average (Time_Series const &in, + Duration const &window, + Time_Point const &earliest) + { + if (in.size () < 2) + return in; + + Time_Series ret {in.market_close_time}; + + auto const forward_window_size = window / 2; + auto const backward_window_size = window - forward_window_size; + + auto const start_time = earliest - window; + + /* These iterators run through _increasing_ dates. */ + auto window_front = in.rbegin (); + while (window_front->time < start_time && window_front != in.rend ()) + ++window_front; + auto window_back = window_front; + + int count = 0; + double sum = 0.0; + + for (; + window_front != in.rend () + && window_front->time < window_back->time + forward_window_size; + ++ window_front) + { + sum += window_front->price; + ++count; + } + + ret.emplace_back (window_back->time, + count > 0 ? sum / count : 0.0); + + for (auto i = window_back + 1; i != in.rend (); ++i) + { + for (; + window_front != in.rend () + && window_front->time < i->time + forward_window_size; + ++window_front) + { + sum += window_front->price; + ++ count; + } + + for (; + window_back != window_front + && window_back->time < i->time - backward_window_size; + ++window_back) + { + sum -= window_back->price; + -- count; + } + + ret.emplace_back (i->time, count > 0 ? sum / count : 0.0); + } + + reverse (std::begin (ret), std::end (ret)); + + return ret; + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/time-series.h b/trader-desk/time-series.h new file mode 100644 index 0000000..d9d98bb --- /dev/null +++ b/trader-desk/time-series.h @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__TIME_SERIES__H +#define DMBCS__TRADER_DESK__TIME_SERIES__H + + +#include +#include +#include + + +/** \file + * + * Definition of \c Time_Point, Currency_Value, Duration types and the + * composed \c Event type, declaration of \c Time_Series class, and + * definition of convenience functions \c number (count ticks in + * duration) and \c t (construct a \c time_t from real-world calendar + * data). */ + + +namespace DMBCS::Trader_Desk { + + + /** All trades and closing prices are fixed to a \c Time_Point. */ + typedef chrono::system_clock::time_point Time_Point; + + /** All trades and closing prices are expressed as \c + * Currency_Value's. */ + typedef double Currency_Value; + + /** All time spans are expressed as \c Duration's. */ + typedef Time_Point::duration Duration; + + + /* The time at which the day started when this application started + * running. Whenever we consider closing prices, we regard this as the + * high-tide mark of the window of closing events. */ + /* !!! We really need to re-evaluate the need for this. */ + extern const Time_Point TODAY_MARK; + + + /** Whatever flavour of \c DURATION \a t we have, determine the number + * of complete \c TICK's that the duration spans. */ + template + inline constexpr Duration::rep number (DURATION const &t) + { + return chrono::duration_cast (t) . count (); + } + + + /** Convert from the ‘normal’ notion of calendar date to a \c time_t. + * THE ARGUMENTS ARE STREET-WISE VALUES. */ + inline time_t t (int const year, int const month, int const day, + int const hour, int const minute, int const second) + { + struct tm t {second, minute, hour, day, month - 1, year - 1900, + 0, 0, 0, 0, 0}; + return mktime (&t); + } + + /** Convert from the ‘normal’ notion of calendar date to a \c time_t. + * THE ARGUMENTS ARE STREET-WISE VALUES. */ + inline time_t t (int const year, int const month, int const day) + { + return t (year, month, day, 0, 0, 0); + } + + + + /** Every trade position open and close, and every market-closing price, + * is expressed as an \c Event: simply a time/value pair. */ + struct Event + { + Time_Point time; + Currency_Value price {0.0}; + + constexpr Event (const Time_Point& t, const Currency_Value& c) + : time {t}, price {c} + {} + + Event (const time_t t, const Currency_Value& c) + : Event {chrono::system_clock::from_time_t (t), c} + {} + + constexpr Event () = default; + }; + + + + /** A vector of (time, price) pairs representing the value history of a + * commodity. It is a class invariant that the vector will ALWAYS be + * sorted with later dates at the front, earlier ones at the back (the + * motivation being that when the history needs to be extended, it will + * invariably be extended backwards in time and then the new data will + * simply be appended to the existing data vector). */ + + struct Time_Series : vector + { + /** A general-purpose transparent data object used to demarcate a box + * in (time x price) space, usually used to indicate the achieved + * bounds of a \c Time_Series. Note that \c start_time is + * numerically less than \c end_time. */ + + struct Range + { + Time_Point start_time; + Time_Point end_time; + Currency_Value min_value {0}; + Currency_Value max_value {0}; + + /** Is the event \a e inside or on the border of this range? */ + constexpr bool contains (const Event& e) const + { return start_time <= e.time && end_time >= e.time + && min_value <= e.price && max_value >= e.price; } + + constexpr bool operator!= (const Time_Series::Range& a) const + { return a.start_time != start_time || a.end_time != end_time + || a.min_value != min_value || a.max_value != max_value; } + }; + + + /** The number of seconds after midnight that the market from which + * this time-series derives closes. */ + Duration market_close_time; + + + /** Effectively our null constructor, creating an empty time + * series. */ + explicit Time_Series (const Duration& m) : market_close_time {m} + {} + + + /** The one useful (named) class constructor. Manufacture a new + * time-series extracted from the database. The time-span will be + * from \a latest_time and spanning \a window_size'd time interval to + * the past. The \a market_close_time is added to the date of all + * data read from the closing prices table. */ + static Time_Series from_database (DB &db, + const int seqid, + const Time_Point& latest_time, + const Duration& window_size, + const Duration& market_close_time); + + + /** Get more data from the database, extending the length of the time + * series we are holding further back in time, from our latest datum + * to a distance \a window_size back in time. */ + void extend_range (DB &db, + const int seqid, + const Duration& window_size); + + + /** Get the value of the commodity at a point in time, linearly + * interpolated between the two closest spanning points recorded in + * the time-series. */ + Currency_Value interpolated_value (const Time_Point& date) const; + + + /** Get a \c Range object which boxes the time-series data up to an + * interval of \a date_range into the past. */ + Range get_range (const Duration& date_range) const; + + + /** Get a \c Range object which boxes the entire time-series data + * (this is actually dealt with as a special case inside the above + * method). */ + Range get_range () const + { return get_range (chrono::seconds::max ()); } + + + /** Insert \a e into its correct place in the current time-series. + * This will likely be an expensive operation. */ + void insert_event (const Event& e); + + + /** Produce a new time series by applying a moving average of size \a + * window to the \a incoming one, which is left unchanged. The + * returned series only goes back as far as \a earliest_time. */ + static Time_Series compute_moving_average (const Time_Series& incoming, + const Duration& window, + const Time_Point& earliest_time); + + + }; /* End of class Time_Series. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__TIME_SERIES__H. */ diff --git a/trader-desk/trade-instruction.cc b/trader-desk/trade-instruction.cc new file mode 100644 index 0000000..6dfcb02 --- /dev/null +++ b/trader-desk/trade-instruction.cc @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include "../trader-desk/trade-instruction.h" +#include + + +/** \file + * + * Implementation of \c Trade_Instruction class. */ + + +namespace DMBCS::Trader_Desk { + + + Currency_Value Trade_Instruction::value () const + { + Currency_Value ret; + istringstream (entry.get_text ()) >> ret; + return ret; + } + + + + void Trade_Instruction::chart_data_changed () + { + if (! chart_data.prices.empty ()) + { + ostringstream hold; + + hold << (chart_data.latest_price.price > 0.0 + ? chart_data.latest_price.price + : chart_data.prices.front ().price); + + entry.set_text (hold.str ().c_str ()); + } + } + + + + Trade_Instruction::Trade_Instruction (Preferences& P, + Chart_Data &cd) : chart_data (cd) + { + pack_start (main_label, Gtk::PACK_SHRINK, 0); + pack_start (entry, Gtk::PACK_SHRINK, 0); + pack_start (units_label, Gtk::PACK_SHRINK, 0); + + entry.set_width_chars (4); + + /* Set up the trade_now_button. */ + chart_data_changed (); + + entry.signal_activate () + .connect ([this, &P] + { DB db {P}; + chart_data.note_current_price (db, value ()); }); + + chart_data.changed_signal + .connect ([this] { chart_data_changed (); }); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/trade-instruction.h b/trader-desk/trade-instruction.h new file mode 100644 index 0000000..e09d441 --- /dev/null +++ b/trader-desk/trade-instruction.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__TRADE_INSTRUCTION__H +#define DMBCS__TRADER_DESK__TRADE_INSTRUCTION__H + + +#include +#include + + +/** \file + * + * Declaration of the \c Trade_Instruction class. */ + + +namespace DMBCS::Trader_Desk { + + + /** A component of a \c Hand_Analysis_Widget which entirely looks after + * itself (interacting with the rest of the program through signals), + * and allows the user to set the current price of a commodity, and to + * indicate that /buy/ and /sell/ actions have taken place on that + * current price. + * + * It is expected that the owning \c Hand_Analysis_Widget will maintain + * the \c Chart_Data object given to the constructor for the lifetime + * of one of these objects. */ + + class Trade_Instruction : public Gtk::HBox + { + /** The data we have on the commodity being watched. We react to any + * changes in these via the \c chart_data_changed method. */ + Chart_Data &chart_data; + + /** String printed in front of information box. */ + Gtk::Label main_label {"Current price "}; + + /** String printed after information box. */ + Gtk::Label units_label {"p "}; + + /** The user edit box. Updates from the user are sent directly to \c + * chart_data. */ + Gtk::Entry entry; + + + /** The user has pressed the buy/sell button. */ + void trade_now_pressed (); + + /** A change in the chart (price history of commodity of interest) has + * taken place. */ + void chart_data_changed (); + + + /** Get the contents of the edit box as a currency (pence) value. */ + Currency_Value value () const; + + + /* Duplicating one of these objects is both pointless and dangerous. */ + Trade_Instruction (Trade_Instruction const &) = delete; + Trade_Instruction &operator= (Trade_Instruction const &) = delete; + Trade_Instruction (Trade_Instruction &&) = delete; + Trade_Instruction &operator= (Trade_Instruction &&) = delete; + + + public: + + /** Sole constructor which registers the \a chart_data object we + * follow, and sets up the GTK widget ready for use. */ + Trade_Instruction (Preferences&, Chart_Data &); + + + }; /* End of class Trade_Instruction. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__TRADE_INSTRUCTION__H. */ diff --git a/trader-desk/trader-desk.cc b/trader-desk/trader-desk.cc new file mode 100644 index 0000000..cd4ef4e --- /dev/null +++ b/trader-desk/trader-desk.cc @@ -0,0 +1,513 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include "auto-config.h" +#include "alpha-vantage--monitor.h" +#include "application.h" +#include "markets.h" +#include "update-latest-prices.h" +#include "wizard.h" + + +/** \file + * + * Implementation of the \c Window object, and the applicationʼs \c main + * entry point. */ + + +namespace DMBCS::Trader_Desk { + + +static void grid_injector (Chart_Grid &grid, + const Update_Latest_Prices::Data& data) + { + Chart *const c {grid.find_chart (data.company_seqid)}; + + if (c) c->data.new_event ({chrono::system_clock::to_time_t (data.time), + data.price}); + } + + + +struct Window : Gtk::Window + { + /** All of the pieces that make up this GTK Window application, + * including widgets that go in the window, engine parts, and + * glue. */ + Application app; + + /** Top-level layout: menu stacked on top of a notebook. */ + Gtk::VBox v_box; + Gtk::HBox menu_strip; + Gtk::HBox alphavantage_clocks; + + + explicit Window (Preferences&&); + + void update_latest_data (); + + void change_company_name (Chart_Data& chart_data, const int& seqid); + + void create_application (Preferences&& P); + + void destroy_application (); + + + }; /* End of Window class. */ + + +static void show_about () + { + Gtk::AboutDialog a; + a.set_program_name (PACKAGE); + a.set_version (VERSION); + a.set_copyright + (gettext ("Copyright (c) Dale Mellor 2017, 2020\nGPLv3+ applies")); + a.set_documenters (vector {"Dale Mellor"}); + a.set_license_type (Gtk::LICENSE_GPL_3_0); + a.set_website ("https://rdmp.org/trader-desk"); + a.set_authors (vector {"Dale Mellor"}); + a.set_logo (Gdk::Pixbuf::create_from_file (PKGDATADIR "/trader-desk.png") + ->scale_simple (100, 100, Gdk::INTERP_NEAREST)); + a.run (); + } + + +static void run_wizard + (Preferences& P, + const bool force, + std::function post_process = {}) + { + Wizard *const W {new Wizard {P.file_path, force}}; + /* W->set_transient_for (*this); */ + /* W->set_attached_to (*this); */ + W->set_modal (1); + W->show (); + W->signal_unmap ().connect ([W, &P, post_process] + { P = W->database_prefs.prefs; + dump (P); + if (post_process) post_process (P); + delete W; }); + } + + + +static void run_preferences_dialog + (Window& W, const string& alpha_vantage_message = {}) + { + auto D {alpha_vantage_message.length () + ? new Preferences_Dialog {W, W.app.user_prefs, + alpha_vantage_message} + : new Preferences_Dialog {W, W.app.user_prefs}}; + D->signal_hide () + .connect ([&W, D, P=W.app.user_prefs] + { delete D; + run_wizard (W.app.user_prefs, 1, + [&W, P] (Preferences& newP) + { + if (! database_equal (P, newP)) + { + W.destroy_application (); + W.create_application (Preferences {newP}); + } + }); }); + D->show (); + } + + + +static void run_unforced_wizard (Window& W) + { + const auto P {W.app.user_prefs}; + + run_wizard (W.app.user_prefs, 0, + [&W, P] (Preferences& newP) + { + if (! database_equal (P, newP)) + { + W.destroy_application (); + W.create_application (Preferences {newP}); + } + }); + } + + + +void Window::change_company_name (Chart_Data& chart_data, const int& seqid) + { + unique_ptr db; + + for (auto &a : app.market_grids) + { + Chart *const c {a->find_chart (seqid)}; + + if (c) { chart_data.subsume (&c->data); + if (! db) db = make_unique (app.user_prefs); + app . hand_analysis -> company_name + . read_names (*db, a->market.seqid, seqid); + return; } + } + } + + + +void Window::create_application (Preferences&& P) + { + app.window = this; + app.user_prefs = std::move (P); + + app.preferences_error + = [this] (const string& alpha_vantage_message) + { run_preferences_dialog (*this, alpha_vantage_message); }; + + app.hand_analysis + = make_unique + ([this] (Chart_Data &cd, int const &seqid) + { change_company_name (cd, seqid); }, + app.user_prefs); + + app.actions = Gtk::ActionGroup::create (); + + app.actions->add (Gtk::Action::create ("quit", Gtk::Stock::QUIT), + Gtk::AccelKey {"q"}, + [this] { close (); }); + + app.actions->add (Gtk::Action::create ("file-menu", + pgettext ("Menu", "_File"))); + app.actions->add + (Gtk::Action::create ("preferences", + pgettext ("Menu", "_Preferences")), + Gtk::AccelKey {"p"}, + [this] { run_preferences_dialog (*this); }); + app.actions->add + (Gtk::Action::create ("wizard", + pgettext ("Menu", "_Wizard")), + Gtk::AccelKey {"w"}, + [this] { run_unforced_wizard (*this); }); + app.actions->add (Gtk::Action::create ("display-menu", + pgettext ("Menu", "_Market"))); + app.actions->add (Gtk::Action::create ("market-menu", + pgettext ("Menu", "_Update"))); + app.actions->add (Gtk::Action::create ("help-menu", + pgettext ("Menu", "_Help"))); + app.actions->add (Gtk::Action::create ("about", + pgettext ("Menu", "_About")), + &show_about); + + + DB db {app.user_prefs}; + for (auto &m : Markets {db}) + if (m.second.tracked) + { + const auto i {app.market_grids.size ()}; + + if (i == 0) set_title (pgettext ("Label", "Trader Desk") + + string {" : "} + + m.second.world_data.name); + + app . market_grids + . emplace_back (make_unique + (app.user_prefs, m.second)); + + /* This needs to go into a sub-routine as it is required + * elsewhere. */ + + ostringstream a; a << "display-grid-" << i; + + if (i < 9) + { + ostringstream b; b << "" << i+1; + + app . actions + -> add (Gtk::Action::create (a.str (), + m.second.world_data.name), + Gtk::AccelKey (b.str ()), + [this, i, name = m.second.world_data.name] + { app.display_grid (i); + set_title (pgettext ("Label", + "Trader Desk") + + string {" : "} + + name); }); + } + + else + app.actions->add (Gtk::Action::create (a.str (), + m.second.world_data.name), + [this, i, name = m.second.world_data.name] + { app.display_grid (i); + set_title (pgettext ("Label", + "Trader Desk") + + string {" : "} + + name); }); + } + + + app.last_data_menu + = Gtk::Action::create ("update-recent", + pgettext ("Menu", "Update _latest data")); + app.last_data_menu->set_sensitive (0); + app.actions->add (app.last_data_menu, + [this] { update_latest_data (); }); + + app.close_data_menu + = Gtk::Action::create ("update-closes", + pgettext ("Menu", "Update _close data")); + app.actions->add (app.close_data_menu, + [this] { app.update_closing_prices (); }); + + app.actions->add (Gtk::Action::create ("ingest-new-market", + pgettext ("Menu", + "_Ingest new market")), + [this] { app.ingest_new_market (); }); + + app.ui_manager = Gtk::UIManager::create (); + + app.ui_manager->insert_action_group (app.actions); + + add_accel_group (app.ui_manager->get_accel_group ()); + + app.ui_manager->add_ui_from_string + ("" + " " + " " + " " + " " + " " + " " + " " + " " + + + [this] + { ostringstream a; + for (size_t i=0; i < app.market_grids.size (); ++i) + a << "\n"; + return a.str (); } () + + + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""); + + v_box.pack_start (menu_strip, Gtk::PACK_SHRINK); + menu_strip.pack_start (*app.ui_manager->get_widget ("/menu"), + Gtk::PACK_SHRINK); + menu_strip.pack_end (alphavantage_clocks, Gtk::PACK_SHRINK); + Alpha_Vantage__Monitor::make_clock_widgets (app.user_prefs, + alphavantage_clocks); + Glib::signal_timeout() + .connect ([] { Alpha_Vantage__Monitor::clocks->update (); + return 1; }, + 200); + + + + /* v_box.pack_start (*app.ui_manager->get_widget ("/ToolBar"), */ + /* Gtk::PACK_SHRINK); */ + + app.notebook.set_show_tabs (0); + app.notebook.set_show_border (0); + app.notebook.append_page (*app.hand_analysis); + + if (app.market_grids.empty ()) + app.notebook.append_page (*Gtk::manage (new Gtk::Label {"No markets"})); + else + for (auto &g : app.market_grids) + app.notebook.append_page (*g); + + v_box.pack_start (app.notebook); + add (v_box); + + show_all (); + + app.display_grid (0); + + for (auto &a : app.market_grids) + a->selection_signal + .connect ([this, &a] { app.hand_analysis->subsume_selected (*a); + app.close_data_menu->set_sensitive (0); + app.last_data_menu->set_sensitive (1); + app.notebook.set_current_page (0); }); + + if (app.market_grids.empty ()) + app.ingest_new_market (); + + set_default_size (650, 420); + set_icon_from_file (PKGDATADIR "/trader-desk.png"); + + } /* End of create_application. */ + + + +void Window::destroy_application () + { + for (auto *const W : app.notebook.get_children ()) + app.notebook.remove (*W); + app.market_grids.clear (); + app.hand_analysis.reset (); + for (auto *const W : menu_strip.get_children ()) + menu_strip.remove (*W); + v_box.remove (menu_strip); + Alpha_Vantage__Monitor::remove_clock_widgets (alphavantage_clocks); + v_box.remove (app.notebook); + remove (/* v_box */); + } + + + +void Window::update_latest_data () + try + { + DB db {app.user_prefs}; + + if (app.notebook.get_current_page () == 0) + { + Update_Latest_Prices::do_update (db, + app.hand_analysis->chart.data, + app.user_prefs); + return; + } + + Chart_Grid *const market + {app.market_grids [app.notebook.get_current_page ()-1].get ()}; + + Update_Latest_Prices::Work update {market->market}; + + do_update (&update, + db, + app.user_prefs, + [&db, market] + (const Update_Latest_Prices::Data& data) -> void + { sql_injector (db, data); + grid_injector (*market, data); }); + } + catch (const Alpha_Vantage::Error& E) + { + Gtk::MessageDialog + {*this, + gettext ("There seems to be a problem with the AlphaVantage " + "account, please check your settings on the " + "preferences panel. The message from the " + "server is:") + string {"\n\n‘"} + E.what () + "’", + 0, + Gtk::MESSAGE_ERROR} + .run (); + run_preferences_dialog (*this); + } + catch (Update_Closing_Prices::No_Connection const &) + { + Gtk::MessageDialog {*this, + gettext ("No Internet Connection"), + 0, + Gtk::MESSAGE_WARNING} + .run (); + } + + + +Window::Window (Preferences&& P) : app {std::move (P)} + { + signal_map_event () + .connect ([this] (GdkEventAny*) -> bool + { run_wizard (app.user_prefs, 1, + [this] (Preferences& P) + { create_application (Preferences {P}); }); + return 0; } ); + } + + +} /* End of namespace DMBCS::Trader_Desk. */ + + + +int main (int argc, char **argv) +try + { + setlocale (LC_ALL, ""); + bindtextdomain (PACKAGE, LOCALEDIR); + textdomain (PACKAGE); + + /* Needed for Alpha_Vantage. */ + curlpp::Cleanup curl_lifetime; + + std::string config_file; + + if (argc > 1) + { + using std::cout; + + if (argv [1] == std::string {"--config"} + || argv [1] == std::string {"-c"}) + { + if (argc < 3) + { + std::cerr << PACKAGE_STRING + << "Error: -c option requires an argument.\n"; + exit (1); + } + config_file = argv [2]; + } + + else if (argv [1] == std::string ("--version")) + { + cout << PACKAGE_STRING << '\n'; + cout << gettext ("Copyright (C) 2017, 2020 Dale Mellor") << "\n\n" + << gettext ("License GPLv3+: GNU GPL version 3 or later " + "\n" + "This is free software: you are free to change " + "and redistribute it.\n" + "There is NO WARRANTY, to the extent permitted " + "by law.\n"); + exit (0); + } + else if (argv [1] == std::string ("--help")) + { + cout << gettext ("usage") << ": trader-desk [options]\n"; + cout << gettext ("To report bugs or contact the authors please " + "refer to http://rdmp.org/trader-desk\n"); + exit (0); + } + } + + Glib::thread_init (); + + Gtk::Main kit (argc, argv); + + namespace TD = DMBCS::Trader_Desk; + TD::Window window {config_file.empty () + ? TD::Preferences::from_default_file () + : TD::Preferences::from_file (config_file)}; + + Gtk::Main::run (window); + + return 0; + } + +catch (std::runtime_error const &e) + { + std::cerr << e.what () << ".\n"; + std::exit (1); + } diff --git a/trader-desk/update-closing-prices.cc b/trader-desk/update-closing-prices.cc new file mode 100644 index 0000000..7364a78 --- /dev/null +++ b/trader-desk/update-closing-prices.cc @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include + + +/** \file + * + * Implementation of the \c Update_Closing_Prices class. */ + + +namespace DMBCS::Trader_Desk { namespace Update_Closing_Prices { + + + void sql_injector (DB& db, const Data& data) + { + db.quick () << "replace into prices set date=\"" + << data.year << "-" << data.month << "-" << data.day + << "\", open=" << data.open + << ", high=" << data.high + << ", low=" << data.low + << ", close=" << data.close + << ", volume=" << data.volume + << ", adjusted_close=" << data.adj_close + << ", company=" << data.company_seqid; + + db.quick () << "update company set last_close_date=greatest(\"" + << data.year << "-" << data.month << "-" << data.day + << "\", last_close_date) where seqid=" << data.company_seqid; + } + + + + vector entries_from_database (DB& db, const size_t market_seqid) + { + auto sql {db.row_query ()}; + + sql << "select rtrim(name), " + << " symbol, " + << " seqid, " + << " unix_timestamp(greatest(date_add(current_date()," + << " interval -" << 10/* TIME_HORIZON */ << " year)," + << "last_close_date)) " + << " from company " + << " where market=" << market_seqid; + + sql.execute (); + + vector entries; + entries.reserve (sql.number_rows ()); + + for (; sql; ++sql) + { + Company e; + sql >> e.name >> e.symbol >> e.seqid >> e.last_close_date; + entries.push_back (move (e)); + } + + return entries; + } + + + + static tm current_tm () + { + using C = chrono::system_clock; + const time_t current_t {C::to_time_t (C::now ())}; + return *localtime (¤t_t); + } + + static tm tm_from (const time_t date) + { + tm start {*localtime (&date)}; + /* ++ start.tm_mday; */ + /* mktime (&start); */ + return start; + } + + static bool same_day (const tm& A, const tm& B) + { + return A.tm_year == B.tm_year && A.tm_mon == B.tm_mon + && A.tm_mday == B.tm_mday; + } + + static tm not_weekend (tm&& T) + { + mktime (&T); + if (T.tm_wday == 0) T.tm_mday -= 2; + else if (T.tm_wday == 6) T.tm_mday -= 1; + else return T; + mktime (&T); + return T; + } + + static tm day_before (tm& T) + { + mktime (&T); + T.tm_mday -= 1; + mktime (&T); + return T; + } + + +static void do_update + (Work& ucp, + Preferences& user_prefs, + const function progress_callback, + const function injector, + const function done_processing, + const vector& entries) + { + ucp.stop = false; + + for (size_t i {0}; i < entries.size (); ++i) + { + if (ucp.stop) break; + + const Company& company {entries [i]}; + + if (progress_callback) + progress_callback (i / double (entries.size ()), company); + + tm now {not_weekend (current_tm ())}; + const auto close_hour {chrono::duration_cast + (ucp.market.world_data.close_time).count ()}; + const auto close_min {chrono::duration_cast + (ucp.market.world_data.close_time).count () + % 60}; + if (now.tm_hour < close_hour + || (now.tm_hour == close_hour && now.tm_min < close_min)) + now = day_before (now); + + if (!same_day (tm_from (company.last_close_date), now)) + try { while (Price_Server::get_closing_prices + (company, + ucp.market.world_data.component_extension, + user_prefs, + injector) + == Price_Server::TO_DO::MORE_WORK) + if (ucp.stop) break; } + + catch (const Price_Server::Bad_API_Key&) { throw; } + + catch (const Price_Server::Error&) + { cerr << "Skipping company " << company.name << ".\n"; } + + /* This will be thrown by curlpp if there is any serious problem + * with the networking. */ + catch (const exception& e) { throw No_Connection {e}; } + + if (ucp.stop) break; + if (done_processing) done_processing (company.seqid); + } + } + + + + void do_update + (Work& ucp, + DB& db, + Preferences& user_prefs, + const function progress_callback, + const function injector, + const function company_done) + { + return do_update (ucp, + user_prefs, + progress_callback, + injector, + company_done, + entries_from_database (db, ucp.market.seqid)); + } + + +} } /* End of namespace DMBCS::Trader_Desk::Update_Closing_Prices. */ diff --git a/trader-desk/update-closing-prices.h b/trader-desk/update-closing-prices.h new file mode 100644 index 0000000..c736fa4 --- /dev/null +++ b/trader-desk/update-closing-prices.h @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__UPDATE_CLOSING_PRICES__H +#define DMBCS__TRADER_DESK__UPDATE_CLOSING_PRICES__H + + +#include +#include + + +/** \file + * + * Declaration of the \c Update_Closing_Prices class. */ + + +namespace DMBCS::Trader_Desk { + + + struct Alpha_Vantage__Monitor; + using Price_Server = Alpha_Vantage__Monitor; + + + /** Really a function object, the implementation being the \c run + * method. The purpose of the combined function is to scan the + * database for all companies in some market, use the Yahoo! API to get + * the latest prices for that company, and then ‘inject’ the results to + * some output destination, most likely back to the database itself, + * but also maybe to working \c Chart_Data objects. */ + + namespace Update_Closing_Prices + { + /** We will throw this object any time we canʼt get service from + * Yahoo!. */ + struct No_Connection : runtime_error + { + explicit No_Connection (const exception& E) + : runtime_error {pgettext ("Error", + "Cannot get data from Internet") + + string {": "} + E.what ()} + {} + }; + + + /** Vessel to hold the data which comes back from the Yahoo! + * server. */ + struct Data + { + /** Our own database sequence ID. */ + int company_seqid; + + /** The date of the closing transaction. */ + int year, month, day; + + /** The values of the daily amounts. We put all of this into + * the database, though the application currently only takes + * any notice of the \c close prices. */ + double open, high, low, close; + + /** The volume of trade on this day--put into the database, + * but not currently used by the application. */ + int volume; + + /** The ‘adjusted’ closing price--put into the database, but + * not currently used by the application. */ + double adj_close; + }; + + + /** Vessel to hold information about companies for which we need to + * get updated information. */ + struct Company + { + /** Human-readable name of the company. */ + string name; + + /** The companyʼs ticker symbol. */ + string symbol; + + /** Our database sequence ID for the company. */ + int seqid; + + /** The time of our most recent close price for this company. */ + time_t last_close_date; + }; + + + + struct Work + { + /** Our database sequence ID for the market in question. */ + const Market_Meta_Data& market; + + /** A flag to tell us to stop processing (may be set from + * outside the class). */ + atomic stop {false}; + }; + + + /** Write the \a data to the database. */ + void sql_injector (DB&, const Data&); + + + /** Get the list of companies on which to get new data, from the + * database based on the market. */ + vector entries_from_database (DB&, const size_t market_seqid); + + + /** Do the work: scan the \a db database for the appropriate \a + * companies and currently known closing prices, obtain new price + * information from data service and store these back to the database. The + * return will list the symbols of companies of which we were unable to + * obtain the latest information. + * + * If given (not \c nullptr), the \a progress_callback will be called + * at regular intervals with the percentage of work completed. + * + * The \a injector will be called for every new datum that needs adding + * to a companyʼs price records. + * + * If not \c nullptr, the \a company_done callback will be called after + * all data for a particular company have been processed as above. */ + void do_update + (Work&, + DB&, + Preferences&, + const function progress_callback, + const function injector, + const function company_done); + + + }; /* End of namespace Update_Closing_Prices. */ + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__UPDATE_CLOSING_PRICES__H. */ diff --git a/trader-desk/update-latest-prices.cc b/trader-desk/update-latest-prices.cc new file mode 100644 index 0000000..27e47f6 --- /dev/null +++ b/trader-desk/update-latest-prices.cc @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include +#include +#include +#include + + +/** \file + * + * Implementation of the \c Update_Latest_Prices class. */ + + +namespace DMBCS::Trader_Desk::Update_Latest_Prices { + + + using Data_Server = Alpha_Vantage__Monitor; + + + void sql_injector (DB& db, const Data& data) + { + time_t t = chrono::system_clock::to_time_t (data.time); + tm tm; + localtime_r (&t, &tm); + + db.quick () << "update company " + << "set last_price=" << data.price << ',' + << "last_price_date=\"" << (tm.tm_year+1900) << '-' + << (tm.tm_mon+1) + << '-' << tm.tm_mday << ' ' + << tm.tm_hour << ':' << tm.tm_min + << ':' << tm.tm_sec << "\" " + << " where seqid=" << data.company_seqid; + } + + + + static void do_update (Work *const work, + Preferences& P, + function injector, + vector const &entries) + try + { + for (const Company& C : entries) + injector (Data_Server::get_latest_data + (C, work->market.world_data.component_extension, P)); + } + catch (exception const &e) + { + throw Update_Closing_Prices::No_Connection {e}; + } + + + + void do_update (Work *const work, + DB& db, + Preferences& P, + function injector) + { + do_update (work, + P, + injector, + Update_Closing_Prices::entries_from_database + (db, work->market.seqid)); + } + + + + void do_update (DB& db, Chart_Data& data, Preferences& P) + { + auto row {db.row_query ()}; + row << "select company.symbol, " + << "unix_timestamp(company.last_close_date), " + << "market.component_extension " + << "from company, market " + << "where company.seqid=" << data.company_seqid + << " and market.seqid=company.market"; + row.execute (); + + Company C; + row >> C.symbol >> C.last_close_date; + C.seqid = data.company_seqid; + + const string market_symbol {row.next_entry ()}; + + const Data D {Data_Server::get_latest_data (C, market_symbol, P)}; + + ++row; + + {unique_lock L {data.prices_mutex}; + data.last_fetch_time = D.time; + data.prices.insert_event ( {D.time, D.price} ); + data.extremes.end_time = D.time; } + + data.changed_signal.emit (); + } + + +} /* End of namespace DMBCS::Trader_Desk::Update_Latest_Prices. */ diff --git a/trader-desk/update-latest-prices.h b/trader-desk/update-latest-prices.h new file mode 100644 index 0000000..73815b9 --- /dev/null +++ b/trader-desk/update-latest-prices.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS_TRADER_DESK__UPDATE_LATEST_PRICES__H +#define DMBCS_TRADER_DESK__UPDATE_LATEST_PRICES__H + + +#include +#include + + +/** \file + * + * Declaration of the \c Update_Latest_Prices class. */ + + +namespace DMBCS::Trader_Desk { + + + /** Function object which reads a list of companies in a market from the + * database, fetches the latest prices for those companies from the + * Yahoo! service, and then writes the new information back to the + * database and gives them back to the calling application. + * + * Similarities with the \c Update_Closing_Prices class are actually + * quite superficial, but we do inherit that class mainly to take + * advantage of the functionality for the initial fetch of the company + * list from the database. */ + + namespace Update_Latest_Prices + { + /** Vessel to carry the information that comes back from the data + * service. */ + struct Data { + /** Our database sequence ID. */ + int company_seqid; + + /** The time at which the price holds. */ + Time_Point time; + + /** The current price of this commodity. */ + double price; + }; + + + using Company = Update_Closing_Prices::Company; + + struct Work : Update_Closing_Prices::Work {}; + + /** Update the database with the new \a data. */ + void sql_injector (DB&, const Data&); + + + /** Get the companies for the registered market, fetch their latest + * data from the Yahoo! server, and pass the results, one at a time, + * to \a injector. */ + void do_update (Work *const work, + DB&, + Preferences&, + function injector); + + + void do_update (DB&, Chart_Data&, Preferences&); + + +} } /* End of namespace DMBCS::Trader_Desk::Update_Latest_Prices. */ + + +#endif /* Undefined DMBCS_TRADER_DESK__UPDATE_LATEST_PRICES__H. */ diff --git a/trader-desk/wizard.cc b/trader-desk/wizard.cc new file mode 100644 index 0000000..887a681 --- /dev/null +++ b/trader-desk/wizard.cc @@ -0,0 +1,989 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#include +#include +#include +#include +#include +#include +#include + + +namespace DMBCS::Trader_Desk { + + +Database_Prefs& Database_Prefs::operator= (const Prefs& P) + { + prefs = P; + host .set_text (P.database_host); + socket .set_text (P.database_socket); + port .set_text (to_string (P.database_port)); + database .set_text (P.database_instance); + user .set_text (P.database_user); + password .set_text (P.database_password); + submit.set_sensitive (0); + return *this; + } + + + +Prefs Database_Prefs::update_prefs (Prefs P) + { + P.database_host = host.get_text (); + P.database_socket = socket.get_text (); + P.database_port = atoi (port.get_text ().c_str ()); + P.database_instance = database.get_text (); + P.database_user = user.get_text (); + P.database_password = password.get_text (); + return P; + } + + + + static void entry_ (Database_Prefs& P, Gtk::Entry& E, + const string& label, const int row) + { + P.attach (*Gtk::manage (new Gtk::Label {label + ": "}), 0, row); + E.set_width_chars (30); + E.signal_changed ().connect ([&P] {P.submit.set_sensitive ();}); + E.signal_activate ().connect ([&P] {P.submit.clicked ();}); + P.attach (E, 1, row); + } + +Database_Prefs::Database_Prefs () + { + const auto entry + { [this] (Gtk::Entry& E, const string& L, const int R) + { return entry_ (*this, E, L, R); } }; + + entry (host, t_("Label", "Host"), 0); + entry (socket, t_("Label", "Socket"), 1); + + attach (*Gtk::make_managed + (t_("Label", "Port") + string {": "}), + 0, 2); + port.signal_changed ().connect ([this] {submit.set_sensitive ();}); + port.signal_activate ().connect ([this] {submit.clicked ();}); + attach (port, 1, 2); + + entry (database, t_("Label", "Database"), 3); + + entry (user, t_("Label", "User"), 4); + user.signal_changed ().connect ([this] { prefs.blank_password_forced = 0; + password.set_sensitive (); }); + + entry (password, t_("Label", "Password"), 5); + + auto *const B {Gtk::make_managed ()}; + attach (*B, 0, 6, 2, 1); + B->pack_start (submit, Gtk::PACK_EXPAND_PADDING); + submit.set_sensitive (0); + show_all (); + } + + + +static tuple, Gtk::TextBuffer*, Gtk::HBox*> + make_panel (Gtk::VBox& V, const string& nature) + { + auto H {make_unique (0, 15)}; + V.pack_start (*H, Gtk::PACK_SHRINK); + H->pack_start (*Gtk::make_managed ("dialog-" + nature, + Gtk::ICON_SIZE_DIALOG), + Gtk::PACK_SHRINK); + auto *const V2 {Gtk::make_managed (0, 15)}; + H->pack_start (*V2); + auto *const T {Gtk::make_managed ()}; + V2->pack_start (*T, Gtk::PACK_SHRINK); + T->set_wrap_mode (Gtk::WRAP_WORD); + T->override_background_color (Gdk::RGBA {"rgba(0,0,0,0)"}); + auto *const H2 {Gtk::make_managed ()}; + V2->pack_start (*H2, Gtk::PACK_SHRINK); + return {move (H), T->get_buffer ().get (), H2}; + } + + + +static void make_configuration_page (Wizard& W) + { + W.configuration_page.set_spacing (65); + auto V {Gtk::make_managed (0, 15)}; + W.configuration_page.pack_start (*V, Gtk::PACK_SHRINK); + V->pack_start (*Gtk::make_managed + (t_("Label", "Using config file") + string {":"}), + Gtk::PACK_SHRINK); + V->pack_start (W.config_file_entry, Gtk::PACK_SHRINK); + W.config_file_entry.set_width_chars (30); + auto *const H {Gtk::make_managed (0, 15)}; + V->pack_start (*H, Gtk::PACK_SHRINK); + H->pack_start (W.use_default_config_button, Gtk::PACK_EXPAND_PADDING); + H->pack_start (W.find_file_button, Gtk::PACK_EXPAND_PADDING); + H->pack_start (W.change_button, Gtk::PACK_EXPAND_PADDING); + + W.configuration_page.show_all (); + + {auto [P, T, H] {make_panel (W.configuration_page, "warning")}; + W.create_file_panel = move (P); + T->set_text (gettext ("This file does not exist, shall I create it?")); + H->pack_start (W.create_file_button, Gtk::PACK_EXPAND_PADDING); + } + + {auto [P, T, H] {make_panel (W.configuration_page, "warning")}; + W.non_default_panel = move (P); + T->set_text (gettext ("This is not the default config file path. " + "This means that you will either have to copy " + "the file to the default location, or use the " + "-c option to specify the file whenever you " + "run the trader-desk application.")); + H->pack_start (W.acknowledge_check, Gtk::PACK_SHRINK); + } + + W.config_file_entry.signal_changed () + .connect ([&W] { W.change_button.set_sensitive (); }); + W.config_file_entry.signal_activate () + .connect ([&W] { W.change_button.clicked (); }); + } + + + +template +static void connect_clicked + (sigc::connection& C, Gtk::Button& B, Callback F) + { + C.disconnect (); + C = sigc::connection {B.signal_clicked ().connect (F)}; + } + + + + static void check_config_file + (Wizard& W, const string& file_name, bool not_default); + +static void config_file_chooser (Wizard& W) + { + Gtk::FileChooserDialog D {W, + t_("Label", "trader-desk config file"), + Gtk::FILE_CHOOSER_ACTION_SAVE}; + D.add_button (t_("Label", "Open"), Gtk::RESPONSE_ACCEPT); + D.set_default_response (Gtk::RESPONSE_ACCEPT); + D.signal_response () + .connect ([&W, &D] (const int response) + { D.hide (); + if (response == Gtk::RESPONSE_ACCEPT) + check_config_file (W, D.get_filename (), 0); }); + D.run (); + } + + + +static void check_config_file + (Wizard& W, const string& file_name, bool not_default = 0) + { + W.config_file_entry.set_text (file_name); + W.config_file_entry + .set_width_chars (max (W.config_file_entry.get_width_chars (), + file_name.length ())); + W.use_default_config_button + .set_sensitive (file_name != Preferences::default_file_name ()); + connect_clicked + (W.udcb_i, + W.use_default_config_button, + [&W, not_default] + { check_config_file + (W, Preferences::default_file_name (), not_default); }); + connect_clicked (W.ffb_i, + W.find_file_button, + [&W] { config_file_chooser (W); }); + W.change_button.set_sensitive (0); + connect_clicked + (W.cb_i, + W.change_button, + [&W, not_default] + { check_config_file + (W, W.config_file_entry.get_text (), not_default); }); + ifstream i {file_name}; + const bool file_exists {i.good ()}; + if (file_exists) + { + i.close (); + W.database_prefs.prefs = Prefs {Preferences::from_file (file_name)}; + W.create_file_panel->hide (); + } + else + { + W.create_file_panel->show_all (); + connect_clicked + (W.cfb_i, + W.create_file_button, + [&W, &E = W.config_file_entry, not_default] + { Preferences::from_file (E.get_text ()); + check_config_file (W, E.get_text (), not_default);}); + } + + if (file_name != Preferences::default_file_name ()) + { + ((Gtk::Image*) ((Gtk::Container*) W.non_default_panel.get ()) + ->get_children ().front ()) + ->set_from_icon_name (string {"dialog-"} + + (not_default ? "information" : "warning"), + Gtk::ICON_SIZE_DIALOG); + W.non_default_panel->show_all (); + W.acknowledge_check.set_active (not_default); + connect_clicked + (W.ac_i, W.acknowledge_check, + [&W] { check_config_file (W, + W.config_file_entry.get_text (), + W.acknowledge_check.get_active ()); }); + } + else + W.non_default_panel->hide (); + + const bool done + {file_exists && (not_default + || file_name == Preferences::default_file_name ())}; + W.set_page_complete (W.configuration_page, done); + if (W.force && done) W.next_page (); + } + + + +static unique_ptr absolute_message (Gtk::VBox& page, + const string& message) + { + auto [P, T, H] {make_panel (page, "warning")}; + T->set_text (message); + return move (P); + } + + + +static void make_database_page (Wizard& W, Gtk::VBox& page) +{ + page.pack_start (W.database_prefs, Gtk::PACK_SHRINK); + page.show_all (); + + {auto [P, T, H] { make_panel (page, "warning") }; + W.no_password_panel = move (P); + T->set_text (gettext ("No password has been set for this user. You are " + "strongly recommended to set one. Enter and commit " + "a new password above, let me generate a random " + "password for you, or press skip if you are sure " + "you donʼt need one.")); + H->pack_start (W.generate_password_button, Gtk::PACK_EXPAND_PADDING); + H->pack_start (W.use_no_password_button, Gtk::PACK_EXPAND_PADDING); + } + + {auto [P, T, H] { make_panel (page, "information") }; + W.blank_password_panel = move (P); + T->set_text (gettext ("No database password is being used for this " + "user.")); + H->pack_start (W.blank_password_ack_button, Gtk::PACK_SHRINK); + } + + W.cannot_access_panel_1 + = absolute_message + (page, + gettext ("Cannot access the database with this account. Please " + "correct the user and password fields above. You may also " + "insert the root password, if you know it, and I will try " + "to help sort the problem out; you will need to do this if " + "you are trying to create a new user.")); + + W.cannot_access_panel_2 + = absolute_message + (page, + gettext ("Cannot access the database with this account. Please " + "correct the user and password fields above.")); + + + {auto [P, text, buttons] {make_panel (W.database_access_page, "warning")}; + W.privilege_panel = move (P); + text->set_text (gettext ("This user does not have access to this database " + "instance. Either modify the settings above or " + "provide the root password in the box below")); + buttons->pack_start (*Gtk::make_managed + (t_("Label", "Database root password: "))); + W.database_root_entry.set_width_chars (30); + buttons->pack_start (W.database_root_entry, Gtk::PACK_SHRINK); + } + + W.database_root_entry + .signal_activate () + .connect ([&W] + { W.database_prefs.prefs.root_password + = W.database_root_entry.get_text (); + W.get_database_connection (W.database_prefs.prefs);}); + + + {auto [P, T, H] {make_panel (W.database_access_page, "warning")}; + W.create_database_panel = move (P); + T->set_text (gettext ("This database instance does not exist. Either " + "edit the entry above or select the create " + "button.")); + H->pack_start (W.create_database_button, Gtk::PACK_EXPAND_PADDING); + } + + + {auto [P, T, H] { make_panel (W.database_access_page, "warning") }; + W.create_password_panel = move (P); + T->set_text (gettext ("The user password is not set in the database. " + "Do you want me to do that for you now?")); + H->pack_start (W.blank_password_button, Gtk::PACK_EXPAND_PADDING); + H->pack_start (W.create_password_button, Gtk::PACK_EXPAND_PADDING); + } + + + W.port_problem_panel + = absolute_message (page, + gettext ("Cannot contact a database on that PORT.")); + + W.socket_problem_panel + = absolute_message + (page, gettext ("Cannot contact a local database on that SOCKET.")); + + W.host_problem_panel + = absolute_message + (page, gettext ("The database host you specified cannot be reached.")); +} + + + +static string slurp_file (ifstream&& input) + { + input.seekg (0, ios::end); + string line (input.tellg (), char {}); + input.seekg (0, ios::beg); + input.read (line.data (), line.length ()); + return line; + } + + + +static void prepare_data_page (Wizard&); + +static gboolean progress_ticker (gpointer W_) + { + Wizard& W {*(Wizard*) W_}; + /* !! Thread local. */ + static int old_progress = 0; + if (W.progress_total < 0) + { + prepare_data_page (W); + return 0; + } + if (W.progress_count != old_progress) + { + old_progress = W.progress_count; + if (W.progress_total == 0) + W.progress_bar.pulse (); + else + W.progress_bar.set_fraction + (W.progress_count / (double) W.progress_total); + } + return 1; + } + + + +static void prepare_data_page (Wizard& W) + { + for (auto *const i : W.data_page.get_children ()) + W.data_page.remove (*i); + + DB db {W.database_prefs.prefs}; + if (db.scalar_result (0, "select count(*) from company") != 0) + { + if (W.force) { W.close (); /* next_page (); */ return; } + W.set_page_complete (W.data_page); + W.data_page.pack_start (*W.database_ready_panel, Gtk::PACK_SHRINK); + W.data_page.show_all (); + return; + } + + ifstream I {PKGDATADIR "/data.sql.xz"}; + if (! I.good ()) + { + if (W.force) { W.close (); /* next_page (); */ return; } + W.set_page_complete (W.data_page); + W.data_page.pack_start (*W.no_data_panel, Gtk::PACK_SHRINK); + W.data_page.show_all (); + return; + } + + + W.data_page.pack_start + (*Gtk::make_managed + (pgettext ("Label", "Pre-populating database")), + Gtk::PACK_SHRINK); + W.data_page.pack_start (W.progress_bar, Gtk::PACK_SHRINK); + W.show_all (); + + gdk_threads_add_timeout (250, progress_ticker, &W); + + std::thread + { + [&W] + { + DB db {W.database_prefs.prefs}; + /* !! Would prefer to call out to xz library. */ + system ("cat " PKGDATADIR "/data.sql.xz " + "| xz -d " + "> /tmp/trader-desk--data.sql"); + const string sql + {slurp_file (ifstream {"/tmp/trader-desk--data.sql"})}; + unlink ("/tmp/trader-desk--data.sql"); + W.progress_total = 0; + W.progress_count = 0; + for (size_t a {0}; ; ) + { + const size_t b {sql.find ("INSERT", a)}; + if (b == sql.npos) break; + ++W.progress_count; + a = b + 1; + } + W.progress_total = W.progress_count.load (); + W.progress_count = 0; + for (size_t a {0}; ; ) + { + const size_t b {sql.find ("INSERT", a)}; + if (b == sql.npos) break; + const size_t c {sql.find (";", b)}; + db.instruction (sql.substr (b, c-b)); + ++W.progress_count; + a = b + 1; + } + W.progress_total = -1; + } + } . detach (); + } + + + +static bool do_tables (Wizard& W, const bool build) +try + { + Prefs& P {W.database_prefs.prefs}; + DB db {P}; + + if (1 == db.scalar_result (0, "select version from global")) + return 1; + + if (! build) return 0; + + db.instruction ("create table global " + "(version int, " + "last_markets_update datetime default 0)"); + + db.instruction ("insert into global " + "set version=1"); + + db.instruction ("create table company " + "(seqid int(6) primary key auto_increment, " + "name varchar(50) not null, " + "symbol varchar(6) not null, " + "market int(6) not null default 0, " + "last_price float, " + "last_price_date datetime, " + "last_close_date date not null " + "default '0000-00-00')"); + + db.instruction ("create table prices " + "(date date, " + "company int(6), " + "open float, high float, low float, " + "close float, " + "volume int(11), adjusted_close float)"); + + db.instruction ("alter table prices " + "add primary key (date, company)"); + + db.instruction ("create table market " + "(seqid int(6) primary key auto_increment, " + "symbol varchar(6), " + "name varchar(36), " + "component_extension varchar(6), " + "last_update datetime default 0, " + "tracked bool default 0, " + "close_time time)"); + + db.instruction ("create table alphavantage_ticks " + "(time int(11) primary key)"); + + return 1; +} +catch (exception&) { return 0; } + + + +static void prepare_table_page (Wizard& W) + { + do_tables (W, 1); + W.set_page_complete (W.tables_page); + if (W.force) { W.next_page (); return; } + Gtk::TextView *const T {Gtk::make_managed ()}; + W.tables_page.add (*T); + T->get_buffer ()->set_text ("The database tables are in place."); + W.tables_page.show_all (); + } + + + +int Wizard::page_order (Gtk::Widget *const a, Gtk::Widget *const b) + { + if (a == 0 || b == 0 || a == b) return 0; + if (a == (Gtk::Widget*)&configuration_page) + return -1; + if (a == (Gtk::Widget*)&database_access_page) + return b == (Gtk::Widget*)&configuration_page ? 1 : -1; + if (a == (Gtk::Widget*)&tables_page) + return b == (Gtk::Widget*)&data_page ? -1 : 1; + return 1; + } + + + +static void make_data_page (Wizard& W, Gtk::VBox& page) +{ + {auto [P, T, H] { make_panel (page, "information") }; + W.no_data_panel = move (P); + T->set_text (gettext ("There are no data available to pre-populate " + "the database. You will be invited to import " + "data from the Internet.")); + } + + {auto [P, T, H] { make_panel (page, "information") }; + W.database_ready_panel = move (P); + T->set_text (gettext ("The database is populated with some data.")); + } +} + + + +Wizard::Wizard (const string& config_file_name, const bool f) : force {f} + { + const auto add_page + { [&W=*this] (Gtk::VBox& V, const string& title) + { W.append_page (V); + W.set_page_type (V, Gtk::ASSISTANT_PAGE_CONTENT); + W.set_page_title (V, title); + V.set_spacing (40); } }; + + add_page (configuration_page, t_("Label", "Config file")); + make_configuration_page (*this); + + add_page (database_access_page, t_("Label", "Database access")); + make_database_page (*this, database_access_page); + + add_page (tables_page, t_("Label", "Tables")); + + add_page (data_page, t_("Label", "Historical data")); + make_data_page (*this, data_page); + set_page_type (data_page, Gtk::ASSISTANT_PAGE_CONFIRM); + + database_prefs.submit + .signal_clicked () + .connect ([this] + { database_prefs.update_prefs (); + get_database_connection + (database_prefs.prefs); }); + + signal_close () + .connect ([this] { signal_wizard_complete.emit (database_prefs.prefs); + hide (); }); + + signal_cancel ().connect ([this] {hide ();}); + show_all (); + set_icon_from_file (PKGDATADIR "/trader-desk.png"); + signal_prepare () + .connect ([this] (Gtk::Widget *const page) + { if (page_order (last_page, page) > 0) force = 0; + last_page = page; + if (page == (Gtk::Widget*) &configuration_page) + check_config_file (*this, + database_prefs.prefs.file_path); + else if (page == (Gtk::Widget*) &database_access_page) + get_database_connection (database_prefs.prefs); + else if (page == (Gtk::Widget*) &tables_page) + prepare_table_page (*this); + else if (page == (Gtk::Widget*) &data_page) + prepare_data_page (*this);}); + check_config_file (*this, config_file_name); + } + + + +static bool host_problem (Wizard& W, const string& error) + { + /* !!!! This probably wouldn't work if the machine is using a + * language other than English? */ + static const regex unknown_host_RE {".*unknown.*server host.*", + regex::icase}; + if (! regex_match (error, unknown_host_RE)) return 0; + W.host_problem_panel->show_all (); + return 1; + } + + + +static bool socket_problem (Wizard& W, const string& error) + { + static const regex bad_socket_RE + {".*can.t.*connect.*local.*server.*socket.*", + regex::icase}; + if (! regex_match (error, bad_socket_RE)) return 0; + W.socket_problem_panel->show_all (); + return 1; + } + + +static bool port_problem (Wizard& W, const string& error) + { + static const regex bad_port_RE {".*can.t.*connect.*server.*", + regex::icase}; + if (! regex_match (error, bad_port_RE)) return 0; + W.port_problem_panel->show_all (); + return 1; + } + + + + static void do_set_password_on_database (Database_Prefs& P) + { + string old_password {}; + swap (old_password, P.prefs.database_password); + DB {P.prefs} + .instruction ("set password=password('" + old_password + "')"); + swap (old_password, P.prefs.database_password); + } + +static bool set_password_on_database (Wizard& W, Database_Prefs& P) + { + if (W.force) { do_set_password_on_database (P); + return 1; } + + W.create_password_panel->show_all (); + connect_clicked (W.bpb_i, W.blank_password_button, + [&W, &P] { P.prefs.database_password = ""; + W.get_database_connection (P.prefs); }); + connect_clicked (W.cpb_i, W.create_password_button, + [&W, &P] + { do_set_password_on_database (P); + W.get_database_connection (P.prefs); }); + P.password.set_sensitive (1); + return 0; + } + + + +static tuple > get_root_access (Prefs P) + { + Prefs root_P {P}; + root_P.database_user = "root"; + root_P.database_instance = ""; + + try { if (P.root_password.length ()) + { + root_P.database_password = P.root_password; + return {move (P), make_unique (root_P)}; + } } + catch (exception&) {} + + root_P.database_password = P.database_password; + + try { P.root_password = P.database_password; + P.database_password = {}; + return {move (P), make_unique (root_P)}; } + catch (exception&) {} + + root_P.database_password = {}; + + try { P.root_password = {}; + return {move (P), make_unique (root_P)}; } + catch (exception& E) { cerr << "XXX: " << E.what () << "\n"; } + + return {move (P), unique_ptr {}}; + } + + + +static void create_database_as_user (Database_Prefs& P) + { + Preferences prefs {P.prefs}; + prefs.database_instance = ""; + DB db {prefs}; + auto I {db.instruction ()}; + I << "create database " << P.prefs.database_instance + << " character set 'utf8'"; + I.execute (); + } + + + +static void create_database_as_root (Database_Prefs& P) + { + Prefs prefs {P.prefs}; + prefs.database_instance = ""; + auto [p, root_db] {get_root_access (prefs)}; + P.prefs.root_password = p.root_password; + if (! root_db) create_database_as_user (P); + root_db->instruction ("create database " + P.prefs.database_instance + + " character set 'utf8'"); + } + + + +static bool database_instance_problem (Wizard& W, Database_Prefs& P) +{ + Prefs prefs {P.prefs}; + prefs.database_instance = ""; + auto [p, root_db] {get_root_access (prefs)}; + prefs.root_password = p.root_password; + + if (root_db) + { + P.prefs.root_password = prefs.root_password; + if (root_db->scalar_result (0, + "select count(*) " + "from information_schema.schemata " + "where schema_name='%s'", + P.prefs.database_instance.data ())) + return 1; + if (W.force) { create_database_as_root (P); return 1; } + W.create_database_panel->show_all (); + connect_clicked (W.cdb_i, W.create_database_button, + [&W, &P] { create_database_as_root (P); }); + return 0; + } + + try + { + DB user_db {prefs}; + if (user_db.scalar_result (0, + "select count(*) " + "from information_schema.schemata " + "where schema_name='%s'", + P.prefs.database_instance.data ())) + return 1; + if (W.force) + { + try { create_database_as_user (P); return 1; } + catch (exception&) + { + W.privilege_panel->show_all (); + return 0; + } + } + W.create_database_panel->show_all (); + connect_clicked (W.cdb_i, W.create_database_button, + [&W, &P] { create_database_as_user (P); }); + } + catch (exception& E) {} + + return 0; +} + + + +inline void grant_privileges + (DB& root_db, const string& database, const string& user) + { root_db.instruction ("grant all on " + database + ".* to " + user); } + +static void grant_privileges (DB& root_db, Wizard& W) + { + const auto& P {W.database_prefs.prefs}; + grant_privileges (root_db, P.database_instance, P.database_user); + } + +static void grant_privileges (Wizard& W) + { + auto [prefs, root_db] {get_root_access (W.database_prefs.prefs)}; + if (root_db) { W.privilege_panel->hide (); + return grant_privileges (*root_db, W); } + W.privilege_panel->show_all (); + } + + + +static bool no_user_access (Wizard& W, const string& error) +{ + static const regex error_re {".*access denied for user.*", regex::icase}; + + if (! regex_match (error, error_re)) return 1; + + {Prefs p {W.database_prefs.prefs}; + p.database_instance = ""; + try { DB {p}; grant_privileges (W); return 1; } + catch (exception&) {} + } + + auto [p, root_db] {get_root_access (W.database_prefs.prefs)}; + W.database_prefs = p; + + if (! root_db) + { + W.cannot_access_panel_1->show_all (); + W.database_prefs.password.set_sensitive (1); + return 0; + } + + if (root_db->scalar_result + (0, + "select count(*) from mysql.user where User='%s'", + W.database_prefs.prefs.database_user)) + { + W.cannot_access_panel_2->show_all (); + W.database_prefs.password.set_sensitive (1); + return 0; + } + + try {auto I {root_db->instruction ()}; + I << "create user '" + << W.database_prefs.prefs.database_user + << "'"; + if (W.database_prefs.prefs.database_password.length ()) + I << " identified by '" + << W.database_prefs.prefs.database_password << "'"; + I.execute (); + } + /* Putting in user names containing a hypen produces errors, + * but the user is correctly constructed nonetheless. */ + catch (exception&) {} + + grant_privileges (*root_db, W); + + return 1; +} + + + + static void generate_random_password (Database_Prefs& P) + { + static mt19937 E; + static uniform_int_distribution I {'a', 'z'}; + string new_password; + for (int i = 0; i < 20; ++i) new_password += I (E); + Preferences temp_P {P.prefs}; + temp_P.database_instance = ""; + DB {temp_P} + .instruction ("set password=password('" + new_password + "')"); + P.prefs.database_password = new_password; + } + +static bool blank_password_problem (Wizard& W, Database_Prefs& P) +{ + bool complete {0}; + if (! P.prefs.blank_password_forced) + { + if (W.force) { generate_random_password (P); + return 1; } + W.no_password_panel->show_all (); + connect_clicked (W.gpb_i, W.generate_password_button, + [&W, &P] + { generate_random_password (P); + W.get_database_connection (P.prefs); }); + connect_clicked (W.unpb_i, W.use_no_password_button, + [&W, &P] + { P.prefs.blank_password_forced = 1; + W.get_database_connection (P.prefs); }); + } + + else /* Blank password forced. */ + { + W.blank_password_panel->show_all (); + W.blank_password_ack_button.set_active (); + W.bpab_i.disconnect (); + W.bpab_i = W.blank_password_ack_button + .signal_toggled ().connect ([&W, &P] + { P.prefs.blank_password_forced = 0; + W.get_database_connection (P.prefs); }); + complete = 1; + } + + P.password.set_sensitive (1); + return complete; +} + + + +void Wizard::get_database_connection (Prefs P) + { + no_password_panel->hide (); + blank_password_panel->hide (); + cannot_access_panel_1->hide (); + cannot_access_panel_2->hide (); + privilege_panel->hide (); + create_database_panel->hide (); + create_password_panel->hide (); + port_problem_panel->hide (); + socket_problem_panel->hide (); + host_problem_panel->hide (); + + database_prefs = P; + database_prefs.password.set_sensitive (0); + database_prefs.submit.set_sensitive (0); + set_page_complete (database_access_page, 0); + bool complete {1}; + + try { DB db {P}; } + catch (exception& E) + { + cerr << "Database response: " << E.what () << "\n"; + + if (host_problem (*this, E.what ()) + || socket_problem (*this, E.what ()) + || port_problem (*this, E.what ())) + return; + + if (P.database_password.empty ()) + complete = no_user_access (*this, E.what ()) && complete; + + else + { + string original_password {""}; + swap (P.database_password, original_password); + + try { { DB db {P}; } + swap (P.database_password, original_password); + complete + = set_password_on_database (*this, database_prefs) + && complete; } + catch (exception& E) + { + swap (P.database_password, original_password); + complete = no_user_access (*this, E.what ()) && complete; + } + } + } + + + if (database_prefs.prefs.database_password.empty () && complete) + complete = blank_password_problem (*this, database_prefs); + + complete = database_instance_problem (*this, database_prefs) + && complete; + + set_page_complete (database_access_page, complete); + if (force && complete) next_page (); + } + + + +} /* End of namespace DMBCS::Trader_Desk. */ diff --git a/trader-desk/wizard.h b/trader-desk/wizard.h new file mode 100644 index 0000000..5ee2274 --- /dev/null +++ b/trader-desk/wizard.h @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2017, 2020 Dale Mellor + * + * This file is part of the trader-desk package. + * + * The trader-desk package is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * The trader-desk package is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +#ifndef DMBCS__TRADER_DESK__WIZARD__H +#define DMBCS__TRADER_DESK__WIZARD__H + + +#include +#include +#include +#include + + +namespace DMBCS::Trader_Desk { + + + +#define t_ pgettext + + + +struct Prefs : Preferences { bool non_default_config_file {0}; + bool blank_password_forced {0}; + string root_password {}; }; + + + +struct Database_Prefs : Gtk::Grid + { + Prefs prefs; + + Gtk::Entry host; + Gtk::Entry socket; + Gtk::Entry port; + Gtk::Entry database; + Gtk::Entry user; + Gtk::Entry password; + Gtk::Button submit {t_("Label", "Commit change")}; + + Database_Prefs (); + Database_Prefs& operator= (const Prefs& P); + Prefs update_prefs (Prefs P); + Prefs update_prefs () { return prefs = update_prefs (prefs); } + }; + + + +struct Wizard : Gtk::Assistant +{ + /* The actual Preferences which we are checking/generating are held + * inside the database_prefs object below. */ + + bool force; + Gtk::Widget* last_page {0}; + + Gtk::VBox configuration_page; + Gtk::Entry config_file_entry; + Gtk::Button use_default_config_button {t_("Label", "Use _default"), 1}; + sigc::connection udcb_i; + Gtk::Button find_file_button {t_("Label", "_Find file"), 1}; + sigc::connection ffb_i; + Gtk::Button change_button {t_("Label", "Change")}; + sigc::connection cb_i; + unique_ptr create_file_panel; + Gtk::Button create_file_button {t_("Label", "Create file")}; + sigc::connection cfb_i; + unique_ptr non_default_panel; + Gtk::CheckButton acknowledge_check {t_("Label", "_Acknowledge"), 1}; + sigc::connection ac_i; + + + Gtk::VBox database_access_page; + Database_Prefs database_prefs; + unique_ptr no_password_panel; + Gtk::Button generate_password_button + {t_("Label", "_Generate password"), 1}; + sigc::connection gpb_i; + Gtk::Button use_no_password_button + {t_("Label", "_Use no password"), 1}; + sigc::connection unpb_i; + unique_ptr blank_password_panel; + Gtk::CheckButton blank_password_ack_button + {t_("Label", "_Acknowledged"), 1}; + sigc::connection bpab_i; + unique_ptr cannot_access_panel_1; + unique_ptr cannot_access_panel_2; + unique_ptr privilege_panel; + Gtk::Entry database_root_entry; + unique_ptr create_database_panel; + Gtk::Button create_database_button + {t_("Label", "Create database"), 1}; + sigc::connection cdb_i; + unique_ptr create_password_panel; + Gtk::Button create_password_button + {t_("Label", "Yes, set password")}; + sigc::connection cpb_i; + Gtk::Button blank_password_button + {t_("Label", "No, use blank password")}; + sigc::connection bpb_i; + unique_ptr port_problem_panel; + unique_ptr socket_problem_panel; + unique_ptr host_problem_panel; + + Gtk::VBox tables_page; + + Gtk::VBox data_page; + Gtk::ProgressBar progress_bar; + atomic progress_total; + atomic progress_count; + unique_ptr database_ready_panel; + unique_ptr no_data_panel; + + + + int page_order (Gtk::Widget *const, Gtk::Widget *const); + + sigc::signal signal_wizard_complete; + + Wizard (const string& config_file_name, const bool force); + + void get_database_connection (Prefs); + }; + + +} /* End of namespace DMBCS::Trader_Desk. */ + + +#endif /* Undefined DMBCS__TRADER_DESK__WIZARD__H. */