Jelajahi Sumber

[finance] per-chat ticker support

Innocenty Enikeew 8 tahun lalu
induk
melakukan
bd37dc4fbc
1 mengubah file dengan 110 tambahan dan 55 penghapusan
  1. 110 55
      plugins/finance.lisp

+ 110 - 55
plugins/finance.lisp

@@ -6,7 +6,7 @@
 (defparameter +yahoo-url+ "https://query1.finance.yahoo.com/v7/finance/quote?lang=en-US&region=US&corsDomain=finance.yahoo.com&fields=regularMarketPrice" "Yahoo Finance API endpoint")
 (defparameter +yahoo-url+ "https://query1.finance.yahoo.com/v7/finance/quote?lang=en-US&region=US&corsDomain=finance.yahoo.com&fields=regularMarketPrice" "Yahoo Finance API endpoint")
 (defparameter +brent-url+ "http://www.cmegroup.com/CmeWS/mvc/Quotes/Future/424/G")
 (defparameter +brent-url+ "http://www.cmegroup.com/CmeWS/mvc/Quotes/Future/424/G")
 
 
-(defvar *rate-pairs* '("USDRUB=X" "EURRUB=X" "GBPRUB=X"))
+(defvar *default-symbols* '("USDRUB=X" "EURRUB=X" "GBPRUB=X"))
 
 
 (defparameter +chart-ranges+ (list (cons "day" (* 24 60))
 (defparameter +chart-ranges+ (list (cons "day" (* 24 60))
                                    (cons "week" (* 7 24 60))
                                    (cons "week" (* 7 24 60))
@@ -14,15 +14,15 @@
                                    (cons "quarter" (* 91 24 60))
                                    (cons "quarter" (* 91 24 60))
                                    (cons "year" (* 365 24 60))))
                                    (cons "year" (* 365 24 60))))
 
 
-(defun get-rates (&optional (pairs *rate-pairs*))
+(defun get-rates (&optional (symbols *default-symbols*))
   (let ((response (agets (json-request
   (let ((response (agets (json-request
                           +yahoo-url+
                           +yahoo-url+
-                          :parameters `(("symbols" . ,(format nil "~{~A~^,~}" pairs))))
+                          :parameters `(("symbols" . ,(format nil "~{~A~^,~}" symbols))))
                          "quoteResponse")))
                          "quoteResponse")))
     (when (aget "error" response)
     (when (aget "error" response)
       (error "Error in rates request"))
       (error "Error in rates request"))
     (loop for rate in (agets response "result")
     (loop for rate in (agets response "result")
-       collect (cons (or (aget "shortName" rate) (aget "symbol" rate))
+       collect (cons (aget "symbol" rate)
                      (aget "regularMarketPrice" rate)))))
                      (aget "regularMarketPrice" rate)))))
 
 
 (defun get-brent ()
 (defun get-brent ()
@@ -46,10 +46,34 @@
              "bpi" "USD" "rate_float")
              "bpi" "USD" "rate_float")
     (error (e) (log:error "~A" e))))
     (error (e) (log:error "~A" e))))
 
 
