summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid <171410+dmb2@users.noreply.github.com>2023-05-27 08:59:14 -0400
committerGitHub <noreply@github.com>2023-05-27 08:59:14 -0400
commit7f8b48106ea2399ed679b83036602e8e0f13b62c (patch)
tree44c581b41e4e99c500199b5d2877f87a6b0772a4
parent36daccd715e1cc6c1badab7cd87e34a8514f3b6b (diff)
parent5d62f0ec703d6e6c439fd0dfad7775f69378a247 (diff)
Merge pull request #271 from santiagopim/ticker-new-module
Ticker new module
-rw-r--r--README.org1
-rw-r--r--modeline/bitcoin/README.org2
-rw-r--r--modeline/ticker/README.org142
-rw-r--r--modeline/ticker/package.lisp6
-rw-r--r--modeline/ticker/screenshot.pngbin0 -> 124164 bytes
-rw-r--r--modeline/ticker/ticker.asd13
-rw-r--r--modeline/ticker/ticker.lisp231
7 files changed, 395 insertions, 0 deletions
diff --git a/README.org b/README.org
index 65df5e9..bdabd13 100644
--- a/README.org
+++ b/README.org
@@ -98,6 +98,7 @@ Advertise your module here, open a PR and include a org-mode link!
- [[./modeline/mem/README.org][mem]] :: Display memory in the modeline, %M conflicts with maildir.
- [[./modeline/net/README.org][net]] :: Displays information about the current network connection.
- [[./modeline/stumptray/README.org][stumptray]] :: System Tray for stumpwm.
+- [[./modeline/ticker/README.org][ticker]] :: Display ticker price on StumpWM modeline.
- [[./modeline/wifi/README.org][wifi]] :: Display information about your wifi.
** Utilities
- [[./util/alert-me/README.org][alert-me]] :: Alert me that an event is coming
diff --git a/modeline/bitcoin/README.org b/modeline/bitcoin/README.org
index e1625fd..7d0d386 100644
--- a/modeline/bitcoin/README.org
+++ b/modeline/bitcoin/README.org
@@ -1,5 +1,7 @@
* Bitcoin
+*THIS MODULE IS DEPRECATED, AND SUPERSEDED BY THE* =ticker= *MODULE*.
+
Show Bitcoin (₿) value in the modeline.
** Usage
diff --git a/modeline/ticker/README.org b/modeline/ticker/README.org
new file mode 100644
index 0000000..e8cb387
--- /dev/null
+++ b/modeline/ticker/README.org
@@ -0,0 +1,142 @@
+* Ticker
+
+This module prints off values from stocks. It is developed with
+cryptocurrencies in mind, so the default API target is the [[https://kraken.com/][Kraken]]
+servers.
+
+[[./screenshot.png]]
+
+** Dependencies
+
+It gets actual price through API, so needs =dexador= and =yason=.
+Also, the price getter is asynchronous with the =lparallel= machinery.
+
+#+begin_src lisp
+ (ql:quickload '("dexador" "yason" "lparallel"))
+#+end_src
+
+** Usage
+
+Place the following in your =~/.stumpwmrc= file:
+
+#+begin_src lisp
+ (load-module "ticker")
+#+end_src
+
+Use =%T= in your mode line format, for example:
+
+#+begin_src lisp
+ (setf *screen-mode-line-format*
+ (list "[%n]" ; Groups
+ "%v" ; Windows
+ "^>" ; Push right
+ " | %T" ; Ticker <<<--- this module
+ " | %d")) ; Clock
+#+end_src
+
+And define some tickers with its parameters. This line defines one
+ticker that defaults to the Bitcoin/USD pair:
+
+#+begin_src lisp
+ (ticker:define-ticker) ; Bitcoin as default
+#+end_src
+
+You can define more tickers and parameterize as desired, see the [[Notes]]:
+
+#+begin_src lisp
+ (ticker:define-ticker
+ :symbol "XBT" ;;"₿" ; Bitcoin
+ :threshold 0.01)
+ (ticker:define-ticker
+ :pair "XETHZUSD" ; Ethereum
+ :symbol "ETH")
+ (ticker:define-ticker
+ :pair "ADAUSD" ; Cardano
+ :symbol "ADA" ;;"₳"
+ :threshold 0.0001
+ :delay 60
+ :decimals 3
+ :gauge-width 9)
+#+end_src
+
+** Notes
+
+The parameters that can be customized when defining a ticker and its
+default values are:
+
+#+begin_src lisp
+ :pair "XXBTZUSD" ; pair to get from API
+ :symbol "BTC" ; label the ticker
+ :colors t ; use colors
+ :threshold 0.001 ; 0.1% deviation from average to colorize
+ :delay 30 ; seconds between updates
+ :decimals 0 ; number of decimal digits
+ :localization 2 ; formatting number
+ :gauge-width 7 ; width of the gauge bar in characters
+#+end_src
+
+The minimum parameters to define are the =:pair= to get the value from
+the API, and the =:symbol= to label the ticker in the modeline. The
+=:pair= is one of the listed at:
+
++ [[https://api.kraken.com/0/public/Ticker]]
+
+The =:symbol= is a string and can be blank.
+
+Price format is colorized depending on the =:colors= flag. You can
+customize setting it to =t= or =nil= when defining the ticker.
+
+Colors depends on a comparison between actual value and the last
+values average:
+
+| Color | Code | Description |
+|---------------+---------+-----------------------------------|
+| Bright yellow | =^B^3*= | Price is higher than average |
+| Red | =^1*= | Price is below average |
+| White | =^7*= | Price is similar to average |
+| Default color | =^**= | When *modeline-use-colors* is nil |
+
+There is a threshold around average, so the increasing or decreasing
+color is only applied if =:threshold= is passed.
+
+Last values average is calculated over a 3 hours values list, where
+values are stored on every modeline refresh in a FIFO fashion.
+
+Connection to the API price server is limited by a =:delay= interval,
+in seconds. So connection attempts between interval time are blocked.
+
+The number of decimal places is set by =:decimals=, when =0= there is
+no decimals.
+
+The localization format is set by =:localization= code, when =0= there
+is no thousand separator, gives =1234.56= and the =:decimals=
+parameter does not work, when =1= the thousand separator is =comma=
+and gives =1,234.56=, when =2= the thousand separator is =period= and
+gives =1.234,56=, and when =3= the thousand separator is =space= and
+gives =1 234,56=.
+
+It is possible to add a gauge bar with the tendency of the actual
+value between the low and high in the last 24 hours with
+=:gauge-width=. Value must be greater than =1= to be shown.
+
+There is an external parameter =*tickers-separator*= that defines the
+string to put between tickers, as a separator. Can be customized, but
+be aware not to use tilde "~" or other combinations because it is
+interpreted by the =format= function:
+
+#+begin_src lisp
+ (setf ticker:*tickers-separator* " | ")
+#+end_src
+
+** Issues
+
+Try to use conditions' =handler-case= machinery to avoid the internet
+timeouts or the computer sleeping process, to stuck the modeline.
+
+The =truncate= function is used when formatting the values, so some
+precission loss is expected.
+
+There is an internal function =ticker::reset-tickers= that closes all
+=lparallel= tasks and kernels and resets the =*tickers*= list. Once
+called, if you want to redefine new tickers, should wait up to the
+maximum =delay= interval.
diff --git a/modeline/ticker/package.lisp b/modeline/ticker/package.lisp
new file mode 100644
index 0000000..d777628
--- /dev/null
+++ b/modeline/ticker/package.lisp
@@ -0,0 +1,6 @@
+;;;; package.lisp
+
+(defpackage :ticker
+ (:use :cl)
+ (:export #:define-ticker
+ #:*tickers-separator*))
diff --git a/modeline/ticker/screenshot.png b/modeline/ticker/screenshot.png
new file mode 100644
index 0000000..e88be3c
--- /dev/null
+++ b/modeline/ticker/screenshot.png
Binary files differ
diff --git a/modeline/ticker/ticker.asd b/modeline/ticker/ticker.asd
new file mode 100644
index 0000000..e44a710
--- /dev/null
+++ b/modeline/ticker/ticker.asd
@@ -0,0 +1,13 @@
+;;;; bitcoin.asd
+
+(asdf:defsystem "ticker"
+ :description "Display ticker price on StumpWM modeline."
+ :author "Santiago Payà Miralta @santiagopim"
+ :license "MIT"
+ :homepage "https://github.com/stumpwm/stumpwm-contrib/"
+ :depends-on ("stumpwm" ; Use add-screen-mode-line-formatter
+ "lparallel" ; Connect to API with concurrency
+ "dexador" ; Get data from url
+ "yason") ; Parse json
+ :components ((:file "package")
+ (:file "ticker" :depends-on ("package"))))
diff --git a/modeline/ticker/ticker.lisp b/modeline/ticker/ticker.lisp
new file mode 100644
index 0000000..4d05ac8
--- /dev/null
+++ b/modeline/ticker/ticker.lisp
@@ -0,0 +1,231 @@
+;;;; ticker.lisp
+
+;;; Ticker formatter for the Stumpwm mode-line. There is no timestamp,
+;;; so let's store up to some historical serie size values got from
+;;; url and calculate its average. Comparing actual value with this
+;;; average, set a color format. Adds a gauge control that draws
+;;; tendency between low and high in 24 hours values.
+
+;;; When creating a new ticker, it launches an asynchronous process
+;;; that reads values every delay time from the API and stores them in
+;;; the structure. The mode-line uses those structures to print the
+;;; values on every refresh.
+
+;;; CODE:
+
+(in-package :ticker)
+
+(defstruct ticker
+ "Parameters of the ticker and state variables."
+ pair ; get from API url
+ symbol ; to show in modeline
+ colors ; show colors
+ threshold ; color change interval
+ delay ; update interval
+ decimals ; digits in decimal part
+ localization ; thousands/comma format
+ gauge-width ; width of gauge in characters
+ ;; Internal state variables
+ (values ()) ; store the last 3 hours values
+ (value 0.0) ; last value got from url
+ (values-low 0.0) ; low value last 24h
+ (values-high 0.0) ; high value last 24h
+ (values-average 0.0)) ; average last 3 hours values
+
+;;; Exported
+
+(defun define-ticker (&key (pair "XXBTZUSD") (symbol "BTC") (colors t)
+ (threshold 0.001) (delay 30) (decimals 0)
+ (localization 2) (gauge-width 7))
+ "Ticker constructor which defaults to Bitcoin and 3 hours historical values."
+ (let ((ticker (make-ticker
+ :pair pair
+ :symbol symbol
+ :colors colors
+ :threshold threshold
+ :delay delay
+ :decimals decimals
+ :localization localization
+ :gauge-width gauge-width
+ ;; Internal state variable
+ :values (make-list (truncate (/ (* 3 60 60) ; 3 hours
+ delay))
+ :initial-element NIL))))
+ ;; Push the `ticker' into the `*tickers*' list, and launch the
+ ;; asynchronous process that will update the values from the API
+ ;; every `delay' seconds.
+ (push ticker *tickers*)
+ (let ((lparallel:*kernel*
+ (lparallel:make-kernel 1 :name pair)))
+ (lparallel:submit-task (lparallel:make-channel)
+ (lambda ()
+ (parallel-getter (car *tickers*)))))))
+
+(defparameter *tickers-separator* " | "
+ "String to separate between tickers in de modeline.")
+
+;;; Global variables
+
+(defparameter *tickers* ()
+ "List of tickers to show.")
+
+(defparameter *url* "https://api.kraken.com/0/public/Ticker?pair="
+ "Location of price provider, the ticker pair will be concatenated.")
+
+(defparameter *stop-parallel-getters* nil
+ "When `t' stop and close all asynchronous loops that get the tickers
+values.")
+
+;;; Get the values
+
+(defun parallel-getter (tick)
+ "The values are stored in the `*tickers*' structure, from where can be
+read by the `ticker-modeline' function."
+ (do ()
+ (*stop-parallel-getters*
+ (lparallel:end-kernel))
+ ;; Store actual, 24h low, and 24h high values from the `*url*' API.
+ ;; If there is no response, store just `nil' values.
+ (let ((values
+ (let* ((url (concatenate 'string *url* (ticker-pair tick)))
+ (response (handler-case
+ (gethash (ticker-pair tick)
+ (gethash "result"
+ (yason:parse
+ (dexador:get url
+ :keep-alive nil))))
+ ;; Return NIL in case some condition is triggered
+ (condition () nil))))
+ (if response
+ (list (read-from-string (first (gethash "c" response)))
+ (read-from-string (second (gethash "l" response)))
+ (read-from-string (second (gethash "h" response))))
+ (list nil nil nil)))))
+ ;; From actual, 24 low, and 24h high, calculate average and
+ ;; store all in the `*tickers*' ticker.
+ (setf (ticker-value tick) (first values)
+ (ticker-values-low tick) (second values)
+ (ticker-values-high tick) (third values))
+ ;; Add value to values list, pushing to front
+ (push (ticker-value tick) (ticker-values tick))
+ ;; Preserve values list size, popping from end
+ (setf (ticker-values tick) (nreverse (ticker-values tick)))
+ (pop (ticker-values tick))
+ (setf (ticker-values tick) (nreverse (ticker-values tick)))
+ ;; Calculate average of values, excluding NIL values
+ ;; that could exist because network issues.
+ (let ((values-clean (remove-if-not #'numberp (ticker-values tick))))
+ (setf (ticker-values-average tick) (/ (reduce #'+ values-clean)
+ (max 1 (length values-clean))))))
+ ;; And again
+ (sleep (ticker-delay tick))))
+
+(defun reset-tickers ()
+ "Stop the getters and reset the list of tickers."
+ (let ((max-delay (reduce 'max
+ *tickers*
+ :key 'ticker-delay
+ :initial-value 0)))
+ (setf *stop-parallel-getters* t)
+ ;; Reset the flag to nil after some time
+ (let ((lparallel:*kernel* (lparallel:make-kernel 1)))
+ (lparallel:submit-task
+ (lparallel:make-channel)
+ (lambda ()
+ (sleep (1+ max-delay))
+ (setf *stop-parallel-getters* nil)
+ (lparallel:end-kernel))))
+ ;; Reset the *tickers* list
+ (setf *tickers* ())))
+
+;;; Write on modeline
+
+(defun format-decimal (n sep int com dec)
+ "Return Number formated in groups of INTerval length every, and
+separated by SEParator, with COMma character as decimal separator.
+DECimals is the number of digits in the decimal part. All parameters
+but N are strings. COMma character should not be the tilde `~'.
+
+Works as a simple formatting positive numbers using directive `~D',
+for thousand separator and direct value displacement in the decimal
+part. Uses `truncate' so there is some precission loss. Does NOT work
+with negative numbers.
+
+Based on https://stackoverflow.com/questions/35012859"
+ (let* ((num-string (concatenate 'string "~,,'" sep "," int ":D"))
+ (decimals (format nil "~D" dec))
+ (dec-string (concatenate 'string com "~" decimals ",'0D")))
+ (multiple-value-bind (i r) (truncate n)
+ (concatenate
+ 'string
+ (format nil num-string i)
+ (when (< 0 dec)
+ (format nil dec-string (truncate (* (expt 10 dec) r))))))))
+
+(defun gauge (v l h n)
+ "Draw a gauge control with Value at the point between Low and High in
+an N length control."
+ (if (and (< l h) (<= l v) (<= v h) (> n 1))
+ (let* ((line (make-sequence 'string n :initial-element #\-))
+ (segment (floor (* n (/ (- v l) (- h l)))))
+ (segment (if (= v h) (1- segment) segment)))
+ (replace line "*" :start1 segment))
+ "-*-*-"))
+
+(defun get-value-string (tick)
+ "Generate the ticker string to show in modeline."
+ (let ((results ()))
+ (when (< 0 (length (ticker-symbol tick)))
+ (push (ticker-symbol tick) results))
+ (push (case (ticker-localization tick)
+ (0 (format nil "~,2F" (ticker-value tick)))
+ (1 (format-decimal (ticker-value tick) "," "3" "."
+ (ticker-decimals tick)))
+ (2 (format-decimal (ticker-value tick) "." "3" ","
+ (ticker-decimals tick)))
+ (3 (format-decimal (ticker-value tick) " " "3" ","
+ (ticker-decimals tick)))
+ (otherwise (format nil "~,2F" (ticker-value tick))))
+ results)
+ (when (< 1 (ticker-gauge-width tick))
+ (push (gauge (ticker-value tick)
+ (ticker-values-low tick)
+ (ticker-values-high tick)
+ (ticker-gauge-width tick))
+ results))
+ (format nil "~{~A~^ ~}" (nreverse results))))
+
+(defun ticker-modeline (ml)
+ "This function is evaluated on every modeline refresh and returns the
+modeline string. The values are always printed off, but only updated
+by the `parallel-getter' function when the `delay' interval has been
+reached. If there are not returned values from the API (nil), then the
+ticker name is printed."
+ (declare (ignore ml))
+ (if *tickers*
+ (let ((results ()))
+ (dolist (tick *tickers*)
+ (if (and (numberp (ticker-value tick)) (plusp (ticker-value tick)))
+ ;; Actual value is a positive number, so print off
+ (let ((value-string (get-value-string tick)))
+ ;; Return with color if desired
+ (push (if (ticker-colors tick)
+ (let* ((diff (- (ticker-value tick) (ticker-values-average tick)))
+ (pdiff (/ diff (max 1 (ticker-value tick)))))
+ (cond ((> pdiff (ticker-threshold tick))
+ (format nil "^[^B^3*~A^]" value-string))
+ ((< pdiff (- (ticker-threshold tick)))
+ (format nil "^[^1*~A^]" value-string))
+ (t (format nil "^[^7*~A^]" value-string))))
+ (format nil "^[^**~A^]" value-string))
+ results))
+ ;; The value is not a positive number, set the tick name as response
+ (push (format nil "-~A-" (ticker-pair tick)) results)))
+ ;; Return aggregated ticks results with proper separator
+ (let ((s (concatenate 'string "~{~A~^" *tickers-separator* "~}")))
+ (format nil s results)))
+ ;; There are no tickers defined
+ "-Ticker-"))
+
+;; Bind modeline formatter character to the drawer function
+(stumpwm:add-screen-mode-line-formatter #\T 'ticker-modeline)