-(defun get-serie (series idx)
-  (loop for row in series
-     when (elt row idx)
-     collect (list (first row) (elt row idx))))
+
+;; Formatting
+(defvar *symbol-aliases*
+  (alexandria:plist-hash-table
+   '("BTCUSD" ("btc" "bitcoin" "биток")
+     "BRENT" ("чёрная" "нефть" "brent" "oil")
+     "USDRUB=X" ("зелёный" "бакс" "доллар" "usd")
+     "EURRUB=X" ("гейро" "eur" "евро")
+     "GBPRUB=X" ("британец" "gbp" "фунт" "стерлинг")
+     "CZKRUB=X" ("чешка" "czk")
+     "DKKRUB=X" ("дашка" "dkk"))
+   :test #'equal))
+
+(defun get-symbol-label (symbol)
+  (let ((aliases (gethash symbol *symbol-aliases*)))
+    (if aliases
+        (car aliases)
+        (let ((lower (string-downcase symbol)))
+          (when (equal "=x" (subseq lower (- (length lower) 2)))
+            (setf lower (subseq lower 0 (- (length lower) 2))))
+          (if (= 6 (length lower))
+              (format nil "~A/~A" (subseq lower 0 3) (subseq lower 3))
+              lower)))))
+
+;; Charts
+(defun get-serie (series symbol)
+  (mapcar #'(lambda (r) (list (car r) (caddr r)))
+          (remove symbol series :key #'cadr :test-not #'equal)))
 
 
 (defun range (seq &key (key 'identity))
 (defun range (seq &key (key 'identity))
   (loop
   (loop
@@ -60,8 +84,7 @@
      finally (return (values min max))))
      finally (return (values min max))))
 
 
 (defvar *chart-points* 800 "Data points in chart")
 (defvar *chart-points* 800 "Data points in chart")
-
-(defun make-chart (series names)
+(defun make-chart (series symbols)
   (let* ((r (multiple-value-list (range series :key #'car)))
   (let* ((r (multiple-value-list (range series :key #'car)))
          (fmt (if (<= (- (second r) (first r))
          (fmt (if (<= (- (second r) (first r))
                       (* 60 60 36))
                       (* 60 60 36))
@@ -69,9 +92,9 @@
                   '((:day 2) "." (:month 2) " " (:hour 2) ":" (:min 2)))))
                   '((:day 2) "." (:month 2) " " (:hour 2) ":" (:min 2)))))
     (adw-charting:with-line-chart ((+ 90 *chart-points*)
     (adw-charting:with-line-chart ((+ 90 *chart-points*)
                                    (floor (* 3 *chart-points*) 4))
                                    (floor (* 3 *chart-points*) 4))
-      (loop for serie in names
-         for idx from 1
-         do (adw-charting:add-series serie (get-serie series idx)))
+      (loop for symbol in symbols
+         do (adw-charting:add-series (get-symbol-label symbol)
+                                     (get-serie series symbol)))
       (adw-charting:set-axis
       (adw-charting:set-axis
        :x "Time" :draw-gridlines-p t
        :x "Time" :draw-gridlines-p t
        :label-formatter #'(lambda (v)
        :label-formatter #'(lambda (v)
@@ -84,54 +107,86 @@
 
 
 ;; Database
 ;; Database
 (def-db-init
 (def-db-init
-  (db-execute "create table if not exists finance (ts, usd, eur, gbp, brent, btc)")
-  (db-execute "create index if not exists fin_ts_ids on finance (ts)"))
-
-(defun db/add-finance (ts usd eur gbp brent btc)
-  (db-execute "insert into finance (ts, usd, eur, gbp, brent, btc) values (?, ?, ?, ?, ?, ?)"
-              ts usd eur gbp brent btc))
-
-(defun db/get-last-finance ()
-  (values-list (first (db-select "select ts, usd, eur, gbp, brent, btc from finance order by ts desc limit 1"))))
-
-(defparameter +finance-db-map+ '(("usd" . "USD/RUB")
-                                 ("eur" . "EUR/RUB")
-                                 ("gbp" . "GBP/RUB")
-                                 ("brent" . "Brent")
-                                 ("btc" . "BTC/USD")))
-
-(defun db/get-series (after-ts &optional (fields '("usd" "eur" "gbp" "brent")) (avg 60))
-  (when fields
+  (db-execute "create table if not exists finance_ticker (ts, symbol, price, primary key (ts, symbol)) without rowid")
+  (db-execute "create table if not exists finance_chat_symbols (chat_id, symbol, primary key (chat_id, symbol)) without rowid"))
+
+(defun migrate ()
+  (db-execute "delete from finance_ticker")
+  (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, usd from finance where usd is not null" "USDRUB=X")
+  (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, eur from finance where eur is not null" "EURRUB=X")
+  (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, gbp from finance where gbp is not null" "GBPRUB=X")
+  (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, brent from finance where brent is not null" "BRENT")
+  (db-execute "insert or ignore into finance_ticker (ts, symbol, price) select ts, ?, btc from finance where btc is not null" "BTCUSD"))
+
+(defun db/add-finance (ts symbol price)
+  (db-execute "insert into finance_ticker (ts, symbol, price) values (?, ?, ?)" ts symbol price))
+
+(defun db/get-last-finance (&optional (symbols *default-symbols*))
+  (loop for symbol in symbols
+       for last = (db-select "select ts, symbol, price from finance_ticker where symbol = ? order by ts desc limit 1"
+                             symbol)
+       when last append last))
+
+(defun db/get-chat-symbols (&optional chat-id)
+  (mapcar #'car
+          (if chat-id
+              (db-select "select symbol from finance_chat_symbols where chat_id = ?" chat-id)
+              (db-select "select distinct symbol from finance_chat_symbols"))))
+
+(defun db/set-chat-symbols (chat-id symbols)
+  (db-transaction
+    (db-execute "delete from finance_chat_symbols where chat_id = ?" chat-id)
+    (dolist (symbol symbols)
+      (db-execute "insert into finance_chat_symbols (chat_id, symbol) values (?, ?)"
+                  chat-id symbol))))
+
+(defun db/get-series (after-ts &optional (symbols *default-symbols*) (avg 60))
+  (when symbols
     (let ((sql (format nil
     (let ((sql (format nil
-                       "select ts/~a*~:*~a,~{avg(~a) as ~:*~a~^,~} from finance where ts >= ? group by ts/~a order by ts"
-                       avg fields avg)))
-      (values
-       (db-select sql (local-time:timestamp-to-unix after-ts))
-       (sublis +finance-db-map+ fields :test #'equal)))))
-
+                       "select ts/~a*~:*~a, symbol, avg(price) from finance_ticker where ts >= ? and symbol in (~{?~*~^, ~}) group by ts/~a, symbol order by ts"
+                       avg symbols avg)))
+      (apply #'db-select sql (local-time:timestamp-to-unix after-ts) symbols))))
 
 
 ;; Cron
 ;; Cron
 (defcron process-rates ()
 (defcron process-rates ()
-  (let ((ts (local-time:timestamp-to-unix (local-time:now)))
-        (rates (get-rates))
-        (brent (get-brent))
-        (btc (get-coindesk-btc-usd)))
-    (db/add-finance ts (aget "USDRUB=X" rates) (aget "EUR/RUB" rates) (aget "GBP/RUB" rates) brent btc)))
+  (let* ((ts (local-time:timestamp-to-unix (local-time:now)))
+         (symbols (db/get-chat-symbols))
+         (rates (when symbols (get-rates symbols)))
+         (brent (get-brent))
+         (btc (get-coindesk-btc-usd)))
+    (loop for (symbol . price) in rates
+       do (db/add-finance ts symbol price))
+    (when brent
+      (db/add-finance ts "BRENT" brent))
+    (when btc
+      (db/add-finance ts "BTCUSD" btc))))
 
 
 ;;; Hooks
 ;;; Hooks
+(defun get-label-symbol (label)
+  (or
+   (loop for symbol being the hash-keys in *symbol-aliases* using (hash-value aliases)
+      when (member label aliases :test #'string-equal)
+      do (return symbol))
+   (string-upcase label)))
+
 (def-message-cmd-handler handler-rates (:rates)
 (def-message-cmd-handler handler-rates (:rates)
-  (multiple-value-bind (ts usd eur gbp brent btc) (db/get-last-finance)
+  (when args
+    (db/set-chat-symbols chat-id (mapcar #'get-label-symbol args)))
+  (let ((last (db/get-last-finance (or (db/get-chat-symbols chat-id) *default-symbols*))))
     (bot-send-message chat-id
     (bot-send-message chat-id
-                      (format nil "Зеленый *~,2F*, гейро *~,2F*, британец *~,2F*, чёрная *~,2F*, btc *~,2F* @ _~A_"
-                              usd eur gbp brent btc
-                              (format-ts (local-time:unix-to-timestamp ts)))
-                      :parse-mode "Markdown")))
+                      (if last
+                          (format nil "~{~A *~$*~^, ~} @ _~A_"
+                                  (loop for (ts symbol price) in last
+                                     append (list (get-symbol-label symbol) price))
+                                  (format-ts (local-time:unix-to-timestamp (caar last))))
+                          "Пока нет данных. Если тикер реальный, проверь через минуту")
+                      :parse-mode "markdown")))
 
 
 (def-message-cmd-handler handler-charts (:charts)
 (def-message-cmd-handler handler-charts (:charts)
   (telegram-send-chat-action chat-id "upload_photo")
   (telegram-send-chat-action chat-id "upload_photo")
   (let* ((args (mapcar 'string-downcase args))
   (let* ((args (mapcar 'string-downcase args))
-         (all-fields (mapcar #'car +finance-db-map+))
-         (fields (or (intersection args all-fields :test 'equal) all-fields))
+         (all-symbols (db/get-chat-symbols))
+         (symbols (or (intersection (mapcar #'get-label-symbol args) all-symbols :test 'equal) all-symbols))
          (day-range (some #'(lambda (a) (aget a +chart-ranges+)) args))
          (day-range (some #'(lambda (a) (aget a +chart-ranges+)) args))
          (number (some #'(lambda (a) (parse-integer a :junk-allowed t)) args))
          (number (some #'(lambda (a) (parse-integer a :junk-allowed t)) args))
          (avg (* 60 (cond
          (avg (* 60 (cond
@@ -140,11 +195,11 @@
                       (:otherwise 1))))
                       (:otherwise 1))))
          (after-ts (local-time:timestamp- (local-time:now)
          (after-ts (local-time:timestamp- (local-time:now)
                                           (* avg *chart-points*) :sec))
                                           (* avg *chart-points*) :sec))
-         (rates (multiple-value-list (db/get-last-finance)))
-         (chart (apply #'make-chart (multiple-value-list
-                                     (db/get-series after-ts fields avg)))))
+         (rates (db/get-last-finance symbols))
+         (chart (make-chart (db/get-series after-ts symbols avg) symbols)))
     (telegram-send-photo chat-id chart
     (telegram-send-photo chat-id chart
                          :caption
                          :caption
-                         (format nil "Зеленый ~,2F, гейро ~,2F, британец ~,2F, чёрная ~,2F, btc ~,2F @ ~A"
-                                 (elt rates 1) (elt rates 2) (elt rates 3) (elt rates 4) (elt rates 5)
-                                 (format-ts (local-time:unix-to-timestamp (elt rates 0)))))))
+                         (format nil "~{~A ~$~^, ~} @ ~A"
+                                 (loop for (ts symbol price) in rates
+                                    append (list (get-symbol-label symbol) price))
+                                 (format-ts (local-time:unix-to-timestamp (caar rates)))))))